踐)
用了這么多年 Kubernetes每次有新同事問(wèn)我的第一個(gè)問(wèn)題基本都集中在“Pod到底是什么”上。我太理解這種困惑了因?yàn)?Docker 時(shí)代大家腦子里已經(jīng)形成了“容器 運(yùn)行單元”的思維定式結(jié)果一到 k8s 里發(fā)現(xiàn)跑起來(lái)的最小單元不是容器而是 Pod而且很多時(shí)候一個(gè) Pod 里還能塞好幾個(gè)容器這跟以前的習(xí)慣完全不一樣。這篇文章我就把自己對(duì) Pod 的理解、底層的運(yùn)行機(jī)制、實(shí)際部署中的操作心得以及這幾年排查 Pod 問(wèn)題總結(jié)的經(jīng)驗(yàn)一次說(shuō)清楚。內(nèi)容從基礎(chǔ)概念一路講到 LNMP 這樣的多容器場(chǎng)景同時(shí)覆蓋到了二進(jìn)制部署、Rancher、離線環(huán)境這些大家常問(wèn)的落地方式適合正在學(xué)習(xí) k8s 的運(yùn)維和開(kāi)發(fā)同學(xué)也適合已經(jīng)被 Pod 各種異常狀態(tài)折磨過(guò)的實(shí)戰(zhàn)派。1. 為什么會(huì)有 Pod 這個(gè)抽象層1.1 從容器到 Podk8s 為什么不做“容器級(jí)調(diào)度”先理清一個(gè)經(jīng)常被人搞混的點(diǎn)Docker 是容器運(yùn)行時(shí)它負(fù)責(zé)的是“在單臺(tái)機(jī)器上把容器跑起來(lái)”而 k8s 是集群編排系統(tǒng)它關(guān)心的是“在多臺(tái)機(jī)器上怎么調(diào)度、怎么保證服務(wù)不掛”。這兩者的抽象粒度天然就不一樣。Docker 時(shí)代我們部署一個(gè) web 服務(wù)直接把代碼打進(jìn)去、端口映射出來(lái)、然后 docker run 一下就算完事。但到了真實(shí)的生產(chǎn)環(huán)境一個(gè)服務(wù)往往不是單獨(dú)一個(gè)進(jìn)程就能搞定的。常見(jiàn)的場(chǎng)景是nginx 要轉(zhuǎn)發(fā)請(qǐng)求給后端 PHP-FPM同時(shí)需要一個(gè)日志采集進(jìn)程在旁邊收日志或者業(yè)務(wù)主進(jìn)程旁邊帶一個(gè)監(jiān)控上報(bào)進(jìn)程定時(shí)把指標(biāo)推給 Prometheus。如果用 Docker Compose這些配套進(jìn)程就得各自啟動(dòng)一個(gè)容器然后再通過(guò) Compose 的網(wǎng)絡(luò)把它們連起來(lái)它們之間的依賴關(guān)系、共享存儲(chǔ)、通信方式都要你手工管理。k8s 的做法是直接在調(diào)度層面引入了 Pod 這個(gè)中間層。Pod 是一組“必須部署在同一臺(tái)宿主機(jī)上、資源聯(lián)合調(diào)度、生命周期完全一致”的容器集合。它不是把 Docker 容器包一層殼那么簡(jiǎn)單而是改變了你組織應(yīng)用的方式以前你在 Compose 里用“服務(wù)”來(lái)組織現(xiàn)在你在 k8s 里用“Pod”來(lái)組織。二者的區(qū)別在于Compose 的服務(wù)之間是“通過(guò)網(wǎng)絡(luò)調(diào)用”的關(guān)系而 Pod 里的多個(gè)容器是“共享同一個(gè)運(yùn)行環(huán)境”的關(guān)系。k8s 之所以不下沉到直接調(diào)度容器核心原因是它需要一種機(jī)制來(lái)表達(dá)“這些進(jìn)程必須緊緊地耦合在一起”的訴求。如果沒(méi)有 Pod 這一層兩個(gè)容器想要共享 localhost 通信、共享一個(gè)數(shù)據(jù)卷、保持同生共死就只能靠外部編排服務(wù)去強(qiáng)行管理復(fù)雜度會(huì)高得離譜。有了 Pod這些問(wèn)題全部從“應(yīng)用層的約定”變成了“基礎(chǔ)設(shè)施層的內(nèi)置能力”。1.2 Pod 真正解決的兩個(gè)實(shí)際問(wèn)題第一個(gè)問(wèn)題是網(wǎng)絡(luò)。Pod 內(nèi)的所有容器共享同一個(gè)網(wǎng)絡(luò)命名空間這意味著它們共享同一個(gè) IP、同一個(gè)端口空間。舉例來(lái)說(shuō)nginx 容器里只要配置 fastcgi_pass 127.0.0.1:9000就能直接訪問(wèn)到同一個(gè) Pod 里 PHP-FPM 容器監(jiān)聽(tīng)的 9000 端口。這在實(shí)際部署 LNMP 的時(shí)候非常好用你不需要去拿 Service 或負(fù)載均衡去連同一個(gè) Pod 內(nèi)部的進(jìn)程。第二個(gè)問(wèn)題是存儲(chǔ)。Pod 內(nèi)的容器可以共享同一個(gè) Volume。最常見(jiàn)的是 emptyDir——一個(gè)隨 Pod 創(chuàng)建而創(chuàng)建、隨 Pod 銷毀而消失的臨時(shí)目錄。nginx 和 PHP-FPM 容器都掛載同一個(gè)代碼目錄代碼發(fā)布的時(shí)候只要更新一次掛載的卷兩個(gè)容器立刻就能同時(shí)看到新代碼。這在容器化的 PHP/Java 應(yīng)用里非常典型。我見(jiàn)過(guò)不少?gòu)?Compose 遷到 k8s 的人一開(kāi)始習(xí)慣把 nginx、PHP-FPM、MySQL 分別做成三個(gè)獨(dú)立 Deployment然后靠 Service 互相訪問(wèn)。這種思路不是不行但你會(huì)發(fā)現(xiàn) nginx 和 PHP-FPM 之間的通信要經(jīng)過(guò) Service 的轉(zhuǎn)發(fā)、要經(jīng)過(guò) kube-proxy延遲和復(fù)雜度都上去了。事實(shí)上 nginx 和 PHP-FPM 是典型的“同生命周期應(yīng)用”它們版本一起升級(jí)、部署一起變化更應(yīng)該放進(jìn)同一個(gè) Pod。而 MySQL 這種有狀態(tài)的數(shù)據(jù)存儲(chǔ)則完全不同它需要獨(dú)立的數(shù)據(jù)持久化、獨(dú)立的擴(kuò)縮容策略絕對(duì)不應(yīng)該和 Web 容器擠在一個(gè) Pod 里。理解了這一點(diǎn)你對(duì) Pod 的編排邊界就有了基本的判斷力。2. Pod 的底層運(yùn)行機(jī)制2.1 一次創(chuàng)建 Pod 時(shí)kubelet 在節(jié)點(diǎn)上做了什么很多教程上來(lái)就寫(xiě) yaml 文件但從來(lái)不解釋“你執(zhí)行 kubectl apply 之后發(fā)生了什么”。我建議每個(gè)想深入研究 k8s 的人都先把這個(gè)鏈路走一遍否則后面排障會(huì)非常痛苦。當(dāng)你在控制平面執(zhí)行 kubectl apply 提交了一個(gè) Pod 定義后請(qǐng)求會(huì)打到 API Server。API Server 把 Pod 對(duì)象寫(xiě)入 etcd然后調(diào)度器kube-scheduler會(huì) watch 到這個(gè)新 Pod根據(jù)它的資源請(qǐng)求、節(jié)點(diǎn)親和性、污點(diǎn)容忍等約束選一個(gè)最合適的節(jié)點(diǎn)并把調(diào)度結(jié)果寫(xiě)回 API Server。接著目標(biāo)節(jié)點(diǎn)上的 kubelet 會(huì) watch 到這個(gè) Pod開(kāi)始在本地執(zhí)行容器創(chuàng)建流程。kubelet 創(chuàng)建 Pod 并不是直接拉起業(yè)務(wù)容器而是先啟動(dòng)一個(gè)基礎(chǔ)設(shè)施容器在 containerd 里叫 sandbox在 Docker 時(shí)代叫 pause 容器。這個(gè)容器極其輕量它不跑任何業(yè)務(wù)邏輯唯一的職責(zé)是持有一組 Linux namespace——比如網(wǎng)絡(luò)命名空間、IPC 命名空間、UTS 命名空間。后續(xù)創(chuàng)建的業(yè)務(wù)容器全部通過(guò) --networkcontainer:sandbox 這種方式加入同一個(gè)命名空間這樣就實(shí)現(xiàn)了 Pod 內(nèi)容器共享網(wǎng)絡(luò)和通信域的效果。這也就解釋了為什么你在節(jié)點(diǎn)上執(zhí)行 docker ps或用 crictl 查看容器時(shí)會(huì)看到一堆“pause”開(kāi)頭的容器。很多人第一次看到會(huì)覺(jué)得是殘留垃圾進(jìn)程其實(shí)那是 Pod 存在的基礎(chǔ)。那個(gè) pause 容器是整個(gè) Pod 生命周期里最先啟動(dòng)、最后刪除的容器它一掛整個(gè) Pod 里的所有容器都會(huì)跟著重建。業(yè)務(wù)容器的創(chuàng)建流程也不止是 pull image 和 run container 兩步。kubelet 會(huì)依次處理初始化容器initContainer如果有、掛載 Volume、設(shè)置環(huán)境變量、設(shè)置資源 cgroup 限制、配置探針探針啟動(dòng)后會(huì)周期性調(diào)用然后依次啟動(dòng)普通容器。任何一個(gè)步驟失敗Pod 都會(huì)停在對(duì)應(yīng)的狀態(tài)上這也是我們排障時(shí)觀察 Pod 事件Events的直接依據(jù)。2.2 探針、就緒、重啟策略與 Pod 狀態(tài)流轉(zhuǎn)搞明白了創(chuàng)建鏈路接著看 Pod 運(yùn)行時(shí)的幾個(gè)關(guān)鍵機(jī)制。首先要分清 Pod 狀態(tài)和容器狀態(tài)——Pod 的狀態(tài)是對(duì)內(nèi)容器狀態(tài)的聚合判斷常見(jiàn)的有 Pending、Running、Succeeded、Failed、Unknown但在實(shí)際開(kāi)發(fā)環(huán)境中我們天天打交道的其實(shí)是 ContainerCreating、CrashLoopBackOff、ImagePullBackOff、Terminating 這些 kubectl 列表里展示的狀態(tài)。探針Probe是保證 Pod 可靠性的重要組件。livenessProbe 決定“容器活著嗎”如果一直失敗kubelet 會(huì)按 restartPolicy 殺掉容器并重啟readinessProbe 決定“容器可以對(duì)外提供服務(wù)了嗎”如果失敗kubelet 會(huì)把該 Pod 從 Service 的 Endpoints 里摘掉流量就不會(huì)打到它。startupProbe 是后來(lái)加的專門(mén)解決“啟動(dòng)很慢的老 Java 應(yīng)用”場(chǎng)景——它先于 liveness 執(zhí)行在 startup 成功之前l(fā)iveness 不會(huì)介入這樣可以避免應(yīng)用啟動(dòng)耗時(shí)太長(zhǎng)被誤殺。這里要特別強(qiáng)調(diào) restartPolicy 的一個(gè)坑Deployment 管理的 PodrestartPolicy 必須是 Always因?yàn)?Deployment 本身就是靠滾動(dòng)重啟來(lái)實(shí)現(xiàn)發(fā)布和自愈的。如果你把 restartPolicy 改成 OnFailure 或 Never很多人會(huì)在這時(shí)發(fā)現(xiàn) Pod 時(shí)不時(shí)變成 Succeeded 或 Failed 狀態(tài)而不是被拉起然后一臉懵。這個(gè)限制從 API 校驗(yàn)層面就會(huì)直接攔下你所以寫(xiě) yaml 的時(shí)候提前注意就好。還有一個(gè)容易被忽略的機(jī)制是容器的日志處理。Pod 里的容器如果崩潰頻繁kubelet 會(huì)按照一定周期做退避重啟拉起的間隔從 10s 開(kāi)始20s、40s、80s……最長(zhǎng) 5 分鐘之后恢復(fù)正常探測(cè)頻率。這個(gè)退避機(jī)制就是 CrashLoopBackOff 狀態(tài)的來(lái)源。很多新手一看到 CrashLoopBackOff 就以為是死循環(huán)了其實(shí)這只是“崩潰后退避等待中”的正常表現(xiàn)重點(diǎn)要看容器為什么崩潰也就是去查日志。3. 實(shí)際部署從最簡(jiǎn)單到 LNMP3.1 單容器 Pod 的 yaml 長(zhǎng)什么樣理論講完必須動(dòng)手。先寫(xiě)一個(gè)最簡(jiǎn)單但完整的單容器 Pod 示例我有一個(gè)習(xí)慣是永遠(yuǎn)不裸寫(xiě)裸 Pod這里是為了教學(xué)先展示 Pod 最小定義生產(chǎn)環(huán)境后面我會(huì)強(qiáng)烈建議你換成 Deployment。apiVersion: v1 kind: Pod metadata: name: nginx-single namespace: default labels: app: nginx-single spec: containers: - name: nginx image: nginx:1.25 ports: - containerPort: 80 protocol: TCP resources: requests: cpu: 100m memory: 128Mi limits: cpu: 250m memory: 512Mi readinessProbe: httpGet: path: /nginx_status port: 80 initialDelaySeconds: 3 periodSeconds: 5 livenessProbe: httpGet: path: /nginx_status port: 80 initialDelaySeconds: 15 periodSeconds: 10執(zhí)行 kubectl apply -f 之后用 kubectl get pods -o wide 就能看到 Pod 被調(diào)度到哪臺(tái)節(jié)點(diǎn)拿到它的 Cluster IP。然后可以用 kubectl exec -it nginx-single -- curl 127.0.0.1 驗(yàn)證一下。注意 resources 那一節(jié)里 cpu 的單位是 m毫核100m 代表 0.1 個(gè) CPU 核心。內(nèi)存單位是 Mi這是二進(jìn)制兆。新手最常見(jiàn)的錯(cuò)誤就是把 cpu 寫(xiě)成 1表示 1 個(gè)完整核心內(nèi)存寫(xiě)成 1024表示 1024 字節(jié)然后發(fā)現(xiàn)調(diào)度和 limit 行為完全不符合預(yù)期。關(guān)于 readinessProbe 和 livenessProbe 的 pathnginx:1.25 官方鏡像里默認(rèn)不帶 /nginx_status 這個(gè)模塊路徑如果你照抄這個(gè)配置會(huì)發(fā)現(xiàn)就緒探針一直失敗。平時(shí)用的話直接探 / 或者 /index.html 更保險(xiǎn)。這種細(xì)節(jié)問(wèn)題在測(cè)試環(huán)境驗(yàn)證一下就能發(fā)現(xiàn)我列出來(lái)是提醒你不要被教程里的示例坑到。3.2 多容器協(xié)作用 Pod 內(nèi)兩個(gè)容器搭建 LNMP 示例平時(shí)被問(wèn)得特別多的問(wèn)題就是“k8s 里怎么部署 LNMP”。網(wǎng)上能搜到一些“k8s lnmp 架構(gòu)實(shí)驗(yàn)”之類的教程但很多講得很含糊。這里我給出一套最經(jīng)典的 Pod 內(nèi)雙容器方案nginx 容器和 PHP-FPM 容器放同一個(gè) Pod共享代碼目錄nginx 把 PHP 請(qǐng)求轉(zhuǎn)發(fā)到本機(jī) 9000 端口PHP-FPM 處理完后把結(jié)果返回給 nginx。先看 Pod 的定義再把關(guān)鍵點(diǎn)拆開(kāi)講。apiVersion: v1 kind: Pod metadata: name: lnmp-pod labels: app: lnmp spec: containers: - name: nginx image: nginx:1.25 volumeMounts: - name: web-code mountPath: /usr/share/nginx/html ports: - containerPort: 80 readinessProbe: httpGet: path: /index.php port: 80 initialDelaySeconds: 5 periodSeconds: 5 - name: php-fpm image: php:8.2-fpm volumeMounts: - name: web-code mountPath: /var/www/html ports: - containerPort: 9000 volumes: - name: web-code emptyDir: {}這段配置只用了幾個(gè)核心字段但表達(dá)了一個(gè)非常重要的概念nginx 和 php-fpm 兩個(gè)容器通過(guò) emptyDir 共享了同一份代碼目錄這正是前面我講的 Pod 內(nèi)容器共享 Volume 的實(shí)際應(yīng)用。emptyDir 的生命周期等于 Pod 的生命周期Pod 刪了它就沒(méi)了所以這個(gè)方案適合驗(yàn)證、實(shí)驗(yàn)和臨時(shí)負(fù)載生產(chǎn)環(huán)境一般會(huì)用 PVC 把代碼卷?yè)Q成持久化的。nginx 配置里要讓 PHP 請(qǐng)求轉(zhuǎn)到 php-fpm你可以用 ConfigMap 掛一個(gè)自定義的 nginx 配置核心是這一句location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME /var/www/html$fastcgi_script_name; include fastcgi_params; }關(guān)鍵點(diǎn)來(lái)了fastcgi_pass 寫(xiě)的是 127.0.0.1:9000因?yàn)閮蓚€(gè)容器共享網(wǎng)絡(luò)命名空間所以 nginx 容器里訪問(wèn) localhost:9000 可以直接到達(dá) php-fpm 容器。這一點(diǎn)如果你拆成兩個(gè) Deployment 就很難做到——你只能通過(guò) Service 或 ClusterIP 連接配置和維護(hù)成本都會(huì)高不少。而 MySQL 在 LNMP 架構(gòu)里我傾向于不放進(jìn)同一個(gè) Pod。它是典型的有狀態(tài)組件數(shù)據(jù)要持久化、要主從同步、要單獨(dú)的存儲(chǔ)和備份策略。一般做法是用 StatefulSet 獨(dú)立部署或者直接使用托管的數(shù)據(jù)庫(kù)服務(wù)。把 MySQL 硬塞進(jìn)和 nginx、php 同一個(gè) Pod短期實(shí)驗(yàn)沒(méi)問(wèn)題上了生產(chǎn)一定會(huì)因?yàn)閿?shù)據(jù)卷生命周期和調(diào)度策略的問(wèn)題吃大虧。3.3 Deployment 還是裸 Pod什么時(shí)候不該直接建 Pod上面兩個(gè)例子我為了講解概念都用的裸 Podkind: Pod。但生產(chǎn)環(huán)境我強(qiáng)烈建議你不要直接創(chuàng)建裸 Pod而是通過(guò) Deployment、StatefulSet、DaemonSet 這些控制器來(lái)管理。為什么裸 Pod 如果所在的節(jié)點(diǎn)宕機(jī)了k8s 不會(huì)自動(dòng)幫你在別的節(jié)點(diǎn)重建。但 Deployment 創(chuàng)建的 Pod 是有 ReplicaSet 這樣的控制器盯著Pod 突然掛掉它會(huì)開(kāi)一個(gè)新的補(bǔ)齊節(jié)點(diǎn)掛了調(diào)度器也會(huì)在健康節(jié)點(diǎn)上重新創(chuàng)建。Deployment 還自帶滾動(dòng)更新、回滾、擴(kuò)縮容能力這些是裸 Pod 完全沒(méi)有的。簡(jiǎn)單說(shuō)Deployment 與 Pod 的關(guān)系就像系統(tǒng)進(jìn)程和守護(hù)進(jìn)程的關(guān)系你自己的代碼是那個(gè) Pod而 supervisor 是那個(gè) Deployment。沒(méi)有 supervisor 的話進(jìn)程死了沒(méi)人管有 supervisor 的話它保證你需要的副本數(shù)永遠(yuǎn)在線。所以實(shí)際生產(chǎn)中應(yīng)用部署的規(guī)格應(yīng)該長(zhǎng)這樣apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deployment labels: app: nginx spec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.25 ports: - containerPort: 80可以看到 Pod 的定義被挪到了 template 里這就是“Pod 模板”??刂破髫?fù)責(zé)管理這份模板產(chǎn)出的所有 Pod 實(shí)例。剛開(kāi)始用 k8s 時(shí)我喜歡用 kubectl run nginx --imagenginx --replicas3 試試手但后來(lái)發(fā)現(xiàn)這樣的自動(dòng)化可維護(hù)性不強(qiáng)所以現(xiàn)在一律推薦用 yaml 文件管理方便進(jìn) Git方便審閱方便回滾。4. Pod 的資源模型、調(diào)度與網(wǎng)絡(luò)4.1 資源請(qǐng)求與限制背后的調(diào)度和驅(qū)逐邏輯Pod 里每個(gè)容器都可以聲明 resources.requests 和 resources.limits。requests 是調(diào)度依據(jù)limit 是運(yùn)行限制。調(diào)度器只看 requests它要保證一臺(tái)節(jié)點(diǎn)上所有 Pod 的 requests 總和不超過(guò)節(jié)點(diǎn)可分配資源。也就是說(shuō)哪怕節(jié)點(diǎn)內(nèi)存其實(shí)有 64G但上面已存在的 Pod 請(qǐng)求了 50G再來(lái)一個(gè)新的 Pod 請(qǐng)求 20G那它就會(huì) Pending直到有節(jié)點(diǎn)騰出空間或你擴(kuò)容節(jié)點(diǎn)。limits 則由 kubelet 在啟動(dòng)容器時(shí)配置成 cgroup 的上限。這里有個(gè)常見(jiàn)的認(rèn)知誤區(qū)CPU 的限制是限流throttling容器超過(guò)了會(huì)被降速但內(nèi)存的限制是硬限制容器只要嘗試分配超過(guò) limit 的內(nèi)存內(nèi)核 OOM Killer 就會(huì)把進(jìn)程殺掉kubelet 檢測(cè)到容器異常退出后再按策略重啟從而表現(xiàn)為 OOMKilled 狀態(tài)。根據(jù) request 和 limit 的設(shè)置方式Pod 會(huì)被劃分成三種 QoS 級(jí)別Guaranteed、Burstable、BestEffort。三者都設(shè)了 request 且等于 limit是 Guaranteed只有部分容器設(shè)或 request 小于 limit是 Burstable完全不設(shè) resources是 BestEffort。當(dāng)節(jié)點(diǎn)內(nèi)存壓力過(guò)大時(shí)kubelet 會(huì)進(jìn)入驅(qū)逐流程驅(qū)逐順序是 BestEffort 先被干掉然后是 Burstable最后才是 Guaranteed。這也是生產(chǎn)環(huán)境我給核心業(yè)務(wù)全設(shè) requestlimit 的原因——我不想讓它成為資源緊張時(shí)第一個(gè)被犧牲的對(duì)象。這個(gè)資源模型也解釋了為什么二進(jìn)制方式搭建 k8s 集群或離線部署時(shí)經(jīng)常有人遇到 Pod 調(diào)度失敗的問(wèn)題節(jié)點(diǎn)容量明明看起來(lái)夠但可用資源被系統(tǒng)預(yù)留system-reserved、kube-reserved 這些參數(shù)扣了一部分然后 requests 就會(huì)被拒絕所以計(jì)算節(jié)點(diǎn)容量時(shí)要把預(yù)留資源考慮進(jìn)去。4.2 調(diào)度約束從節(jié)點(diǎn)選擇到親和性默認(rèn)情況下調(diào)度器根據(jù)資源 requests 選擇節(jié)點(diǎn)但很多時(shí)候我們需要主動(dòng)控制 Pod 的去向。nodeSelector 是最簡(jiǎn)單的方案比如給帶有 gputrue 標(biāo)簽的節(jié)點(diǎn)專門(mén)調(diào)度 GPU 任務(wù)spec: nodeSelector: disktype: ssd更復(fù)雜一點(diǎn)的是節(jié)點(diǎn)親和性和 Pod 親和性。節(jié)點(diǎn)親和性支持硬性要求requiredDuringScheduling和軟性偏好preferredDuringScheduling軟性偏好會(huì)給節(jié)點(diǎn)打分得分高的優(yōu)先被選中但如果沒(méi)有滿足的節(jié)點(diǎn)也不會(huì)調(diào)度失敗。Pod 親和性解決的場(chǎng)景是“我想讓這些 Pod 盡量待在同一臺(tái)機(jī)器上”或者“絕對(duì)不要把有沖突的服務(wù)放一起”比如把 Web 和緩存放在同節(jié)點(diǎn)減少延遲而把兩個(gè)副本分散到不同可用區(qū)保證高可用。污點(diǎn)Taint和容忍Toleration是一個(gè)很容易被忽略但生產(chǎn)環(huán)境一定要明白的機(jī)制。節(jié)點(diǎn)有了污點(diǎn)默認(rèn)所有 Pod 都不能調(diào)度上去除非 Pod 顯式容忍了這個(gè)污點(diǎn)。典型用途是給專用節(jié)點(diǎn)打污點(diǎn)只允許特定 Pod 進(jìn)去或者用 NoExecute 污點(diǎn)把故障節(jié)點(diǎn)上的 Pod 全部驅(qū)逐出去。有時(shí)候 Pod 一直 Pending你用 kubectl describe 能看到類似 0/3 nodes are available: 3 node(s) had untolerated taint 的事件排查方法也就很清晰了。4.3 Pod 網(wǎng)絡(luò)IP 從哪來(lái)端口怎么通每個(gè) Pod 在集群內(nèi)部都有一個(gè)獨(dú)立的 IP這個(gè) IP 由 CNI 插件分配。大部分默認(rèn)安裝的集群用的是 Calico、Cilium 或 Flannel 這類方案Pod 會(huì)被分配一個(gè)與宿主機(jī)不同網(wǎng)段的地址。比如節(jié)點(diǎn)是 192.168.1.xPod 可能是 10.244.x.x各節(jié)點(diǎn)上的 Pod 可以通過(guò) Overlay 網(wǎng)絡(luò)跨主機(jī)通信。Pod 內(nèi)的容器共享同一個(gè) IP 和網(wǎng)絡(luò)命名空間這就是前面講的 127.0.0.1:9000 能直達(dá)兄弟容器的原因。在 Pod 外部訪問(wèn) Pod 時(shí)通常有兩種方式一是同集群內(nèi)通過(guò) Service 的 ClusterIP二是調(diào)試時(shí)用 kubectl port-forward 把本地端口映射到 Pod 端口。還有一種是 hostNetwork: true讓 Pod 直接用節(jié)點(diǎn)網(wǎng)絡(luò)不走 CNI這類用法常見(jiàn)于對(duì)網(wǎng)絡(luò)性能極其敏感的組件或需要固定端口的系統(tǒng)組件。順帶提一個(gè)比較進(jìn)階的方向如果需要給 Pod 配置多個(gè)網(wǎng)絡(luò)接口比如同時(shí)接入業(yè)務(wù)網(wǎng)和管理網(wǎng)就會(huì)用到 Multus 這種“多網(wǎng)絡(luò)插件”方案。Multus 本身不實(shí)現(xiàn)網(wǎng)絡(luò)它把多個(gè) CNI 插件比如搭配 macvlan 或 ipvlan組合起來(lái)給 Pod 創(chuàng)建多個(gè)網(wǎng)卡并附加不同網(wǎng)絡(luò)的 IP。在一些需要 VLAN 隔離的部署場(chǎng)景比如 k8s multus 網(wǎng)絡(luò) vlan 配置就是通過(guò)這種方案把 Pod 接入到不同的二層網(wǎng)絡(luò)中。這是個(gè)加分技能日常單網(wǎng)絡(luò)的集群用不上但遇到多網(wǎng)卡、VLAN 需求時(shí)你會(huì)非常感激這個(gè)設(shè)計(jì)。5. 排查實(shí)錄Pod 起不來(lái)的 10 種典型情況5.1 先學(xué)會(huì)看狀態(tài)、事件和日志排查 Pod 問(wèn)題最忌諱的就是上來(lái)就刪除重建那樣你既看不到根因也可能把現(xiàn)場(chǎng)環(huán)境破壞了。正確順序應(yīng)該是先 kubectl get pods 看整體狀態(tài)再 kubectl describe pod 看事件Events和容器狀態(tài)最后針對(duì)有問(wèn)題的容器 kubectl logs 看日志。如果容器已經(jīng)崩潰重啟了加 --previous 參數(shù)看上一次啟動(dòng)的日志很多問(wèn)題就藏在那里。kubectl describe 輸出里面最重要的部分是 Events 字段它按時(shí)間順序記錄了 kubelet 對(duì) Pod 做的所有動(dòng)作和失敗原因。比如 FailedScheduling、Failed to pull image、Back-off restarting failed container 等每個(gè)關(guān)鍵事件后面一般都有原因和涉及的對(duì)象這足夠我們定位 80% 的問(wèn)題。5.2 常見(jiàn)錯(cuò)誤和排查建議速查表下面的表格是我根據(jù)這幾年實(shí)操整理出來(lái)的高頻 Pod 異常問(wèn)題幾乎每個(gè)集群都用得上現(xiàn)象根本原因最常見(jiàn)排查方向Pending資源不足、節(jié)點(diǎn)有污點(diǎn)、調(diào)度約束不滿足describe 看 FailedScheduling 事件檢查 requests 是否超出節(jié)點(diǎn)可分配ImagePullBackOff鏡像拉取失敗檢查鏡像名、tag 是否正確私有倉(cāng)庫(kù)認(rèn)證是否正確離線環(huán)境是否有鏡像倉(cāng)庫(kù)ErrImagePull鏡像不存在或倉(cāng)庫(kù)無(wú)權(quán)限查看 describe 事件中的具體報(bào)錯(cuò)not found / denied / timeoutCrashLoopBackOff應(yīng)用啟動(dòng)即崩潰或 liveness 探針失敗logs --previous 看上一次日志檢查啟動(dòng)命令和探針配置Running 但沒(méi) ReadyreadinessProbe 一直失敗檢查探針訪問(wèn)的路徑、端口是否真的可訪問(wèn)Running 但無(wú)法訪問(wèn)Service 沒(méi)匹配到標(biāo)簽、端口不一致檢查 Service selector 和 Pod labels 是否匹配檢查 targetPortContainerCreating 卡住存儲(chǔ)掛載不成功、CNI 網(wǎng)絡(luò)插件異常describe 看事件常見(jiàn)是 volume 掛載超時(shí)或 sandbox 創(chuàng)建失敗Terminating 卡住Pod 內(nèi)有進(jìn)程不響應(yīng) SIGTERM、finalizer 未完成檢查容器主進(jìn)程是否處理了優(yōu)雅退出必要時(shí) kubectl delete --forceOOMKilled容器內(nèi)存超過(guò) limit 被 OOM Killer 殺調(diào)大內(nèi)存 limit 或優(yōu)化應(yīng)用內(nèi)存檢查 QoS 級(jí)別Unknown節(jié)點(diǎn)失聯(lián)kubelet 心跳中斷登錄節(jié)點(diǎn)查 kubelet 服務(wù)狀態(tài)檢查節(jié)點(diǎn)網(wǎng)絡(luò)和磁盤(pán)這些異常里我最想單獨(dú)說(shuō)一下 OOMKilled它是“重啟后容器可以起來(lái)跑一會(huì)兒又掛”的常見(jiàn)元兇如果沒(méi)看日志大概率會(huì)被誤解成“應(yīng)用代碼問(wèn)題”。用 kubectl describe pod 看容器狀態(tài)里的 Last State如果顯示 Reason: OOMKilled那就是內(nèi)存不夠不是業(yè)務(wù)代碼崩了。5.3 我實(shí)際踩過(guò)的坑和幾個(gè)現(xiàn)場(chǎng)經(jīng)驗(yàn)第一個(gè)坑是離線部署時(shí)鏡像拉不動(dòng)。內(nèi)網(wǎng)環(huán)境里 IfNotPresent 這個(gè)鏡像拉取策略本來(lái)沒(méi)問(wèn)題但如果你先手動(dòng) ctr -n k8s.io images import 導(dǎo)入了鏡像卻沒(méi)有把 tag 改成和 yaml 里完全一致kubelet 還是會(huì)去遠(yuǎn)端拉。這個(gè)問(wèn)題的排查時(shí)間往往特別長(zhǎng)因?yàn)橐磺锌雌饋?lái)都正常但 imagePullPolicy 不會(huì)自動(dòng)糾錯(cuò)。我的建議是離線環(huán)境一律顯式寫(xiě) imagePullPolicy: IfNotPresent并且部署前用 crictl images 核對(duì)節(jié)點(diǎn)上的鏡像 tag。第二個(gè)坑是探針的 initialDelaySeconds 設(shè)得太小。應(yīng)用啟動(dòng)需要 30 秒但 readinessProbe 的第 3 秒就開(kāi)始探測(cè)結(jié)果連續(xù)失敗 3 次Pod 被標(biāo)記未就緒流量就進(jìn)不來(lái)??雌饋?lái)像服務(wù)雪崩其實(shí)只是探針配置問(wèn)題。如果你部署的是個(gè)啟動(dòng)慢的 Java 應(yīng)用我建議配合 startupProbe 一起用把 startupProbe 的 failureThreshold 調(diào)大等它啟動(dòng)完畢后再接管后續(xù)探測(cè)。第三個(gè)坑是日志一直在刷但沒(méi)有關(guān)鍵信息。遇到 CrashLoopBackOff第一條命令我一般是 kubectl logs --previous --tail200如果還是沒(méi)有啟動(dòng)報(bào)錯(cuò)我會(huì)進(jìn)容器手動(dòng)執(zhí)行啟動(dòng)命令看進(jìn)程能否前臺(tái)運(yùn)行。很多基礎(chǔ)鏡像默認(rèn)通過(guò) shell 腳本啟動(dòng)shell 腳本里 cd 不存在的目錄或引用未注入的環(huán)境變量都會(huì)導(dǎo)致啟動(dòng)分鐘級(jí)崩潰而這種問(wèn)題看容器日志往往只是一個(gè)泛泛的退出碼非??简?yàn)?zāi)托摹_€有一個(gè)我覺(jué)得特別值得說(shuō)的經(jīng)驗(yàn)用 kubectl port-forward 臨時(shí)驗(yàn)證 Pod 內(nèi)部服務(wù)。比如我只想確認(rèn) nginx Pod 內(nèi)部能否正常訪問(wèn) PHP寫(xiě) Service 之前可以先 port-forward 到本地直接 curl 一下。這比構(gòu)建完整 Service 后再測(cè)試快很多也方便區(qū)分問(wèn)題出在 Pod 自身還是出在 Service 層。6. 從 Pod 到集群常用命令與學(xué)習(xí)路徑建議6.1 Pod 相關(guān)命令必須滾瓜爛熟學(xué) k8s 最忌諱是只會(huì)看 kubectl get pods完整排查一套流程下來(lái)以下命令基本缺一不可# 查看 Pod 列表和簡(jiǎn)要狀態(tài) kubectl get pods -o wide # 查看 Pod 詳細(xì)信息重點(diǎn)是 Events 和容器狀態(tài) kubectl describe pod pod-name # 實(shí)時(shí)查看 Pod 日志 kubectl logs -f pod-name # 查看崩潰容器的上一次日志 kubectl logs pod-name --previous # 進(jìn)入 Pod 內(nèi)部容器 kubectl exec -it pod-name -- /bin/sh # 本地端口轉(zhuǎn)發(fā)到 Pod kubectl port-forward pod/pod-name 8080:80 # 查看節(jié)點(diǎn)資源占用排查調(diào)度失敗 kubectl top nodes # 查看所有命名空間下的 Pod kubectl get pods -A用 kubectl get pods 時(shí)我習(xí)慣加 -o wide因?yàn)樗鼤?huì)顯示出 Pod IP 和所在的節(jié)點(diǎn)一眼就能看出調(diào)度分布是否合理。describe 和 top 是排查資源問(wèn)題的兩大法寶不要省。6.2 學(xué)習(xí)閉環(huán)Pod 是入口但不要停在入口很多人問(wèn)怎么快速上手 k8s我的建議是一致的先造一個(gè)小集群然后用 Pod 把你的第一個(gè)服務(wù)跑起來(lái)再把 Deployment、Service、Ingress 串起來(lái)打通“從 Pod 到對(duì)外訪問(wèn)”這條鏈路然后逐步加探針、加資源限制、加自動(dòng)擴(kuò)縮容。Pod 是這個(gè)閉環(huán)的核心起點(diǎn)但學(xué)習(xí)不應(yīng)止步于此。很多網(wǎng)上流傳的“k8s 經(jīng)典版”教程其實(shí)講的就是把單機(jī) Docker Compose 的應(yīng)用比如 LNMP搬進(jìn)集群的過(guò)程這確實(shí)是最適合實(shí)戰(zhàn)的教學(xué)路徑。你從 docker compose 升級(jí)到 k8s 時(shí)第一件要適應(yīng)的就是思維方式的變化Compose 里的 service 在 k8s 里可能是一個(gè) Deployment Service或者一個(gè) Pod 里的多容器Compose 里的 depends_on 在 k8s 里變成了探針配合優(yōu)雅退出Compose 里配置的端口映射在 k8s 里要讓位給 Service 和 Ingress。這些變化本質(zhì)上都發(fā)生在 Pod 這一抽象層上。如果是用 Rancher 這類圖形界面管理集群也是一樣的道理——界面上減少了你敲命令的頻率但 Pod 的狀態(tài)、事件、日志這些核心信息不會(huì)變理解底層機(jī)制才能正確操作那些按鈕。二進(jìn)制部署 k8s 則更鍛煉你對(duì)組件和網(wǎng)絡(luò)的理解至少你親手搭過(guò)一遍集群之后再遇到“Pod 無(wú)法跨節(jié)點(diǎn)通信”“kubelet 沒(méi)起來(lái)導(dǎo)致 Pod 一直 Pending”這類問(wèn)題定位速度會(huì)快得多。7. 最后的幾個(gè)小建議回到標(biāo)題“k8s 中的 Pod”寫(xiě)了這么多最后說(shuō)幾個(gè)實(shí)操層面的個(gè)人感悟。一個(gè)是我每次教學(xué)和排查時(shí)都會(huì)強(qiáng)調(diào)遇到問(wèn)題先 kubectl describe pod這比任何調(diào)試工具都值得依賴。因?yàn)?describe 里的 Events 記錄了 kubelet 對(duì) Pod 做過(guò)的每一個(gè)關(guān)鍵動(dòng)作和失敗原因很多時(shí)候問(wèn)題根因已經(jīng)寫(xiě)在里面只是你沒(méi)看。另一個(gè)是寫(xiě) yaml 時(shí)盡量把標(biāo)簽labels寫(xiě)規(guī)范。Pod 的標(biāo)簽是 Service、Deployment、監(jiān)控告警相互關(guān)聯(lián)的橋梁標(biāo)簽設(shè)計(jì)混亂會(huì)直接導(dǎo)致 Service 選不上 Pod、監(jiān)控抓不到目標(biāo)、滾動(dòng)更新誤傷其他工作負(fù)載。哪怕你用的是 Rancher 這種圖形化工具標(biāo)簽和 selector 的匹配邏輯也不會(huì)變。最后一點(diǎn)是千萬(wàn)別怕實(shí)驗(yàn)時(shí)把 Pod 搞掛。k8s 的聲明式設(shè)計(jì)讓你可以隨便刪除、重建、滾動(dòng)更新試錯(cuò)的成本很低。真正值得投入時(shí)間的不是記住每個(gè)命令參數(shù)而是理解 Pod 在整個(gè)調(diào)度、網(wǎng)絡(luò)、存儲(chǔ)模型里的位置——把這個(gè)抽象層吃透了后面學(xué) StatefulSet、DaemonSet、Operator、自定義控制器你會(huì)發(fā)現(xiàn)全都是同一個(gè)底層邏輯在延伸。我從 Docker 單機(jī)時(shí)代走到現(xiàn)在最深刻的體會(huì)就是Pod 不是容器之上多套了一個(gè)概念它是理解 k8s 一切編排能力的地基。