
凌晨兩點線上某服務(wù)連著三次重啟kubectl get events里OOMKilled紅通通地躺著。第一反應(yīng)很直接把 memory 的 limit 從 512Mi 加到 1024Mi重新發(fā)布然后坐等。結(jié)果第二天凌晨同樣的時間點Evicted又來了。這類場景我見過太多次問題不在“l(fā)imit 不夠大”而在大家把 Pod 資源模型想成了一行數(shù)字但 K8S 里這套資源機制從調(diào)度器一路貫穿到內(nèi)核的 cgroup 和 OOM Killer每個字段管的是不同層的事。這篇內(nèi)容我就把 Pod 資源這條鏈路從頭到尾捋一遍把 requests、limits、QoS、驅(qū)逐排序這些概念串起來講適合正在被 Pod 無故被殺困擾、或者想把資源配到心里有數(shù)的人。1. 從Pod被殺現(xiàn)場說起資源這條隱形鏈路到底連著哪些內(nèi)核機制1.1 三種“死亡方式”的差異OOMKilled、Evicted 與啟動即失敗很多人看到 Pod 狀態(tài)不對第一反應(yīng)都是“加內(nèi)存”。但 Pod 死法不一樣背后對應(yīng)的機制完全不同不先分清再動手基本白改。OOMKilled是容器內(nèi)進程觸發(fā)了內(nèi)核 OOM Killer。最常見的原因有兩個一是容器 cgroup 內(nèi)存上限設(shè)得太低進程申請內(nèi)存時被內(nèi)核直接干掉二是整臺節(jié)點的物理內(nèi)存被耗盡內(nèi)核開始全局掃殺。兩者的共同點是動手殺進程的不是 Kubernetes而是操作系統(tǒng)內(nèi)核。Evicted則完全是另一回事它是 kubelet 主動驅(qū)逐。kubelet 每隔一段時間檢查節(jié)點上的驅(qū)逐信號比如memory.available太低、nodefs.available太低、imagefs.available太低一旦越過閾值它會挑選一批 Pod 殺掉或標記為失敗讓節(jié)點恢復(fù)穩(wěn)定。Event 里會寫The node was low on resource: memory或者disk。啟動即失敗和上面兩種也不一樣。比如CreateContainerConfigError、CreateContainerError通常是鏡像拉取、存儲卷掛載、環(huán)境變量引用出了問題跟資源配額沒有直接關(guān)系但很多人排查時會把它們混在一起繞半天彎子。所以排查的第一步永遠是先看 Event 和 Pod 的 Last State而不是憑“可能內(nèi)存不夠”就先把 limit 拉高。1.2 從YAML三行文字到內(nèi)核動作一條完整的資源鏈路一個最普通的 Deployment 片段長這樣resources: requests: cpu: 200m memory: 256Mi limits: cpu: 500m memory: 512Mi這三行寫下去之后會依次走過這么一條鏈路API Server 持久化資源信息跟著 Pod 定義進入 Etcd這只是個“聲明”沒有任何人執(zhí)行它。調(diào)度器記賬Scheduler 在 Filter 階段會看每個節(jié)點的Allocatable然后算出“所有已調(diào)度 Pod 的 requests 總和”加上新 Pod 的 requests不能超過節(jié)點剩余可分配量。注意它只看 requests完全不看 limits。kubelet 監(jiān)聽與創(chuàng)建Pod 被綁定到某節(jié)點后kubelet 從 API Server Watch 到變化調(diào)用容器運行時創(chuàng)建容器。運行時設(shè)置 cgroup容器運行時比如 containerd在 cgroup 目錄里創(chuàng)建對應(yīng)的控制組并把 limits 數(shù)值寫進內(nèi)核文件。CPU 對應(yīng)cpu.max或cpu.cfs_quota_us內(nèi)存對應(yīng)memory.max或memory.limit_in_bytes。內(nèi)核執(zhí)行限制進程運行后一旦 CPU 使用超過配額CFS 帶寬控制會強制“節(jié)流”內(nèi)存超過上限內(nèi)核就會回收頁面回收不動就 OOM。kubelet 回收狀態(tài)容器被殺或驅(qū)逐后kubelet 更新 Pod 狀態(tài)Controller 決定是否重啟或重新調(diào)度。關(guān)鍵結(jié)論是requests 是調(diào)度器用來簽“工位合同”的limits 是內(nèi)核用來執(zhí)行“上限刑罰”的兩者根本不在同一層生效。這也是為什么很多人把 limit 調(diào)得再大調(diào)度問題依然解決不了。1.3 為什么“調(diào)大 limit”經(jīng)常解決不了問題結(jié)合上面的鏈路調(diào)大 limit 只在一種情況下有效容器確實是死在自己 cgroup 內(nèi)存上限太低導(dǎo)致的 OOM。但如果問題出在節(jié)點物理內(nèi)存被耗盡或者根本就是磁盤水位觸發(fā)了驅(qū)逐那你改 limit 等于改了個寂寞。還有個更容易忽略的場景內(nèi)存工作集逼近 limit 時內(nèi)核會頻繁回收 page cache容器響應(yīng)變慢但還沒死。你以為“還沒到 limit 所以沒問題”其實性能已經(jīng)劣化到閾值邊緣了。比較靠譜的排查順序是先kubectl describe pod xxx看 Last State 的 Exit Code 和 Message再翻節(jié)點監(jiān)控確認當時的內(nèi)存水位、磁盤水位和 cgroup 實際用量最后才決定是不是要改 resource 定義。這個順序看著簡單但真實環(huán)境里很多人是直接跳到最后一步的。2. requests和limits一個是工位合同一個是天花板限高2.1 request調(diào)度器簽工位合同的依據(jù)request 本質(zhì)上是在告訴調(diào)度器“我這個 Pod 至少要占用這么多資源你把我放到節(jié)點上后這塊資源別人就不能用了?!?一個節(jié)點的可分配資源被所有 Pod 的 requests 總和瓜分不管這些 Pod 實際用沒用完。想象一個辦公室你給這個項目組劃了 10 個工位即使項目組只來了 3 個人剩下的 7 個工位也在合同期內(nèi)鎖著別的團隊搬不進來。Pod 的 request 就是這張工位合同。K8S 調(diào)度器按“工位數(shù)”不按 requests 總和來算剩余容量。所以在資源緊張的集群里把 request 調(diào)大會導(dǎo)致調(diào)度明顯變困難調(diào)小調(diào)度容易但節(jié)點超賣風(fēng)險上升。很多人只關(guān)心 limit 夠不夠卻不知道真正卡調(diào)度的是 request。2.2 limitkubelet設(shè)置運行上限的依據(jù)limit 是運行時上限。kubelet 會要求容器運行時把這個值寫進 cgroup內(nèi)核據(jù)此限制進程使用。這跟調(diào)度器沒有一毛錢關(guān)系調(diào)度器在過濾節(jié)點時根本不看 limit。這意味著你完全可以把 request 設(shè)成 100mlimit 設(shè)成 8 核。調(diào)度器只會按 100m 幫你找位置但實際運行時這個容器可以沖高到 8 核。這種配置是合法的也常用于“允許突發(fā)但保底調(diào)度”的場景但代價是節(jié)點可能被突發(fā)流量打穿。反過來你也不能把 limit 設(shè)成 100m 但 request 設(shè)成 2 核因為調(diào)度器會按 2 核記賬而內(nèi)核會把你限制在 100m等于白白浪費調(diào)度配額。2.3 CPU可壓縮與內(nèi)存不可壓縮兩種limit的本質(zhì)差異CPU 和內(nèi)存的 limit 行為有著本質(zhì)區(qū)別維度CPU內(nèi)存資源性質(zhì)可壓縮不可壓縮超過 limit 時CPU 節(jié)流throttle進程不停止觸發(fā)內(nèi)核 OOM進程可能被殺死對同節(jié)點影響節(jié)流后讓出 CPU影響相對可控內(nèi)存耗盡可能拖垮整臺節(jié)點關(guān)鍵監(jiān)控指標container_cpu_cfs_throttled_periodscontainer_memory_working_set_bytes拿生活場景類比CPU limit 像高速公路的限速牌超速不是扣車而是被慢悠悠地壓在限速上走內(nèi)存 limit 像一張信用卡的額度你刷爆了不是被警告而是直接被???。這條差異決定了配置策略上的大方向CPU 可以靠 introspection 和擴容扛過去內(nèi)存問題則必須留足緩沖。CPU limit 設(shè)小了你最多看到容器變慢、耗時上升、被 throttled但它不會死內(nèi)存 limit 設(shè)小了那就是一場事故。所以要給內(nèi)存留的余量通常比 CPU 高一個量級。3. 調(diào)度器只讀request看清“看得見的賬”和“看不見的墻”3.1 節(jié)點到底有多少可分Allocatable、預(yù)留與驅(qū)逐閾值很多人的資源賬是這么算的節(jié)點 4 核 16G那我放 4 個 1 核 4G 的 Pod 總沒問題吧實際一調(diào)度就 Pending報錯Insufficient cpu或Insufficient memory。原因是節(jié)點并不是所有 Capacity 都能分給 Pod。kubelet 在計算節(jié)點可分配資源時會扣掉兩塊系統(tǒng)預(yù)留通過--system-reserved和--kube-reserved指定給操作系統(tǒng)守護進程、kubelet、容器運行時預(yù)留的份額。驅(qū)逐閾值比如memory.available低于 100Mi 時觸發(fā)驅(qū)逐這部分空間也不能算作可分配。Allocatable Capacity - 系統(tǒng)預(yù)留 - 驅(qū)逐閾值。實際生產(chǎn)環(huán)境里很多人沒配置--system-reserved。這意味著 kubelet、containerd、sshd 這些進程用的內(nèi)存沒有從任何 Pod 的 request 里扣除它們會和業(yè)務(wù) Pod 搶物理內(nèi)存。結(jié)果節(jié)點明明“還有空閑”卻提前進入 MemoryPressure業(yè)務(wù) Pod 莫名被驅(qū)逐。這就是“看不見的墻”。3.2 明明節(jié)點很閑卻調(diào)度不上去碎片化的本質(zhì)另一種常見 Pending 是節(jié)點 CPU 實際使用率才 20%但新 Pod 就是放不上去。這不是 Bug而是因為調(diào)度器記賬用的是 requests 總和不是實時用量。舉個直觀例子某節(jié)點 4 核上面跑著 4 個 Pod每個 requests 都是 900m實際用量都只有 100m。此時節(jié)點剩余 request 是 400m實際空閑卻接近 3.6 核。你想放一個 requests 為 500m 的新 Pod調(diào)度器一看剩余 400m直接拒絕。這就像奶茶店收銀抽屜里全是零錢你要給一張 100 元整鈔店員就是找不開。每個 Pod 的 request 都是預(yù)占用的“整鈔”哪怕它實際消費很小別人也借不了這塊。要緩解碎片化我能給的思路有四條把長期穩(wěn)定服務(wù)的 request 調(diào)到貼近真實用量而不是拍腦袋寫大合理使用節(jié)點池讓規(guī)格差異不要過大用 PriorityClass 加搶占高優(yōu)先級 Pod 實在插不進去時可以擠掉低優(yōu)先級 Pod接受一定程度的超賣但必須配合監(jiān)控和驅(qū)逐兜底。3.3 親和性、污點與拓撲分布資源賬之外的隱藏成本還有一類 Pending連Insufficient cpu都不是而是調(diào)度器在別的維度上卡死。比如節(jié)點上有污點而 Pod 沒有對應(yīng)容忍Pod 帶了nodeAffinity把可選節(jié)點鎖死在一小撮機器上開了topologySpreadConstraints要求 Pod 必須打散到不同可用區(qū)或節(jié)點哪怕某個區(qū)資源明明更空。這些約束和資源是兩套獨立系統(tǒng)但常常疊加在一起。排障的時候如果只盯著資源很容易陷進去。我一般先看kubectl describe pod里的 Events里面會明確寫“0/N nodes are available”以及具體原因是Insufficient cpu、node(s) didnt match node selector還是node(s) had untolerated taint。原因不一樣解法完全不同。4. 內(nèi)核如何執(zhí)行l(wèi)imit從CPU周期到OOM Killer的完整鏈路4.1 CFS帶寬控制100ms周期里的時間片游戲CPU 的 limit 不是“每秒只能用 0.5 秒”這么簡單。內(nèi)核的 CFS 帶寬控制把時間切成一段段的周期默認 100ms再給每個容器分配一個配額。比如 limit 為 500m意味著在每個 100ms 周期里容器最多能占用 50ms 的 CPU 時間。如果容器在周期內(nèi)把配額用完內(nèi)核會把它“節(jié)流”到下一個周期。這相當于一個閘機你不是被禁止前進而是排隊到下一輪再走。對延遲敏感的服務(wù)來說節(jié)流直接表現(xiàn)為 P99 毛刺和超時但 CPU 使用率看起來又沒滿。判斷是否被節(jié)流監(jiān)控指標要盯container_cpu_cfs_throttled_periods和container_cpu_cfs_throttled_periods / container_cpu_cfs_periods。如果節(jié)流比例長期高于 10%要么調(diào)高 CPU limit要么減少容器內(nèi)的線程競爭。一個容易踩的坑是Java 或 Go 這類運行時內(nèi)部有多線程limit2 只是一個周期最多 200ms CPU 時間但它可以在 2 個核上同時跑滿 100ms。千萬別把進程內(nèi)的線程數(shù)理解成“受 limit 保護”limit 只限制總量不限制并發(fā)形態(tài)。4.2 內(nèi)存cgroup上限與OOM評分內(nèi)存的 limit 寫在 cgroup 的memory.max里。內(nèi)核會持續(xù)監(jiān)控這個 cgroup 的內(nèi)存使用當接近上限時先嘗試回收 page cache 等可回收頁面回收完還不夠就觸發(fā) OOM Killer。OOM Killer 不是隨機殺它按照每個進程的oom_score排序分高的先殺。這個分數(shù)大致由進程的物理內(nèi)存占用和 oom_score_adj 共同決定。容器里的 pid 1 如果吃內(nèi)存最狠通常它自己就是第一個犧牲品。對 Java 應(yīng)用來說這里有個典型悲劇老版本 JVM 不感知容器 cgroup limit默認堆大小按物理機內(nèi)存來算結(jié)果堆一擴立刻撞 limit進程反復(fù)被殺。JDK 8u191 之后默認開啟UseContainerSupport但如果你還在用老鏡像得顯式加-XX:MaxRAMPercentage75.0這類參數(shù)。排查內(nèi)存問題不要只看container_memory_usage_bytes更準的是container_memory_working_set_bytes。它排除了可以被內(nèi)核回收的 page cache反映的是“真正臟著的、賴在內(nèi)存里不走的部分”。工作集貼著 limit 走才是崩潰的前兆。4.3 從網(wǎng)上抄limit值為什么容易翻車我在社區(qū)里見過太多“通用模板”什么“Java 服務(wù) memory limit 設(shè) 2G 準沒錯”“Go 服務(wù) requests 給 500m 就行”。這些數(shù)值對別人可能合理但換一個業(yè)務(wù)就完全不是一回事。舉個實際例子同樣是 Nginx 網(wǎng)關(guān)靜態(tài)文件緩存命中率高的工作集可能只有 100Mi但開啟了 Lua 腳本和復(fù)雜 access log 后工作集能沖到 500Mi。你照著網(wǎng)上模板寫 256Mi容器三天兩頭 OOM寫 4G又白白占著節(jié)點內(nèi)存調(diào)度器按 request 記賬后面全是資源浪費。正確姿勢是結(jié)合第 6 節(jié)說的基線采集法拿自己業(yè)務(wù)的數(shù)據(jù)來定。別人的模板只能作為初始參考不能作為長期配置。5. QoS三兄弟與節(jié)點驅(qū)逐次序危機關(guān)頭的生存排序5.1 三檔QoS怎么判定只看request和limit的設(shè)定關(guān)系K8S 會根據(jù) Pod 里每個容器的 requests 和 limits 設(shè)定蓋一個 QoS 章共三檔Guaranteed、Burstable、BestEffort。判定規(guī)則如下QoS 等級判定條件風(fēng)險傾向Guaranteed每個容器都設(shè)置了 requests 和 limits且同一資源的二者相等被驅(qū)逐優(yōu)先級最低Burstable至少一個容器設(shè)置了 requests 或 limits但不滿足 Guaranteed 條件常見主形態(tài)超用風(fēng)險中等BestEffort所有容器都沒有設(shè)置任何 requests 和 limits被驅(qū)逐優(yōu)先級最高注意這個判定是“每個容器”維度的。如果 Pod 里有一個 Java 容器配了 Guaranteed另一個 sidecar 只寫了 requests 沒寫 limits整個 Pod 就會被降級成 Burstable。所以想拿 Guaranteed 的穩(wěn)定性所有容器都得嚴格配齊。QoS 最重要的意義是當節(jié)點資源告急輪到 kubelet 決定“誰先走”的時候它是分檔排序的核心依據(jù)之一。但它只是排序規(guī)則并不保證 Guaranteed 就一定不死。5.2 驅(qū)逐流程先來后到但又不完全是kubelet 的驅(qū)逐不是簡單的 FIFO。實際排序邏輯大致是按 PriorityClass 排序優(yōu)先級低的先被驅(qū)逐優(yōu)先級相同時按 QoS 排序BestEffort 先于 BurstableGuaranteed 最后仍然相同時按 Pod 內(nèi)存使用量相對于 request 的超用比例排序超用比例大的先走。這個排序在 Eviction API 的代碼里有明確實現(xiàn)不是玄學(xué)。對你的啟示是核心在線服務(wù)Priority 和 QoS 都要拉滿做成 Guaranteed離線任務(wù)、Batch Job可以安心用 Burstable 或 BestEffort反正它優(yōu)先級低犧牲起來不心疼只是 QoS 高但 Priority 低在驅(qū)逐場景下依然可能先被殺。一個常被忽略的細節(jié)是Pod 被驅(qū)逐后狀態(tài)是FailedEvent 里寫Evicted。Pod 對象還在只是容器沒了配套的 PVC 可能還占著。做清理的時候別忘了kubectl delete pod把尸骨收走。5.3 Guaranteed不是免死金牌很多人以為 QoS 設(shè)成 Guaranteed、Priority 設(shè)高Pod 就安全了。這是認知盲區(qū)。kubelet 驅(qū)逐確實會盡量不碰 Guaranteed但別忘了還有一個獨立于 kubelet 的“暴力部門”——內(nèi)核 OOM Killer。如果你的容器內(nèi)存工作集超過了 cgroup limit內(nèi)核會直接殺進程這個時候 QoS 章一點用都沒有。QoS 管的是 kubelet 的驅(qū)逐順序OOM Killer 管的是內(nèi)核的內(nèi)存戰(zhàn)備線兩層機制完全平行。另外還有個場景整臺節(jié)點陷入 MemoryPressurekubelet 的硬驅(qū)逐閾值已經(jīng)觸發(fā)了但如果根因是節(jié)點上的系統(tǒng)進程吃內(nèi)存就算你把所有業(yè)務(wù) Pod 都調(diào)成 Guaranteed該被殃及還是會被殃及。所以配好節(jié)點級--system-reserved、--kube-reserved比在 Pod 層面死磕 QoS 更治本。6. 從原理到策略一次靠譜的資源清單長什么樣6.1 基線采集三步法不看監(jiān)控改配置等于閉眼開車講了這么多原理最后落到實操。我自己的習(xí)慣是給任何一個服務(wù)寫 resource 之前先做三天到一周的“基線采集”。第一步部署監(jiān)控用 Prometheus 抓container_cpu_usage_seconds_total和container_memory_working_set_bytes。注意是 working set不是 raw usage。第二步統(tǒng)計 P95 / P99。requests 建議取 P95 附近再乘 1.1~1.2 的保險系數(shù)。為什么不用 P99因為 P99 往往被毛刺抬高翻倍配置會讓 request 虛高白白浪費節(jié)點容量。如果一個服務(wù)的內(nèi)存工作集長期穩(wěn)定在 200Mi突然某天 2 分鐘沖到 400Mi那可能是 GC 或緩存填充把 limit 留在 512Mi 更合適而不是把 request 改成 400Mi。第三步定 limits。CPU limit 一般是 request 的 1.5~2 倍內(nèi)存 limit 是 request 的 1.5~2.5 倍具體看業(yè)務(wù)對延遲敏感度和緩存的突刺程度。拿不準的時候?qū)幙上确艑?limit也不要放窄——內(nèi)存 limit 設(shè)小了是會出人命的CPU throttled 只是變慢而已。6.2 結(jié)合HPA/VPA的容量規(guī)劃requests不再是拍腦袋如果你的服務(wù)掛了 HPA那 requests 的準確性就更加關(guān)鍵因為 HPA 的擴縮容計算用的是CPU 當前使用量 / request不是 limit。這句話翻譯一下如果 requests 設(shè)得過大HPA 永遠算不出高利用率結(jié)果就是副本數(shù)漲不起來流量全壓在少數(shù)幾個實例上如果 requests 設(shè)得過小HPA 又會被輕微的 CPU 毛刺刺激得瘋狂擴容Pod 數(shù)暴增但每個 Pod 都在節(jié)流。VPA 能自動推薦 requests但默認只給建議不會改你 YAML。即便你開了 Auto 模式它調(diào)整 requests 時也需要重新創(chuàng)建 Pod對在線服務(wù)多少有影響。我的建議是把 VPA 當“測量儀器”跑一段時間看它推薦的值跟你手工算的 P95 是否一致再決定要不要采納別一上來就全自動。6.3 一套可直接套用的初始配置示例最后給一份模板適合中等規(guī)模的 web 服務(wù)你可以按自己采集到的基線調(diào)整apiVersion: apps/v1 kind: Deployment metadata: name: demo-web spec: replicas: 3 strategy: rollingUpdate: maxSurge: 1 maxUnavailable: 0 template: spec: containers: - name: app image: demo:1.0.2 resources: requests: cpu: 200m memory: 256Mi limits: cpu: 500m memory: 512Mi這份配置的設(shè)計邏輯是CPU requests 200m 保證調(diào)度器給它一個穩(wěn)定的物理位置CPU limits 500m 給突發(fā)留了 2.5 倍空間避免節(jié)流嚴重內(nèi)存 requests 256Mi 貼近多數(shù) web 服務(wù)的空閑工作集內(nèi)存 limits 512Mi 就是為了給 GC 和高峰留緩沖。同時別忽略滾動更新的資源疊加maxSurge: 1意味著發(fā)布瞬間集群里會有 4 個副本同時跑峰值容量大概是4 × 512Mi 2Gi。如果你按“3 副本剛好打滿節(jié)點”去規(guī)劃容量發(fā)布時就會因為資源不夠而卡住滾動更新。這是 Deployment 交付時最常見的隱性故障很多人沒把 Surge 算進資源賬。我在實際配置中的體會是資源參數(shù)沒有“一勞永逸”業(yè)務(wù)流量和鏡像版本一變工作集就會跟著漂。最有效的做法是每個月回頭看一眼監(jiān)控問自己三個問題節(jié)流比例漲了嗎工作集離 limit 多遠HPA 擴容頻率正常嗎如果這三項里有兩項不對勁就該重新走一遍基線采集調(diào)整 resource 了。這才是讓資源模型長期健康運轉(zhuǎn)的方式而不是每次出事都靠臨時加 limit 續(xù)命。