指南:從ClusterIP到LoadBalancer的坑與排查)
K8S里Service這東西表面上看就是給Pod提供一個穩(wěn)定的訪問入口真用起來才發(fā)現(xiàn)里面門道不少。從最簡單的ClusterIP到需要對接云廠商的LoadBalancer從寫YAML到排查Endpoints為空每一步都藏著細節(jié)。這篇總結(jié)我按自己平時排查問題的思路來寫不按官方文檔的順序背書重點放在你真正會踩到的坑和API字段背后的邏輯上。1. 為什么需要Service理解了這點后面的坑都好避剛接觸K8S的人經(jīng)常問一個問題Pod有IP我直接訪問Pod不就行了為什么非要搞個Service出來這個問題想清楚后面所有API字段的用途你基本都能自己推導出來。1.1 Pod的IP天生就不靠譜Pod的IP地址不是固定的。Deployment滾動更新時舊的Pod被殺掉新的Pod被創(chuàng)建IP完全換了一輪。就算不滾動更新Pod被驅(qū)逐、節(jié)點宕機、手動刪除重建IP都會變。你不可能在應用配置里寫死一個Pod的IP它今天能用明天就404了。更麻煩的是副本擴容。一個Deployment起了三副本三個Pod各自有獨立IP客戶端要訪問哪個如果讓客戶端自己維護這三個IP的列表等于把負載均衡的邏輯踢給了調(diào)用方這在微服務架構(gòu)里根本不可行。Service正是為了解決這兩個核心問題而生的給一組動態(tài)變化的Pod提供一個固定的虛擬IPClusterIP同時在這組Pod之間做負載均衡。從API設(shè)計的角度看Service本質(zhì)上是穩(wěn)定入口 動態(tài)后端的抽象層。1.2 Service在K8S網(wǎng)絡(luò)架構(gòu)中的角色K8S集群里的網(wǎng)絡(luò)流量路徑大致是客戶端 - Service(ClusterIP) - kube-proxy規(guī)則 - Pod IP。Service對象本身不干活真正轉(zhuǎn)發(fā)流量的是每個節(jié)點上的kube-proxy組件現(xiàn)在也有其他實現(xiàn)但主流還是這個。kube-proxy通過監(jiān)聽API Server上Service和Endpoints的變化動態(tài)維護節(jié)點上的轉(zhuǎn)發(fā)規(guī)則。你可以把Service理解成一個聲明Endpoints是它對應的后端列表kube-proxy是把聲明落到實處的執(zhí)行者。這個三層分離的設(shè)計是理解Service所有行為的鑰匙Service的YAML寫得不完整kube-proxy就不知道該往哪兒轉(zhuǎn)發(fā)Endpoints沒生成Service就是一個空殼訪問它只會得到連接拒絕。我實際排查中遇到的所有Service不通的問題幾乎都能歸因到這三層中某一層出了問題。先檢查Service有沒有選到Pod再看Endpoints是否生成最后才懷疑kube-proxy這個排查順序能省下大量時間。2. Service API對象拆解核心字段逐個過K8S所有對象都是聲明式APIService也不例外。一個完整的Service定義包含apiVersion、kind、metadata、spec幾個部分最關(guān)鍵的是spec。我把核心字段按重要程度排個序每個都說說它的作用和容易出問題的細節(jié)。2.1 apiVersion和kind不能寫錯Service的apiVersion是v1kind是Service這是最基礎(chǔ)的信息。有人會覺得這有什么好說的但我見過有人把apiVersion寫成apps/v1直接把創(chuàng)建請求打回來。這倆字段是API Server的路由信息告訴它你要創(chuàng)建什么類型的對象版本不對就是404。apiVersion: v1 kind: Service metadata: name: my-service namespace: default spec: selector: app: my-app ports: - port: 80 targetPort: 80802.2 metadata里容易被忽略的注意事項metadata里name是必填的并且在namespace內(nèi)必須唯一。namespace如果沒寫默認就是default。這里有個實際開發(fā)中常見的坑如果你是用kubectl create -f方式創(chuàng)建YAML里沒寫namespace對象會進default但你的Deployment可能部署在別的namespaceselector選不到Service就永遠沒有Endpoints。另外metadata里可以加annotations有些云廠商的LoadBalancer類型Service會通過annotations來控制負載均衡器的具體行為比如是否分配公網(wǎng)IP、帶寬上限等。這塊每個云廠商的鍵名都不同用到的時候需要查對應廠商的文檔。2.3 spec.selector決定后端Pod列表的關(guān)鍵spec.selector是Service和Pod之間的連接器。kube-proxy和API Server的Endpoints控制器會拿你的selector和集群里所有Pod的labels做匹配匹配上的PodIP會進入Endpoints列表。這里有幾個關(guān)鍵點selector里的每個label都必須出現(xiàn)在Pod上是AND的關(guān)系多寫一個不存在的label直接導致選不到任何PodPod的labels是可以在運行中修改的一旦修改導致不再匹配selector它的IP會立刻從Endpoints里被剔除已有連接會斷開。我在實際項目里踩過最深的坑是selector寫對了Pod labels看起來也對但就是選不上。最后發(fā)現(xiàn)原因是Pod所在的namespace和Service不一致Endpoints控制器只在同一個namespace內(nèi)做匹配。每個namespace是獨立的隔離域Service不能跨namespace選Pod。2.4 spec.ports端口映射的完整理解ports字段是數(shù)組可以配置多個端口。每個端口有三個核心配置項port、targetPort、protocol。portService對外暴露的端口ClusterIP上監(jiān)聽的端口客戶端訪問的就是這個端口。targetPort流量轉(zhuǎn)發(fā)到Pod時使用的端口。注意targetPort可以不填默認等于port。如果Pod里的容器監(jiān)聽的是8080而你想讓Service的端口是80就得顯式寫targetPort: 8080。protocol協(xié)議類型支持TCP、UDP、SCTP默認是TCP。UDP和TCP不能混用同一個Service端口不能同時監(jiān)聽TCP和UDP這是很多新手會犯的錯誤。targetPort還有一個明細用法可以用命名端口named port。如果你的Pod定義了命名端口Service里可以直接引用名字好處是改端口號時不用改Service的YAML。舉個例子# Pod里的containerPort定義 spec: containers: - name: app ports: - name: http containerPort: 8080 # Service引用命名端口 spec: ports: - port: 80 targetPort: http2.5 spec.type一個字段決定四種工作模式type字段是Service最核心的行為開關(guān)支持ClusterIP、NodePort、LoadBalancer、ExternalName四種取值。默認是ClusterIP。很多人在這一步就迷糊了不知道選哪種類型也不知道每種類型在實際環(huán)境中到底長什么樣。我的經(jīng)驗是按流量來源來判斷。集群內(nèi)部訪問或者用于微服務間調(diào)用用ClusterIP就夠了這也是默認選項。需要集群外部訪問但沒有現(xiàn)成的負載均衡器用NodePort在每個節(jié)點上開一個端口。部署在云平臺上需要自動創(chuàng)建云負載均衡器用LoadBalancer。只是想給集群外部的服務做一個DNS別名用ExternalName它甚至不創(chuàng)建ClusterIP和Endpoints。3. 四種Service類型怎么選端到端配置實例這一節(jié)把每種類型都過一遍每個都給出完整配置和實際使用場景能直接抄作業(yè)。3.1 ClusterIP默認也是用得最多的apiVersion: v1 kind: Service metadata: name: my-app-svc namespace: prod spec: type: ClusterIP selector: app: my-app env: prod ports: - name: http port: 80 targetPort: 8080 protocol: TCP這個Service創(chuàng)建后會分配一個ClusterIP比如10.96.88.88在集群內(nèi)部所有節(jié)點上都能通。訪問http://10.96.88.88:80kube-proxy會在選中的Pod里挑一個做轉(zhuǎn)發(fā)。注意幾個細節(jié)ClusterIP有默認的IP范圍通常是10.96.0.0/12具體看你的kube-apiserver啟動參數(shù)。如果你在配置里用了clusterIP: None這會讓Service變成headless service不分配ClusterIPDNS直接返回后端Pod的IP列表。headless service在需要直連Pod的場景比如數(shù)據(jù)庫主從或需要客戶端自己做負載均衡的場景下很有用。集群內(nèi)訪問Service通常不走ClusterIP而是走DNS。K8S內(nèi)置的DNS服務CoreDNS會自動為Service生成DNS記錄格式是service-name.namespace.svc.cluster.local。同namespace下可以直接用Service名訪問跨namespace要帶上namespace名再拼完整域名。這也是微服務之間互相調(diào)用的標準方式。3.2 NodePort外部訪問的入門方案apiVersion: v1 kind: Service metadata: name: my-app-nodeport namespace: prod spec: type: NodePort selector: app: my-app env: prod ports: - port: 80 targetPort: 8080 nodePort: 30080NodePort是在ClusterIP基礎(chǔ)上在每個節(jié)點上額外開一個端口默認范圍30000-32767。訪問方式變成http://任意節(jié)點IP:30080流量會從節(jié)點端口進來轉(zhuǎn)發(fā)到ClusterIP再轉(zhuǎn)發(fā)到Pod。nodePort字段可以手動指定也可以不寫讓K8S自動分配一個空閑端口。手動指定的好處是方便配置防火墻白名單和負載均衡器監(jiān)聽規(guī)則壞處是有可能被占用創(chuàng)建時API Server會檢查。使用NodePort有幾個實際注意點。節(jié)點IP得能從外部訪問到如果你有多套環(huán)境共用機器要規(guī)劃好端口別沖突。另外nodePort默認范圍是可以調(diào)的在kube-apiserver啟動參數(shù)里改--service-node-port-range但一般建議保持標準范圍避免外部設(shè)備不識別高位端口。NodePort適合什么場景本地開發(fā)、小規(guī)模集群、不想額外引入負載均衡器的場景。生產(chǎn)環(huán)境你要是直接用NodePort暴露到公網(wǎng)我見過不少但更常見的做法是NodePort配合外部的負載均衡器比如Nginx、HAProxy使用外部LB轉(zhuǎn)發(fā)到多個節(jié)點的NodePort。3.3 LoadBalancer云環(huán)境標準姿勢apiVersion: v1 kind: Service metadata: name: my-app-lb namespace: prod annotations: # 云廠商自定義的注解用于配置LB的具體參數(shù) # 具體鍵名因云廠商而異 spec: type: LoadBalancer selector: app: my-app env: prod ports: - port: 80 targetPort: 8080在云平臺上創(chuàng)建LoadBalancer類型的Service云廠商的控制器會自動調(diào)用云API創(chuàng)建一個負載均衡實例并且自動配置好監(jiān)聽器和后端轉(zhuǎn)發(fā)規(guī)則。你的應用等于直接擁有了一個可以從公網(wǎng)訪問的入口。注意LoadBalancer其實是NodePort的超集它內(nèi)部仍然會分配nodePort云LB會綁定到節(jié)點的NodePort上而不是直接綁定Pod。這意味著每個負載均衡Service額外的nodePort也會被分配只是在HostPort層面占用。如果你的集群是自建的比如裸金屬部署的K8S沒有云廠商的控制器LoadBalancer類型創(chuàng)建完會一直卡在Pending狀態(tài)需要安裝MetalLB這類實現(xiàn)。LoadBalancer的一個重要限制是每個Service都會創(chuàng)建一個獨立負載均衡實例成本和時延都得考慮。多個服務更經(jīng)濟的方式是用Ingress一個入口轉(zhuǎn)發(fā)到多個Service那是另一個話題這里不展開。3.4 ExternalName特殊但偶爾很管用apiVersion: v1 kind: Service metadata: name: external-db namespace: prod spec: type: ExternalName externalName: db.example.comExternalName是一個輕量級Service它不創(chuàng)建ClusterIP也沒有Endpoints只是在CoreDNS里配置了一條CNAME記錄。當你在集群內(nèi)訪問external-db.prod.svc.cluster.local時DNS解析會跳到db.example.com。它可以用來把外部服務的域名包裝成K8S內(nèi)部的Service名這樣應用配置里不需要硬編碼外部域名改起來方便。也可以用來對接一些不在集群內(nèi)的舊系統(tǒng)或者做服務遷移時的兼容層。需要注意ExternalName只支持DNS解析如果你要訪問的外部服務需要走HTTP并有路徑路由的需求ExternalName可能不夠用需要自己再包一層代理。3.5 四種類型的選擇對照我用一張表總結(jié)了下四種類型的關(guān)鍵差異方便你選型時不糾結(jié)。類型是否分配ClusterIP是否支持集群外訪問典型場景注意事項ClusterIP是否集群內(nèi)微服務間調(diào)用默認類型集群外不可直達NodePort是是通過節(jié)點IP端口簡單的外部訪問測試、小規(guī)模服務端口范圍30000-32767受節(jié)點IP可達性影響LoadBalancer是是通過云負載均衡器云上生產(chǎn)環(huán)境的公網(wǎng)入口依賴云廠商的控制器或MetalLBExternalName否取決于外部目標訪問集群外部已有服務只做DNS CNAME無流量轉(zhuǎn)發(fā)能力選型思路就一句集群內(nèi)訪問用ClusterIP需要外部訪問但沒有云資源用NodePort云上有LB需求用LoadBalancer訪問外部服務用ExternalName。4. 實操全流程從編寫YAML到調(diào)試驗證理論說完了實際動手的時候你得有一套標準的操作流程。一個Service從無到有到驗證通過整個流程包含四步確認后端Pod running、編寫Service YAML、創(chuàng)建Service并檢查Endpoints、實際驗證訪問。4.1 確認后端Pod的狀態(tài)和labelsService不是憑空出現(xiàn)的它的selector必須能匹配到Pod所以第一步是確認Deployment或者Pod已經(jīng)正常運行并且labels是你預期的那一套。# 查看命名空間下所有Pod及其labels kubectl -n prod get pods --show-labels輸出里會顯示每Pod的labels比如appmy-app,envprod。檢查labels時不要漏看HEAD部分有的Pod創(chuàng)建時label很多你只摘出自己需要的兩三對來匹配就行。還有人可能遇到這種情況Pod狀態(tài)顯示Running但通過Service訪問時一直超時。排查一圈發(fā)現(xiàn)Pod起來了但容器里的進程根本沒監(jiān)聽targetPort。Service能配置對Pod能匹配到但轉(zhuǎn)發(fā)后沒用因為健康檢查只到容器層面沒到端口層面。做個端口連通性測試很有必要。# 進入集群內(nèi)一個Pod測試后端端口是否真正可用 kubectl -n prod exec -it any-pod -- curl http://某個Pod的IP:80804.2 編寫Service YAML的通用模板我習慣用模板來避免因為筆誤出問題。一份最通用的Service YAML長這樣apiVersion: v1 kind: Service metadata: name: app-svc namespace: prod labels: app: app-svc spec: type: ClusterIP selector: app: my-app env: prod ports: - name: http port: 80 # Service暴露的端口 protocol: TCP targetPort: 8080 # 必須與容器實際監(jiān)聽端口一致 # 如果需要外部訪問按需調(diào)整type和對應配置保存為svc.yaml后執(zhí)行kubectl apply -f svc.yamlapply是聲明式的如果Service已存在則更新如果不存在則創(chuàng)建。用kubectl create也可以但它要求目標對象當前不存在在CI/CD腳本里我個人更推薦apply。4.3 創(chuàng)建后的驗證三部曲創(chuàng)建完Service不能直接走人至少做三件事驗證。第一步看Service基本信息kubectl -n prod get svc app-svc -o wide重點關(guān)注EXTERNAL-IP列和PORT(S)列。ClusterIP類型的EXTERNAL-IP是None這正常。LoadBalancer類型如果EXTERNAL-IP一直Pending就要檢查云廠商控制器是否正常。第二步檢查Endpointskubectl -n prod get endpoints app-svcEndpoints列表有IP說明Selector正確選了Pod。如果顯示none說明后端沒匹配上優(yōu)先檢查namespace和labels。這里我要強調(diào)一下Endpoints對象是Service的實際后端狀態(tài)它是由Endpoints控制器根據(jù)Selector自動生成的不要手動去改Endpoints雖然有API但改了會被控制器回填。第三步做實際的連通性測試。在集群內(nèi)任意節(jié)點或Pod里測試# 從集群內(nèi)Pod訪問Service kubectl -n prod exec -it any-pod -- curl http://app-svc.prod.svc.cluster.local如果用的是NodePort在節(jié)點上直接測curl http://nodeIP:30080三步走完Service通了基本就是真通了。4.4 會話保持配置的細節(jié)Stateful應用需要同一客戶端的請求都落到同一Pod上這就是會話保持。Service層面通過spec.sessionAffinity字段控制支持None和ClientIP兩種。spec: sessionAffinity: ClientIP sessionAffinityConfig: clientIP: timeoutSeconds: 10800配置成ClientIP后kube-proxy會根據(jù)客戶端來源IP做哈希同一個IP的請求會盡量轉(zhuǎn)發(fā)到同一個Pod。timeoutSeconds控制會話保持的超時時間默認10800秒即3小時。這里有個明顯的副作用需要意識到如果你在集群外部前面還有一層負載均衡器做NAT或者代理所有客戶端IP都變成LB的IP那會話保持就會失效所有流量都哈希到同一個Pod上完全喪失負載均衡能力。解決方案是用Ingress層的會話保持或應用層Session而不要依賴Service的ClientIP。5. 常見問題與排查技巧實錄Service問題排查是我日常工作中遇到頻率最高的。把所有遇到過的坑整理了一下按從最常見到最隱蔽排個序。5.1 Endpoints為空最經(jīng)典的問題癥狀Service創(chuàng)建了訪問不通kubectl get endpoints顯示none。排查步驟確認Service和Pod在同一namespace。確認selector的labels和Pod的labels完全匹配注意有沒有拼寫錯誤、額外的空格、key和value的格式問題。確認Pod確實處于Running狀態(tài)。容器一直CrashLoopBackOff的話Pod可能被重建IP在變Selector匹配上也沒用。檢查Pod是否被調(diào)度到不可達的節(jié)點上。某些情況下節(jié)點網(wǎng)絡(luò)異常Pod能跑但網(wǎng)絡(luò)不通Endpoints存在但流量到不了。如果以上的沒問題看eventkubectl -n prod describe svc app-svc大部分問題出在第二步我遇到80%的Endpoints為空都是labels寫錯或者namespace不一致。5.2 端口配置錯誤導致的“可以解析但無法連接”癥狀DNS解析能拿到Service名但請求超時或connection refused。這種情況先確認targetPort。我見過有人把targetPort寫成8080但容器真正監(jiān)聽的是5000流量轉(zhuǎn)過去直接被拒絕。用kubectl -n prod get endpoints app-svc -o yaml查看Endpoints的port字段確認和容器實際監(jiān)聽端口一致。還有一個容易忽略的點多容器的Pod。一個Pod里有多個容器時targetPort如果不指定容器名kube-proxy會把流量都轉(zhuǎn)發(fā)到Pod的第一個容器端口如果你期望轉(zhuǎn)到第二個容器比如sidecar容器就得使用命名端口并在Pod定義里明確命名不同容器端口。5.3 NodePort類型端口沖突或不可達癥狀NodePort類型Service創(chuàng)建成功但外部節(jié)點IP端口訪問不了。常見原因有三個節(jié)點的防火墻/安全組沒放行nodePort范圍。這是最常見的需要檢查云安全組或iptables規(guī)則。訪問的不是運行中Pod所在的節(jié)點但NodePort類型是全節(jié)點監(jiān)聽的理論上訪問任意節(jié)點都通。如果只有特定節(jié)點不通檢查那個節(jié)點的kube-proxy是否正常運行。nodePort端口范圍被修改過而客戶端訪問的是默認范圍。確認服務實際分配的端口用kubectl get svc看PORT(S)列最保險。5.4 會話保持失效的分布不均問題癥狀配置了sessionAffinity: ClientIP后流量還是分散到不同的Pod。這個我前面提過如果上游有代理做NAT源IP被隱藏了所有客戶端看起來都是一個IP哈希就全指向同一個Pod這會導致某個Pod負載過高??梢詮娜肟诩覺-Forwarded-For并讓Service層識別但實際更常用的方案是在應用層實現(xiàn)會話保持。5.5 LoadBalancer一直Pending癥狀LoadBalancer類型Service創(chuàng)建后EXTERNAL-IP一直顯示Pending。自建集群沒有云廠商的負載均衡控制器或者云廠商controller-manager異常都會導致這個問題??碿loud-provider組件日志能定位。如果是裸金屬集群可以考慮部署MetalLB來提供load balancer的實現(xiàn)它通過BGP或Layer2模式把NodePort或Pod IP廣播給外部網(wǎng)絡(luò)。5.6 UDP端口超時和丟包面向UDP的Service排查起來比TCP麻煩得多因為UDP沒有握手和連接狀態(tài)丟包了你很難定位。kube-proxy的負載均衡是基于conntrack的UDP同樣會被跟蹤但某些場景下conntrack表項老化過快導致UDP包頻繁被分到不同后端造成服務異常。如果業(yè)務是UDP且對后端一致性有要求建議對UDP Service關(guān)閉會話保持相關(guān)配置或者用headless service讓客戶端直接連后端做應用層處理。5.7 問題速查表癥狀優(yōu)先排查項次要排查項Endpoints為空selector與labels不匹配namespace不一致、Pod未RunningService DNS可以解析但連接超時targetPort是否正確Pod容器端口是否真正監(jiān)聽NodePort外部訪問不通安全組/防火墻放行kube-proxy狀態(tài)、端口范圍LoadBalancer一直Pending云廠商控制器狀態(tài)自建集群缺少LB實現(xiàn)會話保持不生效上游代理是否做NAT超時時間配置UDP服務偶發(fā)失敗conntrack表狀態(tài)后端Pod是否有UDP監(jiān)聽6. 一些寫在最后的實操體會Service看起來簡單但它連接了K8S的多個核心組件API Server存儲聲明狀態(tài)Endpoints控制器維護后端列表kube-proxy把規(guī)則下發(fā)到每個節(jié)點DNS服務提供域名解析。任何一個環(huán)節(jié)出問題表現(xiàn)都是Service不通但定位路徑完全不同。我的個人習慣是任何Service變更都遵循先確認后端Pod再確認selector再確認端口最后看訪問的順序。遇到通一下不通一下的詭異問題優(yōu)先檢查網(wǎng)絡(luò)層kube-proxy日志、節(jié)點網(wǎng)絡(luò)插件狀態(tài)而不是反復盯著YAML文件看。YAML寫錯會立刻暴露網(wǎng)絡(luò)問題才是AM隱性的。最后分享一個小技巧。調(diào)試Service時別只依賴kubectl get svc多看看kubectl get endpoints和kubectl describe svc。Endpoints是后端真實狀態(tài)的照妖鏡它有沒有IP一眼就能看出來selector是否匹配成功。比跑到節(jié)點上抓包快得多。掌握這個習慣后你會發(fā)現(xiàn)Service排查的效率能提高一大截。