編排器替你做哪些決定
容器 runtime 負責把行程跑起來。編排器負責的是:這一刻該有幾份、放在哪一台、壞了要不要重啟、新版本怎麼換上去。
你交給它的不是一次 docker run,而是一份期望狀態:這個 Deployment 要 3 個這樣的 Pod,這個 Service 要接到帶某個標籤的 Pod。實際狀態隨時會偏離——節點死了、映像拉不下來、探活失敗。控制器下一輪再看期望與實際的差,把差補上。這件事做完不會結束,叢集活著它就一直做。
Scheduler 與 kubelet 都不直接開 etcd。期望狀態先進 API Server,由它寫進 etcd;其他人只對 API Server 做 watch 與更新。
面試時 Kubernetes 的物件停在這一層就夠。Pod 是排程與重啟的最小單位,裡面的容器共享 network namespace 與 volume。ReplicaSet 把同一個 Pod 模板維持在 N 份。Deployment 擁有 ReplicaSet,換模板時換的是 ReplicaSet,不是去改正在跑的容器。Service 的 ClusterIP 給一組 Pod 一個穩定的虛擬位址,後端名單來自標籤選出的 EndpointSlice。節點上的 kubelet 只實現「綁到我身上的 Pod」。
ECS 的詞彙可以對照看,但不是同一個物件。Task Definition 像 Pod 模板,Task 像一個跑起來的 Pod,Service 同時承擔「維持份數」和「掛上負載平衡」這兩件在 Kubernetes 裡分開的事。Cluster 是這批容量的邊界。控制平面是 AWS 託管的,你看不到 etcd,決策的形狀仍是:期望份數在一邊,實際 Task 在另一邊,差多少就補多少。
Operator、CRD、CNI 與叢集升級留在 Kubernetes 專篇。副本數與節點數怎麼跟著指標變,是自動擴縮策略的題;這裡把節點集合當成已經給定。藍綠與金絲雀怎麼切流量,是發佈策略要比較的題;這裡只講 Rolling Update 怎麼替換副本。
排程只加總 request
節點選得對不對,看的是 request 的向量裝箱,不是 limit,也不是程式實際用了多少。
kube-scheduler 分兩段。Filter 淘汰放不下的節點:資源不夠、節點被標成不可排程、taint 沒有對應的 toleration、nodeSelector 或 node affinity 不合、拓樸分散沒滿足。Score 再給剩下的節點打分,取得分最高的,把 nodeName 寫回去。資源這一個分數的預設策略是 LeastAllocated,CPU 與記憶體權重相同:剩餘比例越高分越高,所以傾向把 Pod 鋪開。要改成往少數節點擠,得把 NodeResourcesFit 的計分改成 MostAllocated 或 RequestedToCapacityRatio。那是另外開的 bin packing,不是預設。
ECS 的 EC2 launch type 把策略寫在 Service 上,可以串多個,前面的先做。類型有三種:binpack 把 Task 放到「該維度剩餘最少、但還放得下」的實例,維度是 CPU 或 memory,目的是少用幾台;spread 依可用區或 instanceId 鋪開;random 不挑。Service 沒寫策略時,預設是依 attribute:ecs.availability-zone 做 spread,不是 binpack。Fargate 沒有實例可挑,這些策略用不上,服務會盡力在可用區之間鋪開。
兩邊共同的約束是兩個維度要同一台同時滿足。Pod(或 Task)能放上節點 i,當且僅當:
叢集加總還夠、沒有任何一台同時夠,就是碎片。CPU 剩在記憶體已經用完的節點上,對下一個又要 CPU 又要記憶體的 Pod 沒有用。排程器加總的是 request,不是 limit,也不是現在的用量。limit 是執行期的 cgroup:CPU 超了會被 throttle,不會被殺;記憶體超了,那個容器會被 OOM kill。把 limit 設得很大,不會讓這個 Pod 更容易排進去。
| QoS | 怎麼判定 | 節點記憶體不夠時 |
|---|---|---|
| Guaranteed | 每個容器的 CPU、記憶體都有 request,且 request 等於 limit | 最後才被考慮趕走 |
| Burstable | 至少有一個 request,但不是 Guaranteed | 用量超過自己 request 的先被考慮 |
| BestEffort | 完全沒有 request 與 limit | 最先被考慮 |
排程與「這份容量有沒有被預訂」看 request。BestEffort 在排程上像免費的,記憶體壓力來時卻最先讓位。
服務網格或日誌 sidecar 的 request 與應用容器加在一起,才是排程器看到的數字。應用自己寫 0.5 vCPU,sidecar 再寫 0.5,這顆 Pod 就是 1。忘了設 request 的 BestEffort 容器不佔排程,卻佔真實記憶體,節點上的其他 Pod 會替它付帳。
下面的畫布把兩種目標收成看得到的規則,方便對碎片,不是 kube-scheduler 的逐插件複製品。Binpack 先放 request 總和較大的 Pod,再塞進剩餘 CPU 與記憶體之和最小、但仍放得下的節點。Spread 依 web、worker、cache、batch 的順序,放進剩餘之和最大的節點。LeastAllocated 比的是剩餘比例,還會加上親和性與拓樸分數;ECS 的 binpack 只盯一個維度,而且常常先依可用區 spread。方向與這兩顆按鈕相同,格子不會逐格相同。滑桿上的節點容量請當成 allocatable——機器規格扣掉 kubelet、作業系統與驅逐水位之後剩給 Pod 的那一截——不是雲端實例的標稱 vCPU。
預設三台各 3 vCPU / 4 GiB。Binpack 會讓兩個 cache 各佔滿一台的記憶體,batch 與 worker 擠在第三台,兩個 web Pending——剩餘的 CPU 與那 1 GiB 不在同一台。改成 Spread 後,Pending 會換成兩個 cache:記憶體被拆開,沒有一台還剩 4 GiB。點節點是讓它在 filter 就被淘汰;真實的 cordon 不會趕走已經在上面的 Pod,要趕走得 drain。畫面每次從空叢集重算,所以看不到「舊 Pod 留在不可排程的節點上」。
對帳是一輪一輪補差
控制器不把「收到一個事件」當成只做一次的指令。它反覆讀期望與現況,差多少做多少。
這種迴圈是 level-triggered。ReplicaSet 想要 3 個 Pod、現在只有 1 個,它就再建立 2 個,不管少掉的那兩個是剛被刪掉、還是建立請求根本沒送成功。如果控制器只在「刪除事件」那一刻反應,而那則 watch 剛好在它重啟時漏了,缺口會永遠留著。下一輪 list 會再看到 1 不等於 3。Watch 負責快,定期重新列出負責補漏。動作必須冪等:多做一輪不能把 3 份做成 6 份。
協調服務與分散式鎖那篇的結論直接用在這裡。etcd 是小而強一致的鍵值儲存,寫入要付 Raft 多數派的往返,Watch 沿著 revision 追,不適合當業務資料平面。Kubernetes 的物件就是這種元資料:誰該跑、跑幾份、綁在哪一台、探活結果。訂單與訊息本體不進 etcd。API Server 是唯一寫入者;Scheduler、控制器與 kubelet 都走 API,不自己打開 etcd。
sequenceDiagram
participant Dev as 開發者
participant API as API Server
participant ET as etcd
participant RS as ReplicaSet
participant SC as Scheduler
participant KL as kubelet
Dev->>API: apply Deployment
API->>ET: 寫入期望
RS->>API: 補齊 Pod 數
SC->>API: 綁定節點
KL->>API: 回報 Ready
Deployment 控制器先讓 ReplicaSet 的模板與份數符合期望;圖上的 ReplicaSet 再補 Pod。綁定節點、拉映像、變成 Ready 是後面的寫入。任何一步失敗,下一輪對帳再補,不是同一筆交易一起提交或一起回滾。
所以「apply 成功」只表示期望狀態寫進去了。Pod 還可能 Pending,理由在事件裡:排不進、映像拉失敗、還沒通過探活。排不進是第 2 節的裝箱結果,排程器沒有壞;它會留在 Pending,直到有節點放得下,或你把 request 改小。節點不夠時要不要加機器,是自動擴縮策略的回授迴圈,不是這一輪對帳會變出來的。
誰該重啟,誰只該停止接流量
liveness 回答行程是不是死掉了。readiness 回答現在該不該把請求送過來。兩件事混在同一支探活裡,就會在該摘流量的時候把行程殺掉。
kubelet 對每個容器跑探活。startup 成功之前,liveness 與 readiness 都先不跑,用來放過小啟動:慢的行程不會在起來之前被 liveness 殺掉。startup 超過失敗門檻,kubelet 仍會重啟容器。過了 startup 之後,liveness 失敗超過門檻就殺進程並依重啟策略拉起來;readiness 失敗只把這顆 Pod 從 Service 的端點拿掉,行程繼續跑,恢復後再加回去。沒有寫 readiness 的 Pod,一旦容器跑起來就會被放進端點。
同一種「下游變慢」,readiness 失敗是把流量移走,行程還在;liveness 失敗是把行程殺掉。該摘流量的時候用 readiness。
資料庫或下游一慢,liveness 跟著失敗,kubelet 就重啟容器。重啟後的行程再打同一個下游,雪崩被放大。依賴不健康時讓 readiness 失敗,把這顆 Pod 移出端點,行程留著等下游回來。liveness 只留給死鎖、卡住的事件迴圈這種「重啟才有機會好」的故障。
Deployment 預設用 RollingUpdate,不是 Recreate。Recreate 會先刪光舊 Pod 再起新的,中間沒有可用副本。RollingUpdate 用兩個數控制替換:maxUnavailable 預設 25%,百分比無條件捨去,表示可以暫時少於期望份數多少;maxSurge 預設 25%,百分比無條件進位,表示可以暫時多於期望份數多少。兩者不能同時為 0。期望 2 份時,25% 的 unavailable 是 0、surge 是 1:先多起一個新 Pod,新的 Ready 之後再拆舊的。期望 1 份時也是先湧出第二個,而不是先殺光。
新 Pod 要 Ready 才算 available。minReadySeconds 預設 0,一 Ready 就算數。新版本一直不 Ready,舊 ReplicaSet 會留著;超過 progressDeadlineSeconds(預設 600 秒)Deployment 標上 ProgressDeadlineExceeded,控制器仍不會把舊的一次拆掉。這是停住,不是整組下線。
surge 出去的那顆 Pod 還是要過第 2 節的裝箱。節點的 request 已經塞滿時,新 Pod Pending,更新就停在那裡。把叢集 bin pack 到沒有空洞,滾動更新會缺一台臨時的位置。鋪開的排程比較容易留下這點空間,代價是多佔節點。流量要不要先送 5% 到新版,是發佈策略的題;這裡只決定副本怎麼替換。
要哪一種 API,以及要不要自己養節點
ECS、EKS、GKE 選的是你要維護的控制平面 API。節點與 Fargate 選的是碎片落在叢集上,還是落在單一任務的形狀裡。
| ECS | EKS | GKE | |
|---|---|---|---|
| 你寫的 API | Task、Service、Cluster | Kubernetes API | Kubernetes API |
| 控制平面 | AWS 全管,沒有 etcd 給你碰 | AWS 管控制平面,按叢集另外計費;工作者要自己準備 | Google 管控制平面。Standard 仍有節點;Autopilot 連節點一起抽象掉 |
| 排程 | EC2 上用 binpack/spread/random。Service 預設依可用區 spread | kube-scheduler 的 filter 與 score。預設資源分是 LeastAllocated | 同一套 kube-scheduler。Autopilot 你碰不到節點層的裝箱 |
| 帶走的東西 | AWS 整合直接,換雲就是換模型 | Helm、標準控制器、別的 Kubernetes 叢集 | 與 EKS 相同的工作負載 API。Autopilot 少了節點層的自由 |
選 ECS 是接受 AWS 自己的任務模型。選 EKS 或 GKE 是接受 Kubernetes API,再決定節點要不要出現在你的帳上。EKS 的工作者可以是 managed node group,也可以是 Fargate profile。
| 自己的節點 | Fargate | |
|---|---|---|
| 帳單上的單位 | 整台機器的時間。第 2 節那些塞不滿的洞,機器還在計費 | 任務或 Pod 的 vCPU 與記憶體時間。沒有跨任務的節點碎片 |
| 形狀 | request 可以依容器加總,只要節點的 allocatable 放得下 | 合法的 vCPU/記憶體是離散配對。為了記憶體把 vCPU 一起抬高,浪費在任務內部 |
| 節點代理 | DaemonSet、ECS 的 daemon scheduling 都可以,一台一份 | Fargate 不支援 daemon scheduling,也不跑 DaemonSet |
| GPU 與隔離 | 加速器裝在節點上。同節點的容器靠 cgroup 分開 | 運算模型光譜篇對過文件:Fargate 沒有 GPU 與 Inferentia。任務跑在各自的 microVM 裡 |
節點模型用第 2 節的畫布看碎片:剩餘 CPU 若跟記憶體不在同一台,那台機器的錢已經花了。Fargate 把這個洞消掉,改成每個任務為自己的形狀付費。運算模型光譜比較的是時間與單價,沒有把這個洞算進編排那一欄。
GKE Autopilot 站在 Fargate 這一側的「不養節點」:計費落在 Pod 的 request 上,bin pack 由平台做,你排不了節點。DaemonSet、主機網路這類節點能力有限制,以雲端文件上的允許清單為準。它仍是 Kubernetes API,不是 ECS 的 Task。
可以這樣收束,不必先算完帳單。工作負載都在 AWS、團隊不想經營 Kubernetes API,用 ECS。要 Helm、標準控制器、或同一套 YAML 換叢集,用 EKS 或 GKE。穩定高占用、需要 DaemonSet 或 GPU,用自己的節點,並用第 2 節把碎片算出來;滾動更新還要留得下 maxSurge 那一顆。流量起伏、形狀落在 Fargate 的合法配對裡、不需要節點代理,用 Fargate,把「塞滿機器」這個問題交回去。節點不夠時加機器,以及副本數怎麼跟指標走,仍是自動擴縮策略的事。