K8s Service访问不通?四层漏斗排查法快速定位根因
1. 项目概述为什么一个Service访问不了要花两小时才定位到是NodePort端口冲突K8s里Service访问不通是运维和开发每天睁眼就要面对的高频问题。它不像Pod启动失败那样有明确报错也不像Ingress配置错误那样能一眼看出YAML写错了——它更像一根卡在喉咙里的鱼刺你清楚地知道“服务没通”但HTTP请求发出去后curl卡住、浏览器转圈、健康检查持续失败日志里却找不到一句error连kubectl get events都干干净净。这种“静默故障”最消耗人我见过某团队一位资深SRE在凌晨三点反复删重建Service重装CNI插件甚至怀疑是内核版本bug最后发现只是NodePort范围里有个遗留的iptables规则把30080端口偷偷DROP了。这个标题说的不是“如何创建Service”而是当访问已经发生、流量已经发出、结果却失败时你该按什么顺序、查哪些层、看哪些指标、用哪些命令去逼近真相。它覆盖的是真实生产环境里90%以上的Service不可达场景ClusterIP在集群内调不通、NodePort从宿主机curl失败、LoadBalancer外部IP一直pending、Headless Service的DNS解析异常……所有这些背后其实都遵循同一套分层排查逻辑——网络路径、服务发现、策略控制、后端就绪性。关键词“K8s常见问题”“Service不能访问”“排查流程”三个词缺一不可“常见问题”意味着这不是冷门边缘case而是高频、可复现、有模式“Service不能访问”限定了作用域不涉及Pod调度、PV绑定或HPA扩缩容“排查流程”则强调动作序列——不是罗列命令而是告诉你为什么先查DNS再查iptables为什么必须确认Endpoint状态后再动kube-proxy为什么抓包要选在client侧而非server侧。适合谁来读如果你是刚接触K8s两个月的开发正在为本地Minikube里curl不到自己的API发愁如果你是负责交付的实施工程师客户现场一个Service突然失联你得在30分钟内给出根因如果你是平台团队的SRE正想把这套经验沉淀成内部Runbook——这篇文章就是为你写的。它不假设你熟记所有kubelet参数但要求你已能kubectl get pod -n kube-system它不教你etcd原理但会告诉你etcd里Service对象存的是什么字段它不替代官方文档但比文档更贴近你敲下kubectl exec -it那个瞬间的真实困惑。2. 整体排查思路拆解四层漏斗模型从外到内逐级收敛所有Service访问问题本质都是客户端发出的请求在抵达目标Pod之前被某一层拦截、丢弃、转发错误或根本未生成。我们不靠运气试错而用一套经过上百次线上故障验证的“四层漏斗模型”每一层过滤掉一批无关可能性最终将问题收敛到1-2个具体组件。这个模型不是凭空设计而是对K8s网络数据平面的忠实映射——从客户端发起请求开始流量依次经过DNS解析层 → Service代理层kube-proxy/ebpf→ 网络转发层CNI/iptables/ipvs→ 后端就绪层Endpoint/Pod状态。漏斗越往下问题越聚焦操作越精准。2.1 第一层DNS解析是否成功——90%的“访问失败”其实卡在第一步很多人一上来就kubectl exec进Pod里curl http://my-svc看到Connection refused就直奔kube-proxy。但请先问自己你curl的真的是Service的ClusterIP吗还是你写了http://my-svc.default.svc.cluster.local但DNS根本没返回A记录在K8s里“访问Service”默认走的是DNS域名而不是直接IP。如果CoreDNS挂了、或者Service类型是ExternalName但上游DNS不可达、又或者你的Pod用了hostNetwork导致DNS配置异常——那后续所有排查都是无用功。实操中我见过最典型的案例某微服务架构里订单服务调用用户服务代码里写的是http://user-service:8080。测试环境一切正常上线后订单服务日志疯狂报connect timeout。排查两小时后发现用户服务的Service YAML里忘了加selector导致Endpoints为空但CoreDNS依然为user-service生成了A记录指向一个不存在的ClusterIP订单Pod的DNS缓存里存着这个无效IP每次请求都发向黑洞。所以第一层漏斗的核心动作只有一个确认DNS解析结果是否符合预期且该结果是否真实可达。提示不要只依赖nslookup它只查A记录必须用dig short同时查A和SRV记录尤其对Headless Service并验证返回的IP是否在Service的ClusterIP网段内。对于NodePort访问DNS解析甚至不参与——此时第一层应跳过DNS直奔第二层。2.2 第二层Service代理是否生效——kube-proxy的三种模式决定排查路径K8s通过kube-proxy组件实现Service的虚拟IP代理但它有iptables、ipvs、ebpf三种工作模式每种模式的故障表现和排查命令完全不同。很多团队升级K8s后没同步更新排查手册还在用iptables命令查ipvs模式下的规则自然一无所获。iptables模式最经典规则链长易受其他iptables工具干扰。排查重点是iptables-save | grep Service-ClusterIP看是否有KUBE-SERVICES链跳转以及KUBE-SEP-XXXX链是否指向真实Pod IP。ipvs模式性能更好但规则抽象为virtual server和real server。必须用ipvsadm -ln查看-t ClusterIP:Port是否列出且-r PodIP:Port状态是否Active。ebpf模式如Cilium完全绕过iptables规则存在内核BPF map中。bpftool map dump name kubernetes_services才是真相iptables-save输出全是障眼法。这一层的关键判断标准是客户端请求的目标IP:Port是否被kube-proxy成功捕获并重定向到后端Pod。如果没捕获问题在kube-proxy自身配置错误、进程崩溃、权限不足如果捕获了但重定向失败则进入第三层。注意kube-proxy日志里出现cant open /proc/sys/net/ipv4/conf/all/rp_filter警告并不致命但若伴随Failed to sync endpoint则必须处理。rp_filter是反向路径过滤设为2loose mode即可设为0disable反而可能引发安全告警。2.3 第三层网络转发是否通畅——CNI插件与底层网络的隐性冲突即使kube-proxy正确生成了代理规则流量仍可能死在CNI插件或宿主机网络栈。典型场景包括Calico的NetworkPolicy误封了Service流量、Flannel的VXLAN后端IP配置错误导致跨节点通信失败、宿主机防火墙firewalld/ufw默认DROP了NodePort端口、云厂商安全组未放行LoadBalancer的Health Check端口。这里有个极易被忽略的细节NodePort访问失败不等于Service本身有问题。比如你在宿主机上curl 192.168.1.100:30080失败可能原因有1kube-proxy没监听30080检查netstat -tuln | grep 300802宿主机iptables INPUT链DROP了30080iptables -L INPUT -n | grep 300803云服务器安全组禁止了30080入向4甚至是你curl的IP是节点内网IP但该节点没运行任何对应PodNodePort只在运行Pod的节点上生效。所以第三层必须区分访问入口ClusterIP查kube-proxy规则NodePort查宿主机网络栈LoadBalancer查云平台控制台。2.4 第四层后端Pod是否真正就绪——Endpoint与Pod状态的双重校验这是最后一道防线也是最容易被“假象”欺骗的一层。kubectl get pod显示Runningkubectl get endpoints显示有IP不代表Pod真能收包。常见陷阱有1Pod的readinessProbe配置错误始终返回200但业务实际未启动2Service selector匹配了带污点的Pod该Pod被taint驱逐但endpoint未及时清理3Pod所在节点网络断开kubelet无法上报状态endpoint缓存仍有效。因此第四层必须做双重验证先看Endpoint对象是否包含预期Pod IP再进Pod容器内验证该端口是否真实监听并响应。用ss -tuln | grep :8080比netstat更快用curl -v http://localhost:8080/healthz比单纯telnet更可靠——因为很多服务健康检查端口和业务端口分离telnet通不代表业务可用。这四层漏斗不是线性流程而是决策树。比如你查DNS发现解析失败就无需再查kube-proxy如果kube-proxy规则完整但ipvsadm里real server状态是Weight0说明Endpoint为空直接回退到第四层。整个过程像老中医搭脉望看现象、闻查日志、问问配置、切动手验证每一步都有明确输入和输出拒绝模糊地带。3. 核心细节解析与实操要点每个命令背后的原理与避坑指南光知道“要查DNS”不够关键是要理解为什么用dig而不是nslookup为什么检查iptables要过滤KUBE-SERVICES链为什么Endpoint为空时kubectl get endpoints不报错。这些细节决定你能否在5分钟内定位问题而不是在命令行里盲目翻页。3.1 DNS层CoreDNS配置与缓存的隐蔽影响CoreDNS的ConfigMap通常叫coredns里forward . /etc/resolv.conf这行配置决定了它如何处理非集群域名查询。如果/etc/resolv.conf里写的是公司内网DNS而该DNS无法解析cluster.local后缀就会导致Service域名解析超时。更隐蔽的是CoreDNS默认启用缓存插件cacheTTL设为30秒。这意味着你刚修复了Service selectorkubectl get endpoints立刻显示IP但Pod里curl仍失败——因为CoreDNS缓存了旧的空A记录。此时必须强制刷新缓存kubectl exec -n kube-system deploy/coredns -- rndc flush或临时禁用cache插件。另一个高频坑是Pod的DNS策略。dnsPolicy: Default会让Pod继承节点的/etc/resolv.conf如果节点DNS配置错误所有Pod都会受影响dnsPolicy: ClusterFirst才走CoreDNS。但注意hostNetwork: true的Pod会强制使用Default策略哪怕你显式写了ClusterFirst——这是K8s硬编码行为。所以当你发现DaemonSet里的监控Agent无法解析Service时先检查它是否用了hostNetwork。实操心得在Pod里执行cat /etc/resolv.conf第一行nameserver必须是CoreDNS的ClusterIP通常是10.96.0.10且search行必须包含default.svc.cluster.local svc.cluster.local cluster.local。少任何一个DNS搜索路径就不完整。3.2 Service代理层iptables规则链的生成逻辑与调试技巧以iptables模式为例kube-proxy为每个Service生成三条核心规则链KUBE-SERVICES主入口链匹配--dport ServicePort并跳转到KUBE-SVC-XXXXKUBE-SVC-XXXX负载均衡链用-m statistic --mode random --probability 0.x实现概率转发KUBE-SEP-XXXX真实后端链用-d PodIP --dport TargetPort将流量DNAT过去问题常出在第二步KUBE-SVC-XXXX链里没有规则或概率值计算错误。比如你有一个3副本Service但iptables-save | grep KUBE-SVC只看到两条-j KUBE-SEP-XXXX说明有一个Endpoint未同步。此时kubectl get endpoints svc-name -o wide会显示none但kubectl describe svc里不会提示——因为Endpoint对象本身存在只是subsets为空。调试技巧用iptables -t nat -L KUBE-SVC-XXXX -n -v查看详细计数。如果pkts列为0说明流量根本没到达此链问题在上层DNS或客户端目标地址错误如果pkts有增长但bytes为0说明SYN包被发送但没收到ACK问题在后端网络或Pod防火墙。注意iptables-save输出的规则顺序很重要。K8s生成的规则总在KUBE-*链开头但如果有人手动插入了-A INPUT -j DROP它会拦截所有流量包括kube-proxy的规则。所以查iptables前先iptables -t filter -L INPUT -n看INPUT链末尾是否是ACCEPT。3.3 网络转发层CNI插件日志与跨节点通信验证不同CNI插件的排障命令差异极大。以Calico为例calicoctl get workloadendpoints -o wide能列出所有Pod的IP和所在节点而calicoctl get bgppeers可验证BGP邻居是否建立。如果跨节点Pod无法通信先calicoctl node status看各节点状态是否Ready再ip route show table 201Calico专用路由表确认是否有到对端节点的路由。Flannel则依赖/run/flannel/subnet.env里的FLANNEL_SUBNET这个子网必须和kube-controller-manager的--pod-cidr一致。不一致会导致Pod IP分配错误进而使Service无法路由。验证方法kubectl get nodes -o wide看节点InternalIP再ssh到该节点执行cat /run/flannel/subnet.env对比FLANNEL_SUBNET是否属于节点InternalIP所在网段。对于NodePort宿主机防火墙是最大雷区。Ubuntu的ufw默认开启ufw status verbose会显示22/tcp ALLOW IN但30000:32767/tcp DENY IN——这直接封死了全部NodePort。解决方案不是关ufw而是ufw allow 30000:32767/tcp。CentOS的firewalld同理firewall-cmd --permanent --add-port30000-32767/tcp firewall-cmd --reload。3.4 后端就绪层readinessProbe的致命陷阱与Endpoint控制器机制readinessProbe的initialDelaySeconds和periodSeconds设置不当会导致Pod启动后长时间不就绪。比如initialDelaySeconds: 10但应用实际需要30秒加载缓存这20秒内Endpoint控制器不会将Pod加入Endpoint列表。更危险的是failureThreshold: 1只要一次probe失败就标记NotReady而网络抖动很常见。Endpoint控制器的工作原理是监听Service和Pod事件当Service selector匹配到Pod且Pod处于RunningReady状态时将Pod IP加入Endpoint subsets。但“Ready状态”由kubelet上报而kubelet可能因节点压力延迟上报。所以kubectl get endpoints显示IP不代表该IP此刻可达。必须进Pod容器内验证curl -I http://localhost:port/healthz如果返回200再ss -tuln | grep :port确认端口监听。如果ss无输出说明应用根本没bind端口——可能是配置文件写错端口或应用启动脚本有bug。提示用kubectl wait --forconditionready pod -l appmy-app --timeout60s代替kubectl get pod它会阻塞直到所有Pod Ready避免因状态不同步导致误判。4. 实操过程与核心环节实现从一次真实故障还原完整排查链下面以一次真实的线上故障为例完整演示四层漏斗如何落地。故障现象某支付服务的Service名为payment-svc从外部通过LoadBalancer IP访问超时但集群内curl ClusterIP正常。团队最初认为是云厂商LB问题花了40分钟在控制台反复开关LB无果。4.1 第一层DNS解析验证耗时2分钟首先确认访问入口。既然是LoadBalancerDNS解析不参与跳过第一层。但为严谨起见仍执行# 在任意Pod内执行验证LB域名解析 kubectl exec -it some-pod -- nslookup payment-svc.default.svc.cluster.local # 输出Name: payment-svc.default.svc.cluster.local Address: 10.96.123.45 —— 正常既然集群内访问ClusterIP正常说明DNS和kube-proxy基本功能OK问题锁定在外部流量路径。4.2 第二层Service代理与NodePort验证耗时5分钟LoadBalancer底层必然映射到NodePort所以查NodePort端口kubectl get svc payment-svc -o wide # 输出NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE # payment-svc LoadBalancer 10.96.123.45 203.0.113.10 80:30080/TCP 2d # 注意PORT(S)列80:30080/TCP即外部访问30080端口然后登录任一节点非master因master通常不跑Pod检查30080端口是否监听# 在节点上执行 sudo netstat -tuln | grep :30080 # 无输出问题在此——kube-proxy根本没监听30080进一步查kube-proxy日志kubectl logs -n kube-system daemonset/kube-proxy | grep 30080 # 输出E0520 10:23:45.123456 12345 proxier.go:1234] cant open /proc/sys/net/ipv4/conf/all/rp_filter # 这个警告不影响监听继续查 kubectl logs -n kube-system daemonset/kube-proxy | tail -20 # 发现关键错误F0520 10:23:45.678901 12345 server.go:567] failed to run kube-proxy: unable to create proxier: cant set sysctl net/bridge/bridge-nf-call-iptables1: open /proc/sys/net/bridge/bridge-nf-call-iptables: no such file or directory原来节点内核模块br_netfilter未加载modprobe br_netfilter后重启kube-proxy30080端口立即监听。故障解决。4.3 第三层宿主机防火墙与云安全组验证耗时3分钟虽然端口已监听但为彻底闭环仍需验证防火墙# Ubuntu节点 sudo ufw status | grep 30080 # 无输出说明ufw未放行 sudo ufw allow 30080 # CentOS节点 sudo firewall-cmd --query-port30080/tcp # no sudo firewall-cmd --add-port30080/tcp --permanent sudo firewall-cmd --reload同时登录云厂商控制台确认安全组入向规则包含30080/tcp源地址为0.0.0.0/0或指定IP段。4.4 第四层后端Pod就绪性深度验证耗时4分钟尽管问题已解决但按流程仍需验证后端# 查Endpoint kubectl get endpoints payment-svc # 输出NAME ENDPOINTS AGE # payment-svc 10.244.1.10:8080,10.244.2.15:8080 2d # 两个IP均存在 # 进第一个Pod验证 kubectl exec -it payment-pod-abc123 -- sh -c ss -tuln | grep :8080 curl -I http://localhost:8080/healthz # 输出LISTEN 0 128 *:8080 *:* 和 HTTP/1.1 200 OK —— 完美整个过程耗时约14分钟远低于平均故障恢复时间MTTR。关键在于第二层的netstat -tuln是黄金命令它不依赖任何K8s组件直接反映操作系统层面的端口占用状态。很多团队习惯先kubectl describe svc但describe只显示YAML配置不反映运行时状态。5. 常见问题与排查技巧实录那些文档里不会写的血泪教训以下是我在上百次Service故障排查中总结的“反常识”经验它们往往出现在官方文档的空白处却是压垮骆驼的最后一根稻草。5.1 “Connection refused” vs “Timeout”网络层与传输层的根本区别curl: (7) Failed to connect to 10.96.123.45 port 80: Connection refused表示TCP三次握手失败目标端口明确拒绝连接。原因只能是1kube-proxy未监听该端口如上例2后端Pod根本没监听该端口应用未启动3Pod防火墙DROP了SYN包。curl: (28) Connection timed out after 30001 milliseconds表示SYN包发出后未收到SYN-ACK问题在中间网络DNS解析慢、路由不可达、防火墙静默丢包、CNI插件BGP邻居中断。踩过的坑某次故障curl显示timeout团队全力排查网络路由三天后发现是CoreDNS的cache插件TTL设为300秒而Service刚创建DNS缓存了空记录。dig short payment-svc返回空但nslookup因重试机制显示超时——误导了所有人。5.2 Headless Service的DNS解析SRV记录与A记录的分工Headless ServiceclusterIP: None不提供负载均衡而是直接返回Pod IP列表。但它的DNS解析分两种nslookup headless-svc返回A记录所有Pod IPnslookup _http._tcp.headless-svc返回SRV记录含端口、优先级、权重很多gRPC客户端默认用SRV记录做服务发现如果只查A记录会误判DNS失败。验证命令dig short headless-svc.default.svc.cluster.local # A记录 dig short SRV _http._tcp.headless-svc.default.svc.cluster.local # SRV记录SRV记录格式10 100 8080 pod-1.headless-svc.default.svc.cluster.local.其中10是优先级100是权重8080是端口。如果dig SRV返回空检查Service是否设置了publishNotReadyAddresses: true——否则未Ready的Pod不会出现在SRV记录中。5.3 ExternalName Service的“伪失败”上游DNS不可达的伪装ExternalName Servicetype: ExternalName只是DNS CNAME记录不经过kube-proxy。它的故障表现为nslookup external-svc返回上游域名但curl external-svc失败。此时问题100%在上游但新手常误以为是K8s问题。正确排查路径# 在Pod内执行模拟K8s DNS行为 nslookup external-svc.default.svc.cluster.local # 如果返回上游域名如api.example.com继续查上游 nslookup api.example.com # 如果这一步失败问题在CoreDNS无法解析外部域名检查forward配置 # 如果这一步成功但curl失败则是上游服务问题与K8s无关5.4 kube-proxy重启的副作用iptables规则丢失与连接中断kube-proxy重启时会清空所有KUBE-* iptables规则重新生成。这会导致1已有连接中断因为DNAT规则消失2新连接短暂失败规则生成间隙。在高并发场景下这比Service故障更致命。规避方案滚动更新kube-proxy DaemonSet确保至少一个副本在线或使用ipvs模式其规则更新是原子的不会清空全量规则。最后一个小技巧给所有Service加注解service.beta.kubernetes.io/aws-load-balancer-healthcheck-healthy-threshold: 3AWS或service.beta.kubernetes.io/azure-load-balancer-health-probe-request-path: /healthzAzure让云LB健康检查更精准避免因短暂就绪延迟导致LB剔除后端。我在实际使用中发现把netstat -tuln和dig short做成一键脚本放在运维同学的.bashrc里能节省70%的重复劳动。真正的效率提升从来不是学更多命令而是把高频动作固化为肌肉记忆。