1. 为什么刚配好的 Service 总是“连不上”——这不是网络问题,而是认知断层
K8s 里最让人抓狂的瞬间之一,就是明明 Pod 跑得好好的,kubectl get pods全是 Running,日志里也清清楚楚写着 “Server started on :8080”,可一 curl Service 的 ClusterIP,直接 timeout;换用 NodePort 访问宿主机 IP,还是 connection refused;甚至把kubectl port-forward拉起来本地调试,结果又通了——这时候你盯着终端发呆:到底哪一层在“装死”?
这不是玄学,也不是集群坏了,而是 Service 这个抽象层背后横跨了四层独立子系统:Pod 网络可达性、Endpoint 动态绑定、kube-proxy 规则加载、以及客户端访问路径选择。绝大多数人排查时只盯着“能不能 ping 通”或“端口开没开”,却忽略了 Service 本质不是“端口转发器”,而是一个服务发现+流量分发+状态同步三位一体的协调机制。它不保证“只要 Pod 启动就自动接入”,而是严格依赖控制器间的数据一致性。
我带过的几个团队里,新人平均要踩三次同类坑才建立完整链路意识:第一次以为是防火墙,关掉 iptables 就跑;第二次怀疑 DNS,反复nslookup kubernetes.default.svc.cluster.local;第三次才意识到kubectl get endpoints my-service居然返回空——而此时 Pod 标签早被自己手抖改过两次。这说明问题不在“技术能力”,而在对 K8s 控制平面数据流的理解存在断点。
本文不讲原理图、不列 API 字段定义,而是按真实排障现场还原一条从现象反推根因的完整证据链。你会看到:如何用三条命令锁定问题域;为什么describe service的 Events 字段常被忽略却最关键;Endpoint 列表为空时,到底是 Pod 没就绪,还是 selector 写错,抑或命名空间隔离导致的“隐形失联”;当 kube-proxy 处于 IPVS 模式时,ipvsadm -ln输出里哪几行才是真正有效的健康检查信号。所有操作均基于 v1.24+ 集群实测,适配主流 CNI(Calico/Flannel/Cilium),不依赖任何第三方插件。
提示:本文所有命令默认在控制平面节点或具备
cluster-admin权限的客户端执行。若使用受限账号,请确保有get services,get endpoints,get pods,get nodes权限。
2. 四步定位法:用最小成本圈定故障层级
别急着翻文档、别立刻重装 kube-proxy、更不要删了重来。K8s 排障的核心纪律是:先确认事实,再质疑假设。一个无法访问的 Service,必须按固定顺序验证四个关键事实,跳过任意一步都可能把 5 分钟能解决的问题拖成 2 小时。
2.1 第一步:确认 Service 自身定义是否“自洽”
很多人把 YAML 往集群一扔就认为配置完成,但 K8s 不会校验语义合理性。比如:
apiVersion: v1 kind: Service metadata: name: api-svc spec: selector: app: backend # ← 注意这里写的是 backend ports: - port: 80 targetPort: http # ← 这里引用的是端口名而对应的 Pod 定义却是:
apiVersion: v1 kind: Pod metadata: labels: app: backend-v2 # ← 标签值多了一个 -v2! spec: containers: - name: server ports: - name: web # ← 端口名是 web,不是 http containerPort: 8080这种错位不会报错,但会导致 Service 始终找不到匹配的 Pod。验证方法只有一条命令:
kubectl get service api-svc -o wide重点看三列:
CLUSTER-IP:如果是None,说明是 Headless Service,本就不该被 ClusterIP 访问;SELECTOR:输出值必须与目标 Pod 的labels完全一致(包括大小写、连字符);AGE:如果 AGE 是几秒内新建的,需等待 Endpoint Controller 同步周期(默认 30 秒),不要立即下结论。
紧接着执行:
kubectl describe service api-svc这才是关键。在 Events 区域,你会看到类似这样的记录:
Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal CreatingLoadBalancer 5m service-controller Creating load balancer Warning NoEndpoints 2m endpoints-controller No matching endpoints for this service注意这个NoEndpoints事件——它比任何日志都早出现,且明确指向 Endpoint Controller 的判断结果。只要看到这条 Warning,问题 100% 出在 Pod 侧(标签、就绪探针、命名空间),无需往下查网络层。
注意:
describe输出中的Endpoints字段显示的是当前绑定的后端地址列表。如果为空(<none>),但SELECTOR正确,说明 Pod 存在但未通过就绪检查;如果SELECTOR本身为空或格式错误,则 Service 定义就有硬伤。
2.2 第二步:验证 Endpoint 是否真实生成
Service 的流量分发完全依赖 Endpoint 对象。它由endpoints-controller维护,是 Service 和 Pod 之间的“中间账本”。即使 Pod 运行正常,Endpoint 也可能因以下原因缺失:
- Pod 处于
Pending或ContainerCreating状态(未真正就绪); - Pod 的
readinessProbe失败(如 HTTP 探针返回非 2xx); - Pod 所在节点 NotReady(
kubectl get nodes显示NotReady); - Service 与 Pod 不在同一命名空间(最隐蔽的坑:
kubectl get svc -n defaultvskubectl get pods -n prod)。
执行:
kubectl get endpoints api-svc -n default如果返回No resources found,说明 Endpoint 对象根本没创建。此时必须查endpoints-controller日志(通常在 kube-controller-manager 容器中),但更高效的做法是直接查 Pod 状态:
# 查看所有匹配 selector 的 Pod(忽略命名空间) kubectl get pods --all-namespaces -l app=backend # 查看具体 Pod 的详细状态和事件 kubectl get pods -n default backend-pod-7f8d9c4b5-xv6kz -o wide kubectl describe pod -n default backend-pod-7f8d9c4b5-xv6kz在describe pod输出中,重点关注:
Conditions下的Ready字段是否为True;Events中是否有Readiness probe failed或Failed to pull image;Containers下的State是否为Running,而非Waiting;IP字段是否有值(空 IP 表示 CNI 插件未分配网络)。
我曾遇到一次案例:Pod 状态显示Running,但IP为空,describe里 Events 显示Failed to allocate network: no available IPs in range。根源是 Flannel 的 subnet.env 配置错误,导致某个节点的 Pod CIDR 耗尽。这种问题kubectl get endpoints直接为空,但kubectl get pods却显示正常——这就是为什么必须交叉验证。
2.3 第三步:确认 kube-proxy 规则是否生效
Endpoint 有了,不代表流量就能转过去。kube-proxy 是 Service 的“规则翻译官”,它把 Service 抽象转化为底层网络规则(iptables 或 IPVS)。如果它挂了、没启动、或规则加载失败,ClusterIP 就是纯摆设。
先确认 kube-proxy 运行状态:
kubectl get pods -n kube-system | grep kube-proxy # 应看到类似:kube-proxy-abc12 1/1 Running ...再进入任一工作节点(非 master),检查其 kube-proxy 容器日志:
# 在节点上执行(非 kubectl exec) sudo journalctl -u kube-proxy --since "5 minutes ago" | grep -i "error\|failed\|sync"常见错误:
Failed to retrieve node info: nodes "xxx" not found→ 节点未注册进集群,或 kubelet 证书过期;can't determine kind of data for { }→ kube-proxy 配置文件 YAML 格式错误;SyncLoop (ADD, ...)后无后续日志 → 可能卡在 watch 事件处理。
更直接的验证是检查规则是否存在。以 iptables 模式为例:
# 查看是否包含对应 Service 的链 sudo iptables-save | grep -A 5 "KUBE-SERVICES" | grep "api-svc" # 查看 Service 对应的具体规则(假设 ClusterIP 是 10.96.123.45) sudo iptables-save | grep -A 10 "10.96.123.45"你会看到类似:
-A KUBE-SERVICES ! -s 10.244.0.0/16 -d 10.96.123.45/32 -p tcp -m comment --comment "default/api-svc: cluster IP" -m tcp --dport 80 -j KUBE-MARK-MASQ -A KUBE-SERVICES -d 10.96.123.45/32 -p tcp -m comment --comment "default/api-svc: cluster IP" -m tcp --dport 80 -j KUBE-SVC-XXXXXX如果完全搜不到api-svc,说明 kube-proxy 未加载该 Service 规则。此时检查 kube-proxy 启动参数是否包含--proxy-mode=iptables,并确认其配置文件中mode字段正确。
对于 IPVS 模式,用:
sudo ipvsadm -ln | grep "10.96.123.45:80"正常输出应包含TCP 10.96.123.45:80 rr及后端 Real Server 地址(即 Pod IP)。如果只有 Virtual Server 没有 Real Server,说明 Endpoint 数据未同步到 IPVS,大概率是 kube-proxy 与 apiserver 连接异常或权限不足。
2.4 第四步:模拟客户端路径,逐层穿透验证
前面三步都是“静态检查”,第四步必须“动态验证”。不能只在 master 上curl,因为 master 通常不运行 kube-proxy(除非启用了 hostNetwork),它走的是 localhost 直连 apiserver 的捷径,绕过了 Service 流量路径。
正确做法是:在工作节点上,用 Pod 内部视角发起请求。创建一个临时 busybox Pod:
kubectl run tmp-shell --rm -i --tty --image=busybox:1.35 -- sh进入后执行:
# 1. DNS 解析是否正常 nslookup api-svc.default.svc.cluster.local # 2. ClusterIP 是否可达(绕过 DNS,直连 IP) wget -qO- --timeout=2 http://10.96.123.45:80 # 3. 如果失败,检查到后端 Pod 的连通性(用 Endpoint 列表里的真实 IP) wget -qO- --timeout=2 http://10.244.1.15:8080 # 替换为实际 Pod IP 和容器端口这个过程暴露三个关键断点:
nslookup失败 → CoreDNS 问题(检查kubectl get pods -n kube-system -l k8s-app=kube-dns);wgetClusterIP 失败但直连 Pod IP 成功 → kube-proxy 规则或网络策略拦截;- 直连 Pod IP 也失败 → Pod 自身服务未监听、防火墙阻断、或容器端口未暴露(
containerPort未声明)。
我曾在一个 Cilium 集群中遇到:nslookup正常,wgetClusterIP 超时,但直连 Pod IP 成功。最终发现是 CiliumNetworkPolicy 默认拒绝了 ClusterIP 流量,需显式添加toEntities: [all]规则。这种策略类问题,永远无法通过kubectl get发现,必须靠真实路径验证。
3. 标签与就绪探针:两个看似简单却高频致命的配置陷阱
90% 的 Service 不可达问题,根源不在网络或 kube-proxy,而在于 Service 与 Pod 之间那层脆弱的“契约”——标签选择器(selector)和就绪探针(readinessProbe)。它们不是可选项,而是 Service 流量准入的强制闸门。理解它们的触发逻辑,比背熟所有 kubectl 命令更重要。
3.1 Selector 的精确匹配机制:大小写、连字符、空格全是雷区
K8s 的 selector 匹配是完全字符串匹配,不支持正则、不忽略空格、不自动转换大小写。一个常见的反模式是:
# Service selector selector: app: my-app version: v1 # Pod labels labels: app: my-app version: "v1 " # ← 末尾多了一个空格!kubectl get pods -l "app=my-app,version=v1"查不到这个 Pod,kubectl get endpoints也为空。但kubectl get pods --show-labels输出的 label 字符串里,那个空格肉眼几乎不可见,尤其在终端里用less查看时。
更隐蔽的是环境变量注入导致的标签污染。比如用 Helm 模板:
labels: app: {{ .Chart.Name }} env: {{ .Values.environment | upper }} # ← upper() 后变成 PROD,但 Service selector 写的是 prod解决方案不是靠肉眼检查,而是用kubectl get的-L参数直接投影标签值:
# 查看所有 Pod 的 app 和 version 标签值,一行一个,清晰可见空格和大小写 kubectl get pods -n default -L app,version --no-headers | awk '{print "app=" $NF ", version=" $(NF-1)}' # 或者用 jsonpath 精确提取 kubectl get pods -n default -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.labels.app}{"\t"}{.metadata.labels.version}{"\n"}{end}'另一个高频问题是标签继承丢失。Deployment 创建的 Pod,其 labels 默认继承自 Deployment 的.spec.selector.matchLabels,但如果你手动给 Pod 添加了额外 label,或修改了 Deployment 的 template spec 但没更新 selector,就会导致新旧 Pod 标签不一致。此时kubectl rollout restart deploy/my-app并不能修复,必须先kubectl patch deploy my-app -p '{"spec":{"template":{"metadata":{"labels":{"revision":"2"}}}}}'强制滚动,再同步更新 Service selector。
3.2 ReadinessProbe 的“假就绪”陷阱:HTTP 探针的响应体与状态码博弈
就绪探针失败,Pod 会被从 Endpoint 列表中移除。但很多开发者以为“只要返回 200 就算就绪”,却忽略了 HTTP 探针的两个隐藏规则:
- 响应体长度限制:kubelet 默认只读取前 10KB 响应体。如果应用启动后返回一个 50MB 的欢迎页 HTML,探针会超时(默认 timeoutSeconds=1),判定失败;
- 状态码必须严格为 2xx:返回 302 重定向、401 未授权、甚至 204 No Content,都会被判定为失败(204 是合法的 2xx,但某些旧版 kubelet 有 bug)。
更危险的是“伪就绪”场景:应用进程已启动,监听了端口,但内部缓存未加载、数据库连接池未初始化、配置中心未拉取完成。此时 HTTP 探针返回 200,但实际业务请求 100% 失败。Service 流量涌入,雪崩开始。
正确的就绪探针设计必须反映业务就绪,而非进程存活。例如:
readinessProbe: httpGet: path: /health/ready port: 8080 initialDelaySeconds: 30 periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 3其中/health/ready接口必须:
- 检查数据库连接
ping; - 检查 Redis 连接
INFO命令; - 检查本地缓存加载状态(如
if !cache.IsLoaded() { return 503 }); - 不查询外部依赖(如调用第三方 API),避免探针自身成为瓶颈。
我见过最典型的“伪就绪”案例:一个微服务启动时加载 2GB 机器学习模型,耗时 4 分钟。开发人员设置了initialDelaySeconds: 240,但探针路径是/,返回静态 HTML。结果是:Pod 状态变Running后立即加入 Endpoint,前 4 分钟所有请求都打到“空壳”上,5xx 错误率 100%。修复方案是:将探针路径改为/health/ready,并在模型加载完成后才返回 200。
提示:用
kubectl wait命令可自动化等待就绪:kubectl wait --for=condition=ready pod -l app=my-app --timeout=300s它比
sleep 300更可靠,因为直接监听 Pod 的 Ready Condition。
4. 网络策略与 CNI 插件:当“通”与“不通”的边界变得模糊
当上述所有检查都通过,Service 依然无法访问时,问题大概率进入了网络策略(NetworkPolicy)或 CNI 插件的领域。这里没有银弹,只有针对性的排除路径。CNI 插件(Calico/Flannel/Cilium)决定了 Pod 网络的底层实现,而 NetworkPolicy 是其上的访问控制层。两者叠加,让连通性问题呈现出“有时通、有时不通”“部分 Pod 通、部分不通”的诡异特征。
4.1 NetworkPolicy 的默认拒绝原则:看不见的墙
K8s 的 NetworkPolicy 遵循默认允许,显式拒绝(Default Allow)原则,但这是指 Pod 间的通信。而 Service 的 ClusterIP 访问,本质上是通过 kube-proxy 的 DNAT 转发到 Pod IP,因此受 NetworkPolicy 的ingress规则约束——但仅当流量到达 Pod 所在节点时才生效。
一个经典误区是:以为 NetworkPolicy 只控制 Pod 到 Pod 的直连,却忽略了 Service 流量经过 kube-proxy 后,目的地已是 Pod IP,此时 NetworkPolicy 的ingress.from必须允许该流量来源。
例如,你有一个 NetworkPolicy:
apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: deny-all spec: podSelector: {} policyTypes: - Ingress它拒绝所有入向流量。此时即使 Service 和 Endpoint 都正常,ClusterIP 访问也会失败,因为 kube-proxy 转发来的流量(源 IP 是节点 IP 或其他 Pod IP)被拦截。
验证方法:临时删除 NetworkPolicy,看 Service 是否恢复:
kubectl delete networkpolicy deny-all -n default # 等待 10 秒,再测试 curl如果恢复,说明 NetworkPolicy 是元凶。此时需添加白名单:
ingress: - from: - namespaceSelector: matchLabels: network-access: "true" # 允许特定命名空间 - podSelector: matchLabels: app: ingress-controller # 允许 Ingress Controller对于 Calico 用户,还需检查GlobalNetworkPolicy和HostEndpoint,它们作用于节点级别,可能拦截 kube-proxy 的流量。
4.2 CNI 插件特有问题:Flannel 的 VXLAN 端口与 Calico 的 BGP 邻居
不同 CNI 插件有各自的“脾气”,排查时必须切换到对应心智模型。
Flannel(VXLAN 模式):
- 核心是
flanneld进程在每个节点上维护cni0网桥和flannel.1VXLAN 设备; - 常见故障:
flanneld未启动、/run/flannel/subnet.env文件损坏、VXLAN 端口8472被防火墙阻止; - 验证命令:
# 检查 flanneld 进程 ps aux | grep flanneld # 检查 VXLAN 设备状态 ip link show flannel.1 # 检查节点间 VXLAN 通信(在节点 A 上 ping 节点 B 的 flannel.1 IP) ping 10.244.1.0 # 假设节点 B 的 flannel 网段是 10.244.1.0/24
Calico(BGP 模式):
- 核心是
calico-node容器,通过 BGP 协议宣告 Pod 网段; - 常见故障:BGP 邻居未建立(
calicoctl node status显示No peers)、bird进程崩溃、FELIX_LOGSEVERITYSCREEN=info日志中大量BGP: Failed to connect; - 验证命令:
# 查看 Calico 节点状态 calicoctl node status # 查看 BGP 邻居 calicoctl get bgppeer # 查看路由表(应包含其他节点的 Pod 网段) ip route | grep "10.244"
Cilium(eBPF 模式):
- 核心是
cilium-agent,用 eBPF 程序替代 iptables; - 常见故障:eBPF 程序加载失败(
kubectl -n kube-system logs ds/cilium | grep "failed to load")、cilium-health服务异常、cilium status显示KubeProxyReplacement: Strict但 kube-proxy 未停用; - 验证命令:
# 查看 Cilium 状态 cilium status # 查看健康检查 cilium-health status # 查看 eBPF map(应包含 Service 和 Endpoint 信息) cilium bpf lb list
一个真实案例:某集群升级 Calico 后 Service 不通。calicoctl node status显示所有节点BGP state = established,但ip route里没有其他节点的 Pod 网段。最终发现是升级后calicoctl版本不匹配,calicoctl get nodes返回空,而kubectl get nodes正常。强制指定--config指向正确配置后修复。这提醒我们:CNI 工具链版本必须与集群版本严格对齐。
5. 实战复盘:一次从超时到秒通的完整排障纪实
理论终需落地。下面还原一次我在某电商项目中处理的典型 Service 故障,全程耗时 18 分钟,步骤完全公开,无任何“上帝视角”跳跃。
现象:
用户反馈订单服务order-svc无法访问。curl http://order-svc:8080/health返回curl: (7) Failed to connect to order-svc port 8080: Connection refused。
Step 1:快速扫描 Service 自身(2 分钟)
kubectl get svc order-svc -n prod # 输出:NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE # order-svc ClusterIP 10.96.201.88 <none> 8080/TCP 3h kubectl describe svc order-svc -n prod | tail -10 # Events: # Type Reason Age From Message # ---- ------ ---- ---- ------- # Warning NoEndpoints 3h (x12 over 3h) endpoints-controller No matching endpoints for this service→NoEndpoints事件明确指向 Endpoint 问题,无需查网络。
Step 2:聚焦 Endpoint 与 Pod(5 分钟)
kubectl get endpoints order-svc -n prod # <none> kubectl get pods -n prod -l app=order # 0 个结果! kubectl get pods -n prod --show-labels | grep order # 输出:order-pod-5c8d9b4a2-xyz12 1/1 Running 0 3h app=order-service # 注意:label 是 order-service,不是 order→ Service selectorapp: order与 Pod labelapp: order-service不匹配。
Step 3:修正并验证(3 分钟)
# 临时 patch Service selector(生产环境应走 GitOps 流程) kubectl patch svc order-svc -n prod -p '{"spec":{"selector":{"app":"order-service"}}}' # 等待 30 秒,检查 Endpoint kubectl get endpoints order-svc -n prod # 输出:NAME ENDPOINTS AGE # order-svc 10.244.2.15:8080,10.244.2.16:8080 10sStep 4:终极验证(3 分钟)
# 启动临时 Pod 测试 kubectl run test-curl --rm -i --tty --image=curlimages/curl -- sh # 在容器内: curl -v http://order-svc.prod.svc.cluster.local:8080/health # 返回:{"status":"UP","checks":[]}Step 5:根因归档(5 分钟)
- 问题根源:Helm Chart 的
values.yaml中service.selector.app被错误覆盖为order,而deployment.labels.app保持order-service; - 修复方案:统一 Chart 中
app值为order-service,并添加 CI 检查kubectl diff验证 selector 与 labels 一致性; - 预防措施:在 CI 流程中增加
kubectl get endpoints -n $NS $SVC | grep -q "<none>" && exit 1断言。
这次排障没有重启任何组件,没有修改网络配置,纯粹靠四步定位法和对标签匹配机制的深刻理解。它印证了一个事实:K8s 的复杂性不在分布式系统本身,而在其声明式模型与人类直觉之间的鸿沟。填平它,靠的不是更多命令,而是更精准的提问——“K8s 认为这个 Service 应该绑定哪些 Pod?它为什么认为没有?”。
最后分享一个小技巧:把
kubectl get endpoints -w和kubectl get pods -w两个命令放在分屏里同时运行。当你修改 Service selector 或 Pod labels 时,能实时看到 Endpoint 列表如何动态增减,这种可视化反馈比任何文档都直观。运维的本质,是让不可见的状态变得可见。