Kubernetes Service 连不上?四步定位 Endpoint 与标签匹配问题
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 集群实测适配主流 CNICalico/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 探针返回非 2xxPod 所在节点 NotReadykubectl get nodes显示NotReadyService 与 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 appbackend # 查看具体 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字段是否为TrueEvents中是否有Readiness probe failed或Failed to pull imageContainers下的State是否为Running而非WaitingIP字段是否有值空 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 证书过期cant 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-modeiptables并确认其配置文件中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 Podkubectl run tmp-shell --rm -i --tty --imagebusybox:1.35 -- sh进入后执行# 1. DNS 解析是否正常 nslookup api-svc.default.svc.cluster.local # 2. ClusterIP 是否可达绕过 DNS直连 IP wget -qO- --timeout2 http://10.96.123.45:80 # 3. 如果失败检查到后端 Pod 的连通性用 Endpoint 列表里的真实 IP wget -qO- --timeout2 http://10.244.1.15:8080 # 替换为实际 Pod IP 和容器端口这个过程暴露三个关键断点nslookup失败 → CoreDNS 问题检查kubectl get pods -n kube-system -l k8s-appkube-dnswgetClusterIP 失败但直连 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 appmy-app,versionv1查不到这个 Podkubectl 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探针会超时默认 timeoutSeconds1判定失败状态码必须严格为 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 --forconditionready pod -l appmy-app --timeout300s它比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必须允许该流量来源。例如你有一个 NetworkPolicyapiVersion: 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 插件有各自的“脾气”排查时必须切换到对应心智模型。FlannelVXLAN 模式核心是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/24CalicoBGP 模式核心是calico-node容器通过 BGP 协议宣告 Pod 网段常见故障BGP 邻居未建立calicoctl node status显示No peers、bird进程崩溃、FELIX_LOGSEVERITYSCREENinfo日志中大量BGP: Failed to connect验证命令# 查看 Calico 节点状态 calicoctl node status # 查看 BGP 邻居 calicoctl get bgppeer # 查看路由表应包含其他节点的 Pod 网段 ip route | grep 10.244CiliumeBPF 模式核心是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 与 Pod5 分钟kubectl get endpoints order-svc -n prod # none kubectl get pods -n prod -l apporder # 0 个结果 kubectl get pods -n prod --show-labels | grep order # 输出order-pod-5c8d9b4a2-xyz12 1/1 Running 0 3h apporder-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 --imagecurlimages/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 列表如何动态增减这种可视化反馈比任何文档都直观。运维的本质是让不可见的状态变得可见。