
凌晨三點(diǎn)的告警電話我接過太多次了但每一次還是讓人瞬間清醒。業(yè)務(wù)同學(xué)在群里說“某某服務(wù)超時(shí)率飆升趕緊看看”我打開監(jiān)控面板翻了半天Pod 全是 RunningCPU、內(nèi)存曲線平得像一條直線連日志都沒有報(bào)錯(cuò)??煞?wù)就是訪問不通。當(dāng)時(shí)的第一反應(yīng)是檢查網(wǎng)絡(luò)插件、防火墻、安全組折騰了一大圈最后才發(fā)現(xiàn)問題出在一個(gè)不起眼的地方——Invalid Container Port。這個(gè)場景在 Kubernetes 里非常典型資源都正常、Pod 也活著、網(wǎng)絡(luò)看起來也沒問題但流量就是到不了應(yīng)用。這篇排障手記我就把“端口不通但資源都正常”這一類問題的完整排查思路、根因分析和實(shí)操步驟整理出來尤其適合正在做 Kubernetes 運(yùn)維、DevOps 或者后端開發(fā)的朋友遇到同類故障時(shí)可以直接照著抄。1. 癥狀與初始定位明明一切正常為什么就是連不上1.1 這類問題最容易讓人誤判的三個(gè)方向先說說我那次踩坑的經(jīng)歷。業(yè)務(wù)服務(wù)通過 Service 對外暴露監(jiān)控顯示異常之后我先后懷疑過三件事。第一個(gè)懷疑的是 CNI 網(wǎng)絡(luò)插件。因?yàn)?Kubernetes 的 Pod 網(wǎng)絡(luò)是靠 Calico、Flannel、Cilium 這類組件來維護(hù)的一旦網(wǎng)絡(luò)插件出問題最常見的表象就是端口不通。我當(dāng)時(shí)先看了網(wǎng)絡(luò)插件的 Pod 狀態(tài)全部正常又看了節(jié)點(diǎn)上的網(wǎng)絡(luò)策略也沒有異常。這一輪排查純屬浪費(fèi)時(shí)間。第二個(gè)懷疑的是云平臺的安全組和防火墻。因?yàn)樵谠骗h(huán)境里安全組規(guī)則確實(shí)會影響 NodePort 和 LoadBalancer 類型的訪問。我把安全組規(guī)則翻了又翻發(fā)現(xiàn)規(guī)則都是對的放行的端口也沒問題。后來證明這個(gè)方向也是錯(cuò)的。第三個(gè)懷疑的是 Service 的 selector 沒有匹配上 Pod。這個(gè)方向?qū)α艘话搿獑栴}確實(shí)和 Service 有關(guān)但我當(dāng)時(shí)只看了一眼 selector發(fā)現(xiàn)標(biāo)簽?zāi)芷ヅ渖?Pod就以為 Service 配置沒問題。實(shí)際上問題出在更隱蔽的 targetPort 上?,F(xiàn)在回頭看這類問題之所以容易繞彎路是因?yàn)樗耐庠诒憩F(xiàn)太像“網(wǎng)絡(luò)不通”但資源指標(biāo)又太像“一切正?!?。Pod 沒重啟、內(nèi)存沒飆升、CPU 也沒打滿任何人第一反應(yīng)都不會是配置問題。但恰恰是這種“看著一切正?!钡谋硐笞钊菀鬃屓撕雎宰罨A(chǔ)的配置檢查。1.2 先理清流量路徑再動手吃了一次虧之后我養(yǎng)成了一個(gè)習(xí)慣遇到端口不通先不碰網(wǎng)絡(luò)插件也不碰防火墻而是先理清楚流量走的哪條路徑。Kubernetes 里訪問一個(gè)應(yīng)用的路徑通常有幾種直接訪問 Pod IP這個(gè)路徑最底層繞過了 Service、kube-proxy、DNS 轉(zhuǎn)發(fā)直接打到容器網(wǎng)卡。通過 ClusterIP 訪問 Service這是最常用的路徑流量經(jīng)過 kube-proxy 轉(zhuǎn)發(fā)的 iptables 或 IPVS 規(guī)則再到達(dá)后端 Pod。通過 NodePort 或 LoadBalancer 訪問在 ClusterIP 的基礎(chǔ)上多了一層節(jié)點(diǎn)端口映射。通過 Ingress 訪問多一層七層轉(zhuǎn)發(fā)。排查的時(shí)候要一步一步收縮范圍。我的做法是先用kubectl run臨時(shí)起一個(gè)調(diào)試 Pod里面放 curl 或 wget分別測試這幾條路徑kubectl run debug --imagenicolaka/netshoot --rm -it --restartNever -- /bin/bash進(jìn)去之后先直連 Pod IPcurl http://Pod-IP:port如果直連 Pod IP 能通說明容器里的進(jìn)程本身在監(jiān)聽、端口沒問題那問題就出在 Service 層。如果直連都不通問題大概率在容器內(nèi)部比如進(jìn)程沒監(jiān)聽、監(jiān)聽地址綁錯(cuò)了、或者協(xié)議不匹配。這個(gè)順序至關(guān)重要。先確認(rèn)底層通不通再往上層查效率至少提高一倍。我見過不少同事一上來就去翻 kube-proxy 日志或 iptables 規(guī)則翻了半天才發(fā)現(xiàn)容器里監(jiān)聽的端口跟 Service 配的 targetPort 根本不是同一個(gè)數(shù)?;A(chǔ)層沒確認(rèn)上層查得再深都是白搭。2. Invalid Container Port 到底是誰在“判定無效”2.1 containerPort 只是“說明書”不是“開關(guān)”Kubernetes 里有個(gè)非常容易被誤解的概念Pod 的containerPort字段。很多新手以為寫了這個(gè)字段Kubernetes 就會幫你在容器里打開某個(gè)端口。實(shí)際上完全不是這么回事。containerPort不是一個(gè)“開關(guān)”K8s 不會去監(jiān)聽這個(gè)端口也管不了容器進(jìn)程到底有沒有在這個(gè)端口上做監(jiān)聽。它只是一份“說明書”把信息寫在 Pod 的定義里告訴別人“這個(gè)容器預(yù)期會使用這樣的端口”。真正把端口打開并監(jiān)聽的是容器里的業(yè)務(wù)進(jìn)程本身。我習(xí)慣用一個(gè)生活化的比喻來解釋這件事containerPort相當(dāng)于你家門口貼的門牌號寫的是“302室”。Kubernetes 的 Service 就像快遞員按照門牌號去送包裹。但如果 302 房間里面根本沒有人在聽門鈴——也就是說容器進(jìn)程根本沒有監(jiān)聽這個(gè)端口——快遞員按了半天門鈴沒人開門包裹就只能被退回。此時(shí)快遞員會報(bào)告“這個(gè)門鈴無效”這就是我們看到的 Invalid Container Port 現(xiàn)象。理解了這一點(diǎn)你就明白為什么“資源都正?!眳s“端口不通”了容器里的進(jìn)程確實(shí)在跑內(nèi)存、CPU 都正常但它要么沒有監(jiān)聽 Service 指向的那個(gè)端口要么監(jiān)聽地址不對要么 Service 引用了一個(gè)根本不存在于 Pod 定義里的端口名。Pod 本身活著但流量沒人接。2.2 五種常見成因逐個(gè)拆解根據(jù)我處理過的真實(shí)故障Invalid Container Port 這類問題可以歸納成下面五種典型成因。第一種也是最常見的一種targetPort使用了命名端口但在 Pod 里找不到同名的containerPort或者大小寫不一致。Kubernetes 的targetPort支持兩種寫法數(shù)字端口和命名端口。當(dāng)targetPort寫的是數(shù)字時(shí)控制面會直接把流量轉(zhuǎn)發(fā)到 Pod IP 的對應(yīng)數(shù)字端口這個(gè)場景下即使containerPort沒有聲明也能通。但當(dāng)targetPort寫成命名端口時(shí)控制面就必須去 Pod 的 spec 里找名字完全一致的containerPort。注意名字是完全區(qū)分大小寫的HTTP和http是兩個(gè)完全不同的名字。我踩過最經(jīng)典的一個(gè)坑是Service 里寫的是targetPort: HTTPPod 里聲明的是containerPort: 80, name: http。服務(wù)控制面找不到HTTP這個(gè)名字于是把 endpoint 置空表現(xiàn)就是端口完全不通但 Pod 一切正常。第二種targetPort配的數(shù)字端口跟容器進(jìn)程實(shí)際監(jiān)聽的端口不是同一個(gè)。這種情況不需要命名端口匹配endpoints 也是能生成出來的Service 的底層規(guī)則也正常但流量到容器門口時(shí)容器里沒有進(jìn)程在對應(yīng)端口監(jiān)聽。比如業(yè)務(wù)進(jìn)程監(jiān)聽的是 8080Service 的targetPort寫成 80結(jié)果就是訪問超時(shí)或連接拒絕。問題的根源在于K8s 不會因?yàn)槟阏f了 80 就去調(diào)用進(jìn)程切換端口它只是原樣把流量送過去。第三種多容器場景下端口名定義在了錯(cuò)誤的容器上。一個(gè) Pod 里可能跑多個(gè)容器比如業(yè)務(wù)容器加 sidecar 容器。如果targetPort的命名端口匹配到了 sidecar 容器而業(yè)務(wù)進(jìn)程實(shí)際在另一個(gè)容器里監(jiān)聽就會發(fā)生“端口聲明有但實(shí)際沒人處理業(yè)務(wù)流量”的情況。這種問題檢查起來特別費(fèi)勁因?yàn)槊謱Φ蒙?、endpoints 也正常就是不通。第四種協(xié)議不匹配。Service 定義端口的時(shí)候可以指定protocol默認(rèn)是 TCP。如果容器里業(yè)務(wù)進(jìn)程實(shí)際監(jiān)聽的是通過 UDP 通信的服務(wù)而 Service 定義的protocol: TCP同樣會出現(xiàn)端口不通。反過來也是同樣的問題。這種問題在日志里最不明顯因?yàn)?TCP 和 UDP 都是同一個(gè)端口號肉眼很難發(fā)現(xiàn)。第五種業(yè)務(wù)進(jìn)程監(jiān)聽的地址不對。這不是 Kubernetes 的配置問題但很容易被誤認(rèn)為 K8s 問題。比如很多 Node 應(yīng)用默認(rèn)只在127.0.0.1上監(jiān)聽這個(gè)地址在容器里只有自己能看到外部流量從 eth0 進(jìn)來自然連不上。查看監(jiān)聽狀態(tài)時(shí)一定要確認(rèn)監(jiān)聽地址是0.0.0.0或至少是 Pod 的網(wǎng)卡地址。2.3 事件日志里的有效線索遇到這類問題一定要會看事件日志。kubectl describe svc的輸出里會直接告訴我們關(guān)鍵線索。kubectl describe svc demo-svc正常情況下輸出里會顯示Endpoints字段帶有實(shí)際的 Pod IP如果端口聲明有問題Endpoints會顯示none。與此同時(shí)Service 的TargetPort字段會顯示你配置的值比如HTTP/TCP這時(shí)候你去對 Pod 里的containerPort一眼就能發(fā)現(xiàn)大小寫不一致的問題。在比較新的 Kubernetes 版本里EndpointSlice Controller 還會在事件里輸出類似 “invalid container port” 的警告信息直接告訴你目標(biāo)端口在 Pod 里找不到對應(yīng)項(xiàng)。不同版本的具體文案可能不一樣但排查思路完全一致事件里只要提到invalid container port或failed to build model for service十有八九就是targetPort引用出了問題。3. 完整排查流程從 Pod 命令到抓包驗(yàn)證下面這段是我現(xiàn)在遇到“端口不通但資源都正常”類問題時(shí)實(shí)際執(zhí)行的標(biāo)準(zhǔn)流程每一步都有對應(yīng)的命令和判斷邏輯你可以直接拿來當(dāng)模板用。3.1 第一步確認(rèn)端口是“假活”還是“真活”第一個(gè)動作永遠(yuǎn)不是查網(wǎng)絡(luò)插件而是進(jìn)到 Pod 里看進(jìn)程到底監(jiān)聽了什么。這一步的目的是回答最基礎(chǔ)的問題容器里到底有沒有進(jìn)程在監(jiān)聽對應(yīng)的端口kubectl exec -it pod-name -n namespace -- netstat -tulpn如果 Pod 里有多個(gè)容器記得加-c container-name指定容器kubectl exec -it pod-name -n namespace -c container-name -- netstat -tulpn有些基礎(chǔ)鏡像里沒有netstat可以用ss命令kubectl exec -it pod-name -n namespace -- ss -tlnp判斷邏輯很簡單如果看到目標(biāo)端口有進(jìn)程在監(jiān)聽而且監(jiān)聽地址是0.0.0.0或::那容器這一層沒問題。如果端口根本沒有監(jiān)聽記錄說明業(yè)務(wù)進(jìn)程沒起來或者起來了但監(jiān)聽的不是這個(gè)端口。這時(shí)候去容器里看進(jìn)程列表確認(rèn)進(jìn)程是否活著。如果監(jiān)聽了但監(jiān)聽地址是127.0.0.1那流量從外部進(jìn)不來需要改應(yīng)用的監(jiān)聽配置讓它監(jiān)聽0.0.0.0。這個(gè)步驟能快速把問題縮小到“應(yīng)用問題”還是“Kubernetes 配置問題”。我在這一步至少救回過三次誤判都是因?yàn)橄热霝橹饔X得資源正常進(jìn)程就一定健康結(jié)果發(fā)現(xiàn)業(yè)務(wù)進(jìn)程其實(shí)已經(jīng)悄然退出了容器靠 init 進(jìn)程活著看著是 Running 但業(yè)務(wù)早已不服務(wù)。3.2 第二步Service 與 Endpoints 的關(guān)聯(lián)排查如果容器里監(jiān)聽正常下一步就看 Service 層。先看 Service 配置kubectl get svc service-name -n namespace -o yaml重點(diǎn)看三個(gè)字段selector、ports[].port、ports[].targetPort。然后把 selector 選中的 Pod 標(biāo)簽對比一下確認(rèn)匹配關(guān)系kubectl get pod -n namespace --show-labels再查看 endpoinds。老版本里這個(gè)對象叫 Endpoints新版本切到了 EndpointSlice但kubectl get endpoints依然能看到聚合結(jié)果排查思路是一樣的kubectl get endpoints service-name -n namespace這里會出現(xiàn)三種結(jié)果第一Endpoints顯示none說明后端的地址列表沒有生成。此時(shí)有兩種可能selector 沒匹配到任何 Pod或者targetPort引用的命名端口在 Pod 里找不到。區(qū)分方法很簡單看kubectl describe svc的輸出如果 selector 匹配了 Pod但Endpoints還是空那幾乎可以斷定是targetPort的問題。第二Endpoints顯示了 Pod IP用kubectl get endpoints能看到類似10.244.1.5:8080這樣的記錄。這說明 Service 層正常endpoint 已經(jīng)生成。如果這時(shí)候再從集群內(nèi)訪問 ClusterIP 不通問題就出在 kube-proxy 轉(zhuǎn)發(fā)這一層。第三Endpoints顯示 Pod IP 和端口但端口號跟你預(yù)期的不一樣。這通常也是targetPort的問題——你寫成命名端口匹配到了 Pod 里另一個(gè)容器的端口。這一步是最關(guān)鍵的一步。一個(gè)空 Endpoints 直接說明問題在 Service 引用上不用再去翻網(wǎng)絡(luò)插件。3.3 第三步kube-proxy 規(guī)則層面的驗(yàn)證如果前兩步都正常Service 和 endpoints 都有內(nèi)容但通過 ClusterIP 訪問還是不通這時(shí)候再來看節(jié)點(diǎn)上的轉(zhuǎn)發(fā)規(guī)則。先確認(rèn) kube-proxy 使用的模式?,F(xiàn)在大多數(shù)集群默認(rèn) IPVS 模式有些還是 iptables??梢杂孟旅娴拿畲_認(rèn)kubectl logs -n kube-system kube-proxy-pod | grep Using如果是 iptables 模式在 Service 對應(yīng)的節(jié)點(diǎn)上查一下 nat 表規(guī)則iptables -t nat -S | grep service-name如果是 IPVS 模式ipvsadm -L -n | grep ClusterIP要特別提醒的是這一步不要放在排查的第一步去做。原因很簡單iptables 和 IPVS 規(guī)則非常多在沒確認(rèn) endpoints 正常之前去看規(guī)則等于大海撈針。而且 kube-proxy 的規(guī)則本身就是根據(jù) endpoints 生成的endpoints 是空的規(guī)則自然也是空的endpoints 正常規(guī)則大多也正常。在這一步你還可以做一個(gè)快速的連通性測試。找一個(gè)集群節(jié)點(diǎn)直接 curl Service 的 ClusterIPcurl http://ClusterIP:port如果這個(gè)都不通而直連 Pod IP 又是通的再考慮是不是 kube-proxy 的問題。有一種比較少見但真實(shí)存在的情況endpoints 變化后kube-proxy 規(guī)則刷新有延遲或者卡住了。這時(shí)候解決辦法通常是刪除并重建一個(gè) Pod強(qiáng)制觸發(fā) endpoint 刷新極端情況下重啟 kube-proxy 也能解決。3.4 修復(fù)操作兩種典型場景的改法定位到根因之后修改通常很簡單但操作過程中有一些細(xì)節(jié)需要注意。場景一命名端口大小寫不一致。Service 里寫的是spec: ports: - port: 8080 targetPort: HTTP而 Pod 里聲明的是ports: - containerPort: 80 name: http解決辦法是把 Service 的targetPort改成http或者把 Pod 里的端口名改成HTTP。修改 Service 是動態(tài)生效的改完之后 endpoint 會立刻重建不用重啟任何 Pod。場景二targetPort 寫的是數(shù)字端口但和容器實(shí)際監(jiān)聽端口不一致。Service 里spec: ports: - port: 8080 targetPort: 8080容器實(shí)際監(jiān)聽的是 80 端口ports: - containerPort: 80這種情況要改 Service 的targetPort為 80或者改容器監(jiān)聽的端口為 8080取決于業(yè)務(wù)約定。如果改 Pod 配置記得是修改 Deployment 的模板然后滾動更新而不是直接改運(yùn)行中的 Pod否則 Pod 重建后配置就沒了。我個(gè)人的一個(gè)建議是能用數(shù)字端口就用數(shù)字端口簡單直接不依賴containerPort的聲明如果團(tuán)隊(duì)規(guī)范要求解耦就必須使用命名端口并且把命名規(guī)則寫進(jìn)規(guī)范避免“HTTP”和“http”這種低級問題反復(fù)出現(xiàn)。4. 常見問題速查表與避坑經(jīng)驗(yàn)4.1 癥狀-根因-解法速查表實(shí)戰(zhàn)里遇到的情況多種多樣我整理了一張速查表做二次排障的時(shí)候?qū)χ匆谎勰苁〔簧贂r(shí)間。癥狀最可能根因快速確認(rèn)方法解法資源正常SVC訪問不通Endpoints為空targetPort是命名端口但Pod里無同名端口或大小寫不一致describe svc對比TargetPort與Pod端口名修正端口名或改用數(shù)字端口Endpoints有IP但訪問不通直連PodIP也不通容器進(jìn)程沒監(jiān)聽對應(yīng)端口或監(jiān)聽地址為127.0.0.1exec進(jìn)Pod執(zhí)行netstat/ss改應(yīng)用監(jiān)聽0.0.0.0改containerPort聲明Endpoints有IP直連PodIP通ClusterIP不通kube-proxy規(guī)則刷新異?;蜓舆t節(jié)點(diǎn)上curl ClusterIP檢查iptables/ipvs刪掉重建Pod觸發(fā)endpoint刷新必要時(shí)重啟kube-proxy只有NodePort不通ClusterIP正常節(jié)點(diǎn)端口被占用或安全組未放行檢查nodePort占用檢查安全組調(diào)整nodePort端口配置安全組放行headless Service訪問不通繞過了kube-proxy直接走Pod IPdig解析域名直連解析出的Pod IP按Pod IP不通的方向排查多容器場景下命名端口聲明在錯(cuò)誤容器targetPort匹配到了sidecar容器端口查看Pod內(nèi)多個(gè)容器端口名將containerPort聲明調(diào)整到正確的容器Service的protocol和業(yè)務(wù)實(shí)際協(xié)議不一致TCP/UDP不匹配確認(rèn)業(yè)務(wù)實(shí)際使用的協(xié)議修改Service的protocol字段4.2 讓這類問題少發(fā)生的幾條工程習(xí)慣排查是事了但反復(fù)踩同一個(gè)坑就是工程管理的問題了。我歸納了幾條實(shí)際操作中驗(yàn)證過的經(jīng)驗(yàn)。第一嚴(yán)格約定端口命名規(guī)則。如果團(tuán)隊(duì)決定使用命名端口就一定要在小寫、短橫線分隔的規(guī)范上達(dá)成一致。寫 Service 的人、改 Pod 模板的人、負(fù)責(zé)評審的人都要清楚targetPort是大小寫敏感的。這個(gè)問題在 CI/CD 流程里很容易被忽視因?yàn)?YAML 校驗(yàn)通常只檢查語法不檢查端口名是否真的存在。第二區(qū)分“Pod 狀態(tài)正?!焙汀皯?yīng)用可用”是兩碼事。Kubernetes 里的 Running 只說明容器起來了不代表業(yè)務(wù)就緒。排查端口不通問題時(shí)先看kubectl get pod輸出的 READY 列確認(rèn)前面的數(shù)字和容器數(shù)一致。如果Running 但 0/1 Ready說明探針沒通過endpoint 也不會掛到 Service 上這同樣會表現(xiàn)為“資源正常但端口不通”。第三做好端口連通性監(jiān)控。Kubernetes 平臺自己有健康檢查還不夠最好在集群外部加一層黑盒監(jiān)控比如定時(shí)從測試機(jī)去撥測 Service 的端口或者用 Prometheus Blackbox Exporter 做 TCP 探測。只有業(yè)務(wù)視角的監(jiān)控才能在你睡覺的時(shí)候幫你從“Pod 還活著但服務(wù)掛了”的假象里醒過來。第四排障順序固化下來。我現(xiàn)在遇到端口不通心里默認(rèn)走的流程是先看 READY 列再 exec 到 Pod 里確認(rèn)監(jiān)聽再查 Endpoints再查 Service 的 targetPort最后才考慮 kube-proxy 和網(wǎng)絡(luò)插件。這套順序能把 90% 的“端口不通但資源正?!眴栴}在十分鐘內(nèi)定位。4.3 一個(gè)容易被忽略的“假端口”陷阱最后單獨(dú)提一個(gè)小眾但真實(shí)發(fā)生過的場景有些業(yè)務(wù)進(jìn)程監(jiān)聽的端口是 IPv6 地址比如監(jiān)聽在[::]:8080而 Kubernetes 的網(wǎng)絡(luò)路徑走的是 IPv4。從 IPv4 角度看去這個(gè)端口是“不存在”的因?yàn)?netstat 里可能看到tcp6而不是tcp。某些語言或框架默認(rèn)綁定到 IPv6 時(shí)會同時(shí)支持 v4-mapped 地址但并非所有框架都會這樣做。真遇到這種情況去容器里執(zhí)行cat /proc/net/tcp /proc/net/tcp6對比 IPv4 和 IPv6 的監(jiān)聽情況很快能找到答案。解決辦法是改應(yīng)用的監(jiān)聽配置明確綁定到0.0.0.0。我個(gè)人在實(shí)際操作中的體會是這類“端口不通但資源都正常”的問題最大的困難從來不是技術(shù)本身而是心理上的“它看起來沒事”帶來的迷惑感。資源指標(biāo)正常、Pod 沒崩、日志干凈太容易讓人把注意力引向網(wǎng)絡(luò)層。但只要耐住性子按“Pod 內(nèi)監(jiān)聽 → Endpoints → Service targetPort → kube-proxy 轉(zhuǎn)發(fā)”的順序一步步排除幾乎都能在十分鐘內(nèi)把根因揪出來。最后再分享一個(gè)小技巧我寫 Service 時(shí)習(xí)慣在targetPort上直接使用命名端口并且把 Pod 的containerPort名字寫進(jìn)注釋里。這個(gè)習(xí)慣看起來不起眼但配合代碼評審能從源頭攔住一大批大小寫不一致、端口名對不上的問題。如果你現(xiàn)在正被端口不通的問題折磨先別急著重啟 Pod花兩分鐘看一眼kubectl get endpoints很多時(shí)候答案就擺在那里。