Spark:短命 executor 的彈性運算
· tech
📑 目錄
前三篇(Kafka、Redis、RabbitMQ)都在光譜的 stateful 那一端——狀態綁在自己的磁碟或記憶體,擴縮要搬資料、故障要救資料。這篇翻到光譜的另一端:Spark,系列第一個 stateless 的運算工具。同一套 體檢表套上去,你會發現它幾乎每一題都跟前面相反——而這一切,都源自一件事:Spark 幾乎不存狀態,它「借」外部的。
拓撲與狀態:算在叢集內,資料在叢集外
從 infra 的角度,這是最該先抓住的一點:Spark 自己近乎無狀態。 它上面看似有「狀態」的東西其實都是過渡性、可重算的——executor 本機磁碟上的 shuffle 中間資料(算壞了重算)、driver 記憶體裡的協調狀態(哪些 task 跑完了)。真正持久的資料,住在叢集外的 S3、資料庫、Kafka。它借別人的狀態來算,自己不保管——這一個事實,決定了它後面每一題 infra 的答案。
擴展:stateless 換來的甜頭
因為 executor 不保管資料,加減 executor 完全不用搬資料——這是無狀態最大的紅利。Spark 甚至能 dynamic allocation:按「還有多少 task 排隊(backlog)」自動長出、又自動收掉 executor:
HA、容量、監控、在 k8s 上
- HA:executor 可拋,driver 是命門。executor 掛了,Spark 用 lineage 重算那塊、或 task retry,完全不影響最終結果——這是 stateless 的韌性。但 driver 是單點:它掛了,整個 app 的協調狀態就沒了、作業直接死。所以實務上 driver 一定釘在穩定的 on-demand node、executor 才丟去便宜的 spot(這條原則我在 Airflow + Spark on K8s 那篇詳細畫過)。
- 容量:瓶頸在 executor 記憶體與 shuffle。每個 executor 的記憶體切成 execution(算)與 storage(快取)兩區;算不下就 spill 到本機磁碟,慢但不會死。最常見的 OOM 來自 data skew(某個 key 的資料特別多,壓垮一個 task)和 driver 上
collect()太多資料。 - 監控:看 stage/task 與 shuffle。Spark UI 看當下、History Server 看跑完的作業;重點盯 stage 進度、shuffle read/write 量、GC 時間、以及有沒有某個 task 拖很久(skew 的訊號)、executor lost 的次數。
- 在 k8s 上:K8s 直接當 cluster manager,取代 YARN。driver 與 executor 全部變成 Pod,
spark-submit --master k8s://…提交,Spark 直接呼叫 K8s API 要 executor pod。搭配 dynamic allocation,尖峰長出一堆 executor pod、算完即刪,只在真正運算時才佔資源。
反思
翻到光譜另一端,才看懂「無狀態」有多奢侈
連寫三篇有狀態工具再回頭看 Spark,我最大的感觸是:stateless 的那些好處——好擴、好搬、好回收、掛了重算就好——全都是拿「自己不保管狀態」換來的。 Spark 之所以能 dynamic allocation 隨開隨關,不是因為它比 Kafka「進步」,而是因為它把最難的那塊(持久資料)外包給了 S3、資料庫。這讓我對「stateless 好棒棒」的迷思清醒很多:沒有真正無狀態的系統,只有把狀態推去別處的系統。 那塊狀態不會消失,只是換一個地方、換一個人扛。看一個架構,我現在都會多問一句:它宣稱的「無狀態」,是把狀態丟到哪裡去了?
每個系統都有一塊「逃不掉的狀態」,認出它就掌握了命門
即使 Spark 近乎無狀態,它還是有一塊逃不掉的——driver 裡那份協調狀態。而它偏偏是單點、是整個作業的命門。這呼應了 體檢表那個核心提醒:再無狀態的系統,也總有一小塊狀態核心,而那塊往往就是它最脆弱、最該保護的地方。 Kafka 是磁碟上的 log、Redis 是記憶體、RabbitMQ 是 queue、Spark 是 driver 的協調——每個工具的「命門」都在它那塊狀態上。我看任何新系統,第一件事就是把這塊狀態找出來:它存在哪、掛了會怎樣、怎麼保護。找到它,就找到了這個系統 infra 上的七寸。(下一篇 Airflow 更是如此——它的真狀態藏在一個你可能沒注意到的 metadata DB 裡。)
「死了能不能重來」是我判斷一切彈性的那把尺
寫完這篇,dynamic allocation 那句話我想再強調一次:一個組件敢不敢隨開隨關、能不能水平彈性擴,根本不取決於它跑什麼,而取決於「它存不存資料、死了能不能重來」。 executor 能重來,所以放 spot、隨便縮;driver 不能重來,所以釘 on-demand、小心伺候。這把尺我後來套用在所有元件上——無狀態的運算層儘量做成可拋、可自動擴縮;有狀態的核心則收斂、保護、少動。先分清楚哪些「死了能重來」、哪些「死了要人命」,再決定各自該怎麼養——這是我做完 stateful、踏進 stateless 這批工具後,最想守住的一條設計紀律。