與心跳通信源碼解析:注冊機(jī)制與容錯(cuò)設(shè)計(jì))
1. 從一個(gè)被忽略的啟動(dòng)細(xì)節(jié)說起很多人第一次翻 Spark 源碼注意力基本都被 Master 和 Driver 吸走了Worker 往往被當(dāng)成一個(gè)“干活的節(jié)點(diǎn)”草草略過。但真在生產(chǎn)環(huán)境里踩過坑的人都清楚集群里最先出問題的往往不是 Master而是某個(gè) Worker 掉線、心跳超時(shí)、資源上報(bào)對不上。我印象很深的一次一個(gè)測試集群里三臺(tái) Worker 里有一臺(tái)總是過一會(huì)兒就被 Master 標(biāo)記成 Dead日志里翻來翻去只有一句“Worker removed”最后定位下來是心跳線程被一個(gè)長任務(wù)阻塞了。從那以后我就養(yǎng)成了一個(gè)習(xí)慣看 Spark 的分布式協(xié)調(diào)先看 Worker 怎么起來、怎么跟 Master 保持聯(lián)系這條鏈路通了整個(gè)集群的“生命體征”才算穩(wěn)。這篇內(nèi)容就是圍繞Spark 源碼解析里 Worker 的啟動(dòng)流程以及它跟 Master 的心跳通信展開的。我會(huì)把 Worker 從main方法進(jìn)入、到注冊成功、再到周期性心跳的整條鏈路拆開講重點(diǎn)放在為什么這么設(shè)計(jì)、關(guān)鍵參數(shù)怎么算、實(shí)際排查時(shí)看哪里。適合已經(jīng)能跑起 Spark 集群、想進(jìn)一步理解內(nèi)部協(xié)調(diào)機(jī)制的開發(fā)者也適合正在做資源調(diào)度相關(guān)二次開發(fā)的同學(xué)。讀完之后你應(yīng)該能自己回答Worker 啟動(dòng)時(shí)到底做了哪幾件事、心跳里到底帶了什么、Master 收到心跳后怎么判斷一個(gè) Worker 還活著。在展開之前先給個(gè)整體印象Worker 本質(zhì)上是一個(gè)長期駐留的進(jìn)程它做兩件核心事——向 Master 匯報(bào)自己有多少資源、還活著以及接收 Master 的指令去啟動(dòng)或殺死 Executor。啟動(dòng)流程解決的是“我怎么讓 Master 認(rèn)識(shí)我”心跳通信解決的是“我怎么讓 Master 一直記得我”。這兩件事看著簡單但里面藏著 Spark 獨(dú)立部署模式Standalone最核心的容錯(cuò)邏輯。2. Worker 啟動(dòng)流程的整體設(shè)計(jì)與思路拆解2.1 為什么 Worker 要設(shè)計(jì)成“先注冊、后心跳”兩段式如果你去看 Worker 的源碼入口會(huì)發(fā)現(xiàn)它并沒有一上來就瘋狂發(fā)心跳而是老老實(shí)實(shí)先走一遍注冊。這個(gè)設(shè)計(jì)不是多余的。分布式系統(tǒng)里一個(gè)節(jié)點(diǎn)要加入集群必須先讓協(xié)調(diào)者這里是 Master知道“我是誰、我在哪、我有什么”這個(gè)動(dòng)作就是注冊。注冊成功之后雙方才建立一種“約定好的周期性聯(lián)系”也就是心跳。為什么不能直接用心跳代替注冊因?yàn)樾奶禽p量的、高頻的它只適合傳遞“我還活著”這種增量信息。而注冊需要攜帶完整的身份和資源描述是一次性的、重量級的交互。把兩者分開好處是心跳可以做得非常薄網(wǎng)絡(luò)開銷小、處理快注冊則可以做得比較重允許做校驗(yàn)、持久化、狀態(tài)變更。這跟現(xiàn)實(shí)里“入職登記”和“每天打卡”是一個(gè)道理——登記要填一堆表打卡只需要刷個(gè)臉。從源碼結(jié)構(gòu)上看Worker 繼承自ThreadSafeRpcEndpoint這意味著它本身就是一個(gè) RPC 端點(diǎn)能收發(fā)消息。它的生命周期由onStart觸發(fā)注冊和心跳的啟動(dòng)都掛在這個(gè)階段。理解這一點(diǎn)很關(guān)鍵Worker 不是一個(gè)簡單的 main 函數(shù)跑完就結(jié)束的進(jìn)程而是一個(gè)常駐的 RPC 服務(wù)main 只是把它拉起來真正的工作都在消息循環(huán)里。2.2 啟動(dòng)階段涉及的核心組件與職責(zé)劃分把 Worker 啟動(dòng)這條鏈路拆開主要涉及這么幾個(gè)角色。Worker 自身負(fù)責(zé)組織注冊信息、發(fā)起注冊、啟動(dòng)心跳線程。Master負(fù)責(zé)接收注冊、校驗(yàn)、把 Worker 加入自己的資源池并在收到心跳時(shí)更新 Worker 的活躍狀態(tài)。中間還有一個(gè)RpcEnv它是 Spark 自己封裝的一套 RPC 通信環(huán)境負(fù)責(zé)端點(diǎn)注冊、消息投遞、遠(yuǎn)程調(diào)用。你可以把 RpcEnv 理解成“郵局”Worker 和 Master 都是郵局里的信箱消息通過郵局投遞。這里有個(gè)容易混淆的點(diǎn)Worker 注冊時(shí)發(fā)給 Master 的RegisterWorker消息里帶的不只是主機(jī)和端口還有cores、memory、webUiAddress等資源信息。這些信息是 Master 后續(xù)做資源分配的依據(jù)。所以注冊信息一旦上報(bào)錯(cuò)了比如 cores 報(bào)多了Master 就會(huì)超發(fā)任務(wù)最后 Executor 起不來。這也是為什么我強(qiáng)調(diào)注冊階段值得細(xì)看——它是資源賬本的第一次寫入。2.3 啟動(dòng)流程的時(shí)序脈絡(luò)用文字把時(shí)序捋一遍Worker 進(jìn)程啟動(dòng)創(chuàng)建 RpcEnv實(shí)例化 Worker 端點(diǎn)并注冊到 RpcEnv然后onStart被調(diào)用。在onStart里Worker 先嘗試向 Master 發(fā)起注冊拿到注冊響應(yīng)后啟動(dòng)一個(gè)心跳線程之后進(jìn)入消息循環(huán)等待 Master 的指令。如果注冊失敗比如 Master 還沒起來Worker 會(huì)重試而不是直接退出。這個(gè)重試機(jī)制是 Standalone 模式能“先起 Worker 后起 Master”的原因。我特別想強(qiáng)調(diào)onStart這個(gè)鉤子。很多初學(xué)者以為注冊是在 main 里直接調(diào)的其實(shí)不是。Spark 把端點(diǎn)的啟動(dòng)邏輯收斂到onStart是為了保證“端點(diǎn)已經(jīng)準(zhǔn)備好接收消息”之后才去做注冊這種對外交互。如果順序反了Master 回消息的時(shí)候 Worker 還沒準(zhǔn)備好接收就會(huì)丟消息。這種“先就緒、再對外”的順序是寫分布式程序的基本素養(yǎng)。3. 核心細(xì)節(jié)解析與實(shí)操要點(diǎn)3.1 注冊消息里到底裝了什么RegisterWorker這個(gè)消息體是理解啟動(dòng)流程的關(guān)鍵。它主要包含workerIdWorker 的唯一標(biāo)識(shí)、host、port、cores、memory、webUiAddress以及 endpoint 的引用。workerId 的生成規(guī)則值得一提它通常是host:port拼出來的或者帶一個(gè)隨機(jī)后綴保證同一臺(tái)機(jī)器上多個(gè) Worker 實(shí)例不會(huì)撞 ID。cores 和 memory 來自啟動(dòng)參數(shù)或者配置文件是 Master 做調(diào)度的硬約束。這里有個(gè)實(shí)操要點(diǎn)memory 上報(bào)的是 Worker 可用于 Executor 的內(nèi)存不是機(jī)器物理內(nèi)存。如果你在spark-env.sh里配了SPARK_WORKER_MEMORY那上報(bào)的就是這個(gè)值。很多人機(jī)器有 64G但只配了 8G然后納悶為什么只能跑一個(gè)小 Executor問題就出在這。cores 同理SPARK_WORKER_CORES決定上報(bào)多少核。這兩個(gè)參數(shù)是 Worker 啟動(dòng)時(shí)最該確認(rèn)的東西。3.2 注冊響應(yīng)與失敗重試的處理邏輯Master 收到注冊后會(huì)返回一個(gè)RegisteredWorker或者RegisterWorkerFailed。成功的話響應(yīng)里會(huì)帶上 Master 的地址Worker 拿到之后就知道以后心跳往哪發(fā)。失敗的話Worker 不會(huì)立刻放棄而是走一個(gè)重試邏輯。這個(gè)重試不是無腦循環(huán)通常會(huì)有一個(gè)間隔避免把 Master 打爆。我在實(shí)際調(diào)試時(shí)發(fā)現(xiàn)如果 Master 一直沒起來Worker 日志里會(huì)反復(fù)出現(xiàn)注冊嘗試的記錄間隔比較規(guī)律。這個(gè)設(shè)計(jì)的好處是運(yùn)維友好——你可以先批量拉起 Worker再起 Master集群能自己收斂。但要注意如果重試間隔配得太短Master 剛起來那一瞬間可能被大量 Worker 同時(shí)注冊沖垮所以生產(chǎn)上 Worker 數(shù)量多的時(shí)候啟動(dòng)最好錯(cuò)峰。3.3 心跳線程的啟動(dòng)時(shí)機(jī)與參數(shù)注冊成功后Worker 會(huì)啟動(dòng)一個(gè)心跳線程核心是sendHeartbeat的周期性調(diào)用。心跳間隔由spark.worker.timeout相關(guān)配置間接決定默認(rèn)情況下 Worker 每隔一段時(shí)間發(fā)一次心跳Master 如果在超時(shí)時(shí)間內(nèi)沒收到就把這個(gè) Worker 標(biāo)記為 Dead。這里有個(gè)經(jīng)典的計(jì)算關(guān)系心跳間隔要明顯小于超時(shí)時(shí)間一般超時(shí)時(shí)間是心跳間隔的若干倍留出容錯(cuò)余量。提示心跳間隔和超時(shí)時(shí)間是一對需要配合的參數(shù)。間隔太接近超時(shí)網(wǎng)絡(luò)稍微抖一下 Worker 就被判死間隔太小Master 處理心跳的壓力就上來了。生產(chǎn)上建議保持默認(rèn)比例不要單獨(dú)去調(diào)其中一個(gè)。3.4 心跳消息的內(nèi)容與 Master 的更新動(dòng)作心跳消息本身很輕主要就是 workerId。Master 收到后會(huì)去自己的workers映射里找到對應(yīng)的 WorkerInfo更新它的lastHeartbeat時(shí)間戳。這個(gè)時(shí)間戳就是判斷存活的依據(jù)。Master 內(nèi)部還有一個(gè)后臺(tái)線程定期掃描所有 Worker把lastHeartbeat超過閾值的標(biāo)記為 Dead并觸發(fā)資源回收。這里有個(gè)細(xì)節(jié)值得注意心跳只更新活躍時(shí)間不重新上報(bào)資源。也就是說Worker 運(yùn)行期間如果資源發(fā)生變化比如動(dòng)態(tài)調(diào)整心跳是不會(huì)自動(dòng)同步的。資源變更需要走另外的消息。理解這一點(diǎn)能幫你避免“改了配置為什么 Master 沒感知”的困惑。4. 實(shí)操過程與核心環(huán)節(jié)實(shí)現(xiàn)4.1 從 main 到 onStart 的完整鏈路Worker 的入口是Worker.main。它做的事情依次是解析命令行參數(shù)、構(gòu)造WorkerArguments、讀取配置、創(chuàng)建RpcEnv、實(shí)例化Worker端點(diǎn)、把端點(diǎn)注冊到 RpcEnv、最后調(diào)用rpcEnv.awaitTermination()讓進(jìn)程掛住。注意最后這一步它讓 Worker 進(jìn)程不會(huì)因?yàn)?main 執(zhí)行完就退出而是持續(xù)等待消息。Worker的構(gòu)造過程里會(huì)初始化一堆狀態(tài)比如cores、memory、masterRpcAddress、workDir等。其中workDir是 Worker 存放 Executor 工作目錄的地方默認(rèn)在 Spark 安裝目錄下的work文件夾。這個(gè)目錄如果磁盤滿了Executor 會(huì)起不來而且報(bào)錯(cuò)往往不直觀。我踩過一次坑日志里只看到 Executor 啟動(dòng)失敗最后發(fā)現(xiàn)是 work 目錄所在分區(qū)滿了。4.2 注冊消息的構(gòu)造與發(fā)送在onStart里Worker 會(huì)調(diào)用registerWithMaster。這個(gè)方法會(huì)構(gòu)造RegisterWorker消息通過masterRpcAddress對應(yīng)的 endpoint 發(fā)出去。如果配置了多個(gè) Master高可用場景Worker 會(huì)嘗試向所有 Master 發(fā)注冊誰先回就認(rèn)誰。這個(gè)邏輯保證了 Master 切換時(shí) Worker 能重新找到組織。發(fā)送注冊之后Worker 會(huì)設(shè)置一個(gè)定時(shí)器或者用receiveAndReply等待響應(yīng)。收到RegisteredWorker后它會(huì)記錄 Master 的地址然后調(diào)用startHeartbeat啟動(dòng)心跳線程。整個(gè)鏈路是串行的注冊沒成功心跳不會(huì)啟動(dòng)。這個(gè)順序保證了心跳一定發(fā)給了“認(rèn)識(shí)自己”的 Master。4.3 心跳線程的具體實(shí)現(xiàn)心跳線程通常是一個(gè)ScheduledExecutorService或者基于RpcEndpoint的定時(shí)消息。它會(huì)周期性地向 Master 發(fā)送Heartbeat(workerId)。Master 端在receive里處理這個(gè)消息更新對應(yīng) WorkerInfo 的lastHeartbeat。如果 Master 找不到這個(gè) workerId說明 Worker 已經(jīng)被移除會(huì)回一個(gè)ReconnectWorker之類的消息讓 Worker 重新注冊。這個(gè)“找不到就要求重連”的機(jī)制很巧妙。它處理了一種邊界情況Worker 以為自己還注冊著但 Master 因?yàn)槟撤N原因已經(jīng)把它清掉了比如 Master 重啟后狀態(tài)丟失。這時(shí)候心跳會(huì)觸發(fā)重新注冊讓集群自愈。我在測試 Master 重啟時(shí)觀察到過這個(gè)行為Worker 日志里會(huì)先報(bào)心跳異常然后自動(dòng)重新注冊整個(gè)過程不需要人工干預(yù)。4.4 參數(shù)計(jì)算與配置示例假設(shè)你要部署一個(gè) Worker機(jī)器有 32 核、128G 內(nèi)存但你希望留一部分給系統(tǒng)和其他服務(wù)。一個(gè)常見的配置是export SPARK_WORKER_CORES24 export SPARK_WORKER_MEMORY96g export SPARK_WORKER_INSTANCES1這里 cores 給 24 是留了 8 核給系統(tǒng)memory 給 96g 是留了 32g 給系統(tǒng)和緩存。如果你要在一臺(tái)機(jī)器上跑多個(gè) Worker 實(shí)例SPARK_WORKER_INSTANCES就派上用場了但要注意每個(gè)實(shí)例的 cores 和 memory 要相應(yīng)減少否則會(huì)超發(fā)。比如兩個(gè)實(shí)例每個(gè)給 12 核、48g。心跳相關(guān)的配置通常不需要改但如果你網(wǎng)絡(luò)環(huán)境特殊可以關(guān)注spark.worker.timeout。它的默認(rèn)值是基于心跳間隔算出來的改的時(shí)候要成比例地改別只動(dòng)一個(gè)。5. 常見問題與排查技巧實(shí)錄5.1 Worker 注冊不上 Master 的排查順序注冊失敗是最常見的問題。排查順序我一般是這樣先看 Worker 日志里有沒有“Connecting to Master”之類的記錄確認(rèn)它有沒有在嘗試連接再看 Master 的地址配得對不對spark.master或者SPARK_MASTER_HOST有沒有寫錯(cuò)然后確認(rèn)網(wǎng)絡(luò)通不通端口有沒有被防火墻擋最后看 Master 日志里有沒有收到注冊請求。如果 Master 壓根沒收到問題基本在網(wǎng)絡(luò)或地址如果收到了但拒絕問題在資源校驗(yàn)或版本不匹配。注意Master 和 Worker 的版本必須一致混用不同版本的 Spark 組件經(jīng)常導(dǎo)致注冊失敗而且報(bào)錯(cuò)信息不一定直白。5.2 心跳超時(shí)導(dǎo)致 Worker 被標(biāo)記 Dead這個(gè)問題的表現(xiàn)是 Worker 莫名其妙變成 Dead但進(jìn)程還在。原因通常是心跳線程被阻塞了。比如 Worker 上跑了一個(gè)特別重的任務(wù)把線程池占滿心跳線程搶不到資源。解決辦法是給心跳線程獨(dú)立的線程池或者排查是什么任務(wù)阻塞了 Worker 的主線程。我在一次壓測里遇到過某個(gè) Executor 啟動(dòng)時(shí)做了大量磁盤 IO把 Worker 的調(diào)度線程卡住了心跳發(fā)不出去最后被 Master 判死。5.3 常見問題速查表現(xiàn)象可能原因排查方向Worker 日志反復(fù)注冊失敗Master 地址錯(cuò)、網(wǎng)絡(luò)不通、版本不一致檢查配置、網(wǎng)絡(luò)、版本W(wǎng)orker 被標(biāo)記 Dead 但進(jìn)程存活心跳線程阻塞、GC 停頓過長看線程棧、GC 日志Master 重啟后 Worker 失聯(lián)Master 狀態(tài)丟失、Worker 未重連看是否觸發(fā)重新注冊資源上報(bào)與實(shí)際不符cores/memory 配置錯(cuò)誤核對啟動(dòng)參數(shù)心跳正常但任務(wù)起不來work 目錄滿、權(quán)限問題檢查磁盤和權(quán)限5.4 幾個(gè)我踩過的坑第一個(gè)坑是 work 目錄權(quán)限。Worker 啟動(dòng)用戶和 Executor 運(yùn)行用戶如果不一致Executor 目錄創(chuàng)建會(huì)失敗而且報(bào)錯(cuò)藏在很深的日志里。第二個(gè)坑是主機(jī)名解析。Worker 上報(bào)的 host 如果 Master 解析不了Master 就無法反向連接 Worker導(dǎo)致任務(wù)下發(fā)失敗。第三個(gè)坑是心跳間隔和超時(shí)時(shí)間被一起改小了結(jié)果網(wǎng)絡(luò)一抖動(dòng)就大面積判死集群反復(fù)震蕩。這幾個(gè)坑的共同點(diǎn)是報(bào)錯(cuò)信息都不直接指向根因需要你對整條鏈路有理解才能快速定位。6. 從源碼理解到生產(chǎn)實(shí)踐的幾點(diǎn)體會(huì)把 Worker 啟動(dòng)和心跳這條鏈路看透之后我對 Spark Standalone 的容錯(cuò)設(shè)計(jì)有了更具體的感受。它沒有用很復(fù)雜的共識(shí)算法而是用“注冊 心跳 超時(shí)判死 自動(dòng)重連”這套樸素機(jī)制撐起了整個(gè)集群的成員管理。這套機(jī)制的關(guān)鍵在于參數(shù)之間的配合以及消息處理的順序。任何一個(gè)環(huán)節(jié)的順序錯(cuò)了或者參數(shù)比例失調(diào)都會(huì)表現(xiàn)為“Worker 不穩(wěn)定”。如果你正在做資源調(diào)度相關(guān)的開發(fā)我建議重點(diǎn)看Master里處理RegisterWorker和Heartbeat的那兩個(gè)分支以及Worker里registerWithMaster和startHeartbeat的實(shí)現(xiàn)。把這幾段代碼和實(shí)際日志對照著看很多之前覺得玄乎的現(xiàn)象都會(huì)變得清晰。最后分享一個(gè)小技巧調(diào)試心跳問題時(shí)把 Worker 和 Master 的日志級別調(diào)到 DEBUG能看到每條心跳的收發(fā)記錄比猜要快得多。