ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Kubernetes端口telnet不通:五段链路排查与根因定位

Kubernetes端口telnet不通:五段链路排查与根因定位

下午三点多,联调群里出现一张截图:终端里敲完telnet 10.20.3.15 30080,光标闪了几秒,抛出一行Connection refused。开发同学说服务明明在跑,日志里都有请求;运维同学说节点负载正常、网络没告警。两边各握着一半证据,谁也不服谁。这种对话我经历过太多次,问题往往不在于谁对谁错,而在于双方验证的压根不是同一段链路。

这篇东西就是把这根链路彻底拆开讲。主题词很明确:Kubernetes 里的端口,用 telnet 连不上,怎么一步步定位到真正的断点。它适合三类人看——刚接手容器化项目、遇到"服务在跑但连不上"的开发;负责集群运维、天天被叫去"看下网络"的运维;以及想把这套排查思路固化进发布流程的技术负责人。不论你现在是只会kubectl get pod的新手,还是能背出 iptables 规则链的老手,下面这套拆法都能直接拿去用,因为我不打算只给结论,而是把每一跳的取证动作、判读标准、以及我踩过的坑都摊开写。

1. telnet 不通这件事,先别急着改配置

1.1 telnet 验证的只是三次握手,不是业务可用

很多人把 telnet 当成"服务健康检查",这是第一个认知偏差。telnet 一个端口,成功标志只有一个:TCP 三次握手完成,屏幕上出现Connected to ...或者Escape character is '^]'。它压根不知道对面的应用是不是 HTTP、是不是 gRPC、有没有正常返回数据。反过来,Connection refused这个报错也极其具体——它是收到了对端返回的 RST 包,含义是"这台机器活着,内核也处理了这个包,但没有任何进程在监听这个端口"。而Connection timed out、No route to host、Connection reset by peer是完全不同的故事。

我在实际排查里做过一个粗糙的统计:真正因为"端口没监听"导致的 refused 大概只占三成,剩下七成里,一半是网络策略或转发规则把包丢了(表现为超时而不是 refused),另一半是 telnet 打到的地方本身就不对——比如打的是 NodePort 但externalTrafficPolicy设成了 Local,而目标 Pod 恰好不在这个节点上。所以第一步永远不是翻 YAML,而是先确认这个报错到底属于哪一类,因为它直接决定了你该往哪个方向查。

1.2 三个方向的断点,症状长得很像但根因天差地别

我把 Kubernetes 里"端口连不上"的断点归成三个方向,第一个是流量根本没到达目标节点或 Pod,第二个是到达了但被策略或规则拦掉,第三个是到达了 Pod 但没人监听。这三者的外在表现有时几乎一样,尤其是中间那一层,因为丢包和拒绝在客户端看都是"连不上"。

有个细节特别容易误导人:如果你在节点宿主机上telnet 一个 ClusterIP,走的是本机 kube-proxy 装的 iptables/IPVS 规则,链路里没有物理网络这一段;但如果你在集群外的笔记本上telnet 一个 NodePort,中间隔着交换机、安全组、节点防火墙,链路长了三倍。同样一个 Service,两种测法的结论可能完全相反。所以我习惯在动手前先明确一件事:我现在站在哪里、我要打到哪里、中间隔了几层。这个习惯帮我省掉的返工时间,比任何工具都多。

1.3 先画一张四层链路草图再动手

我几乎每次排查都会先在纸上画一条横线,从左到右标出几个节点:客户端 → 节点网卡 → kube-proxy 规则 → Pod 网卡 → 应用监听。然后在这条线上逐个打点验证,验证顺序是从右往左,也就是从最靠近应用的 Pod 开始。

为什么反着来?因为越靠近应用,信息越确定、成本越低。kubectl exec进容器看一眼监听状态,几秒钟的事;反过来先去抓交换机镜像流量,那是最后手段。从右往左还有一个好处:如果 Pod 内部监听正常,那你就排除了"应用代码没起来"这个最常见也最容易被人推卸责任的选项,后面所有讨论都能建立在"服务是真的在跑"这个共识上。

提示:如果你连 Pod 都进不去,比如容器是 distroless 或者崩溃重启中,那也先别急着抓包,用kubectl debug挂一个临时容器进去看,这个后面第 4 章会详细讲。

2. 把一条访问链路拆成可验证的五段

2.1 第一段:客户端到节点,先确认包有没有出去

这一步最容易被跳过,但坑不少。我遇到过一类情况:开发同学的笔记本在公司办公网,目标节点在隔离的测试网段,两者之间压根不通,telnet 出来是No route to host,但他一直在查 Kubernetes 配置。所以第一个动作应该是用ping或者traceroute(Windows 上用tracert)确认三层可达性,再确认目标端口在防火墙上是否放行。

如果是有安全组的环境,这里还有个隐形陷阱:安全组的入方向规则放行了 NodePort 范围(默认 30000-32767),但如果你在 Service 里把nodePort手动指定成了范围外的端口,它会直接被 API Server 拒绝创建,或者在某些旧版本上创建成功但转发行为异常。另外节点上如果跑了本地防火墙服务,它的规则优先级往往高于 iptables 的 nat 表规则,包在到达 kube-proxy 规则之前就被 REJECT 了。判断方法很简单:在节点上iptables -t nat -nvL KUBE-SERVICES | grep <你的端口>看规则是否存在,再iptables -nvL INPUT | head -30看有没有位置更靠前的拒绝规则。这两个命令的输出顺序,就是包的实际处理顺序。

2.2 第二段:节点上的转发规则,Service 到底有没有生效

确认包到了节点,接下来看 kube-proxy 有没有为这个 Service 生成转发规则。这里要区分两种模式,因为验证手法完全不同。

iptables 模式下,用iptables -t nat -nvL KUBE-SERVICES --line-numbers应该能看到一条目标为 ClusterIP:端口 的规则,类似DNAT tcp -- 0.0.0.0/0 10.96.12.34 tcp dpt:8080 to:10.244.1.7:8080。如果这条规则的目标地址是:<随机端口>或者干脆没有这条链,说明 Service 没被正确解析——大概率是 Endpoints 为空,kube-proxy 就把这类 Service 的规则跳过了。

IPVS 模式下,用ipvsadm -Ln看虚拟服务列表,应该能找到TCP 10.96.12.34:8080这一项,下面挂着 RealServer 列表。如果虚拟服务存在但 RealServer 为空,说明后端端点没注册;如果连虚拟服务都没有,那要么 kube-proxy 没同步,要么这个节点上的 IPVS 模块加载有问题。

我特别想强调一个容易忽略的细节:IPVS 模式下每个 ClusterIP 都会在节点上挂一个虚拟网卡地址(通常叫kube-ipvs0),用ip addr show kube-ipvs0能看到一长串地址。有时候集群规模大、Service 数量多,这张网卡上的地址数量会碰到上限,表现出来就是"部分 Service 突然不通"。这种问题没有报错日志,只能靠观察这张网卡的状态定位,比较阴。

2.3 第三段:Service 到 Pod,端点列表是空还是满

这一段是所有排查里性价比最高的检查点,一条命令就能定性:kubectl get endpointslices -n <命名空间> -l kubernetes.io/service-name=<服务名>。看ENDPOINTS那一列,如果显示<unset>或者空,那问题已经找到了,往下就不用看了。

端点为空的原因主要有三类。第一类是selector 不匹配,Service 的spec.selector里的标签和 Pod 的实际标签对不上,比如 Service 写的是app: web,而 Pod 的标签是app: web-server,一个字符的差别,kubectl get pod --show-labels一眼就能看出来,但很多人就是不看。第二类是Pod 没通过就绪探针,只要 Pod 的 Ready 状态是 False,它就不会被加进端点列表,这就是为什么"Pod 明明在 Running"却连不上——Running 和 Ready 完全是两件事。第三类是命名空间搞错,同名 Service 在多个命名空间里都有,你查的和连的不是同一个。

还有一种更隐蔽的:多端口 Service 里,每个port都必须有name,而且 Pod 端的containerPort名称要和targetPort对得上。一旦名称对不上,端口会被错误映射,端点虽然非空但转到的是错误端口,表现出来就是端点正常、telnet 不通。

2.4 第四段:Pod 内部,监听地址才是真凶

端点正常、规则正常,那就要进容器看了。kubectl exec -it <Pod名> -- ss -lntp(有的精简镜像没有 ss,用netstat -lntp,再不行就直接读/proc/net/tcp)看监听表。这里的关键是看本地地址那一列,而不是端口。

如果你看到的是127.0.0.1:8080而不是0.0.0.0:8080或*:8080,那基本可以收工了——应用只监听在回环地址上,集群里其他任何地方都连不进来,包括同节点的其他 Pod。这种情况在本地开发时完全感觉不到问题,因为你在本机 curl 一下是通的,一上集群就"不通",很容易被误判成集群网络故障。

/proc/net/tcp的长相比较劝退,但我建议记一下:第二列的格式是十六进制IP:十六进制端口,0100007F就是 127.0.0.1,00000000就是 0.0.0.0。用awk 'NR>1 {print $2}' /proc/net/tcp | sort -u能快速看出这台机器上都有哪些监听组合,比装工具快。

2.5 第五段:把五段的验证结果拼成一张判读表

单独看每段信息容易晕,我一般会边查边填一张表,填完根因基本就锁定了。这张表我用了很久,分享出来:

验证点正常表现异常表现对应根因方向
客户端到节点连通ping 通、tracert 无黑洞超时、路由不可达网络链路、安全组
节点 nat 规则存在对应 DNAT 或 IPVS 项规则不存在或 RealServer 为空kube-proxy、端点缺失
端点列表有且地址与 Pod IP 一致<unset>或数量不对selector、就绪探针
Pod 监听地址0.0.0.0/::127.0.0.1应用绑定配置
Pod 监听端口与 targetPort 一致端口不一致端口映射三级关系

这张表的价值不在于"好看",而在于它逼你把"我以为"变成"我验证过"。我见过太多排查绕远路的案例,本质上是因为跳过了一行,然后在后面的行里反复猜测。

3. 六类高频根因的定位与修复

3.1 containerPort、targetPort、port 的三角关系

这是新人最容易绕晕、老手偶尔也会翻车的地方。三个端口各管一段:port是 Service 自己对外暴露的端口,targetPort是流量最终要打到 Pod 的哪个端口,containerPort只是 Pod 模板里的一个声明。关键点是:containerPort纯粹是文档性质的,写错了、不写、写重复,都不影响实际连通性。真正决定转发目的地的是targetPort,而targetPort不写的时候默认等于port,不是默认等于containerPort。

所以有一种经典错误配置:Service 写port: 80,没写targetPort;Pod 里应用监听 8080,containerPort写了 8080。看上去"什么都写了",实际上流量全被转到了 Pod 的 80 端口,而那里没人监听,客户端收到 RST,报Connection refused。修起来很简单,把targetPort: 8080补上就行。

还有一种情况是端口对了但名称错了。多端口 Service 要求每个端口有唯一name,此时targetPort可以用名称引用 Pod 端口的名字。一旦 Service 里的名字和 Pod 里的containerPort.name有一个字对不上,转发就会落到错误端口。我现在的习惯是能用数字就用数字,除非有明确的多端口语义需求,因为数字不会拼错。

3.2 端点为空:Selector 与就绪探针的双重陷阱

上一章讲过怎么查端点,这里讲为什么会出现。除了标签不匹配这种低级错误,最常见的是就绪探针配置不当。探针的initialDelaySeconds设得太短,应用启动要 40 秒、探针 5 秒后就开查,连续失败达到阈值后 Pod 一直处于 NotReady;或者探针的path写错,返回 404,永远不 Ready。这两种情况 Pod 都是 Running 状态,kubectl get pod看起来一切正常,只有READY那一列是0/1,不看就发现不了。

修复方向不是把探针删掉,而是调整参数。启动慢的应用应该用startupProbe单独兜住启动阶段,把failureThreshold和periodSeconds的乘积放大到足够覆盖冷启动时间。我给的经验值是:先用秒表实测三次冷启动耗时,取最大值乘 1.5,作为 startupProbe 的总容忍时间,然后再把livenessProbe和readinessProbe的正常探测接上去。这么做的好处是 liveness 的判定不会在启动期误杀容器,避免那种"启动到一半被重启、重启又到一半又被重启"的死循环。

还有个细节:readinessProbe探测的是 Pod 自身地址,如果你的应用在启动早期只监听 127.0.0.1,探针可能通过而外部访问不通——探针通过不代表对外可达,这个认知要建立起来。

3.3 NetworkPolicy 的白名单模型把流量吃掉

NetworkPolicy 的语义是白名单,这一点很多人理解反了。更麻烦的是它的"一旦被选中就全拒"特性:只要某个 Pod 被任意一条带Ingress段的策略选中,那么除了策略里from明确允许的来源,其他所有入向流量默认全拒。也就是说,你为了限制 A 服务只允许 B 访问,写了一条策略,结果 C、D、E 全被顺手禁掉了。

定位方法比较直接:kubectl get networkpolicy -n <命名空间>看有没有策略,再看kubectl describe networkpolicy <名字>里的PodSelector是否选中了你的 Pod。如果集群用的 CNI 支持策略,还可以在节点上查具体实现了哪些规则。判断"是不是策略拦的"有个小技巧:策略拒绝通常表现为超时而不是 refused,因为包被直接丢弃了,没有 RST 回来。所以看到超时就该想到策略,看到 refused 反而应该先怀疑监听。

修复上我建议两条原则。第一,默认拒绝策略只加在真正需要隔离的命名空间,不要图省事全集群铺开。第二,加策略时同步加一条allow from的显式规则给健康检查和监控来源,否则探针和监控会一起挂掉,排查时又会多一层干扰。

3.4 监听地址绑成回环:最隐蔽的一类

这一类我在前面提过,值得单独展开,因为它太常见又太不容易被发现。表现是:Pod 内部 curl 自己通,同节点其他 Pod curl 不通,kubectl port-forward却能用。最后这个现象特别有迷惑性,因为 port-forward 是从 kubelet 侧直接连到 Pod 网络的,它走的是另一条路径,能通不代表服务对外可用。

根因通常是应用配置里绑定了127.0.0.1,或者绑定的是localhost被解析成了回环。有些框架的默认值就是回环,本地开发图安全,容器化之后就成了坑。修复就是改成0.0.0.0或者::,注意 IPv6 环境下::和0.0.0.0行为不同,双栈集群里建议显式绑::并开启双栈监听。

还有一种衍生情况:应用确实绑了0.0.0.0,但容器里还有一层代理或 sidecar 抢占了同一个端口,导致主应用绑不上、静默失败或者退到其他端口。这种情况看ss -lntp输出的进程名就能分辨,如果监听端口的进程 PID 和主应用对不上,那就是被劫持了。

3.5 kube-proxy 模式差异带来的规则不同步

iptables 和 IPVS 两种模式在超大规模下的表现差异很明显。iptables 模式下规则是线性匹配的,Service 数量上千之后,一次包处理要遍历成千上万条规则,延迟和 CPU 都会上来;IPVS 用哈希表,规模不敏感,但依赖内核模块,模块加载异常时表现为部分节点转发失效。

判断规则是否同步,最直接的办法是对比"期望状态"和"实际状态"。期望状态来自kubectl get svc -o wide里的 ClusterIP,实际状态来自节点上的ipvsadm -Ln或 iptables 链。如果某个 Service 在 API 里存在、在节点规则里缺失,先看 kube-proxy 日志:kubectl logs -n kube-system <kube-proxy-pod> --tail=200,重点找同步失败、连接 API Server 超时这类关键字。

我遇到过一次比较典型的:某个节点上的 kube-proxy 因为和 API Server 的长连接断了,规则停留在几分钟前的快照,新建的 Service 在这台节点上完全不可见,其他节点正常。这种"部分节点异常"的问题最容易被误判成"偶发网络抖动",因为你在出问题的节点上测是不通,换一台就通了。所以排查时一定要在多个节点上分别验证,只测一台的结论不可靠。

3.6 节点防火墙与 conntrack 表打满

节点侧的问题有两个高频点。第一个是本地防火墙服务与容器网络规则的冲突,某些发行版默认启用防火墙并拒绝未知入向,NodePort 的包在到达 nat 规则前就被拦了。判断方法是临时停掉防火墙服务测一次,如果能通就定位了;但生产环境别直接停,正确做法是把 Pod 网段和 Service 网段加入放行列表。

第二个是 conntrack 表满,这个更隐蔽。连接跟踪表有容量上限,高并发短连接场景下很容易打满,打满之后新连接直接被丢弃,表现出来就是"服务好好的,突然大面积连不上,过一会儿又自己好了"。查法很简单:cat /proc/sys/net/netfilter/nf_conntrack_count和nf_conntrack_max对比一下,如果前者逼近后者,基本确诊。修复分两步,短期调大nf_conntrack_max,长期要从应用侧减少短连接,比如加连接池、调长 keepalive。

注意:调 conntrack 上限要同步评估内存占用,每条跟踪表项都占内存,盲目调到几十万在内存紧张的节点上会引发更严重的问题。我一般按"当前峰值的 2 倍"设,并且加上监控告警。

4. 现场取证:用调试容器和抓包把问题钉死

4.1 kubectl debug 挂临时容器,不污染原镜像

排查时最尴尬的情况是目标镜像是 distroless,里面连sh都没有,kubectl exec直接失败。这时候用kubectl debug:

kubectl debug -it <Pod名> -n <命名空间> \ --image=busybox:1.36 \ --target=<容器名> \ -- sh

--target参数是关键,它让临时容器共享目标容器的进程命名空间,这样你在临时容器里ps能看到主应用的进程,ss -lntp能看到主应用的监听,排查才有意义。不加--target的话,你看到的是一个干净的独立命名空间,什么也查不到,很多人第一次用就栽在这。

临时容器有个限制:它不支持端口映射和资源限制,纯粹是调试用,用完即弃。另外它需要有对应权限,如果集群开了 Pod 安全策略,可能会被拒绝,这时候退而求其次,用kubectl run起一个独立的调试 Pod,指定--overrides让它调度到目标节点上。

4.2 在节点上抓包,判断 SYN 有没有进来

抓包是终极手段,但我建议把它放在倒数第二步,因为它的信息量大、判读成本也高。命令本身不复杂:

tcpdump -i any -nn -c 50 'tcp port 30080'

-i any能同时抓到多个网卡的流量,避免漏掉经过网桥的包;-c 50抓够 50 个包就停,防止刷屏。然后在客户端发起 telnet,观察输出。判读逻辑是:

  • 只看到 SYN,没有 SYN-ACK,也没有 RST —— 包被静默丢弃,优先怀疑 NetworkPolicy、防火墙 DROP 规则、路由问题
  • 看到 SYN 和 RST —— 对端明确拒绝,说明包到了但没人监听,重点查 Pod 监听
  • 看到完整的 SYN / SYN-ACK / ACK —— 三次握手成功,网络层没问题,问题在应用层协议或者后续数据交互
  • 连 SYN 都看不到 —— 包压根没到这台节点,回到第 2 章第一段去查链路

这套判读我用了很多年,几乎没失手过。唯一要注意的是容器网络里包会经过多次 NAT,抓到的地址可能是转换后的,别被误导。

4.3 反向验证:从 Pod 里往外打

正向查不通的时候,反向打一次往往有奇效。在目标 Pod 里telnet或者nc -zv一下同命名空间的其他服务、ClusterIP、甚至外网地址,看哪个方向不通。

如果 Pod 能出去但外面进不来,问题在入向;如果 Pod 连 ClusterIP 都不通,那可能是这个 Pod 所在的节点上 kube-proxy 规则有问题,或者 Pod 的网络命名空间配置异常。如果 Pod 连外网都不通,那要看 DNS 和出向 NAT,跟端口本身没关系了。

反向验证还有一个用途:确认应用是不是真的能接受连接。有些应用监听正常但对特定来源做了 IP 白名单,这时候从 Pod 内部打自己能通,从外部打就不通,排查方向完全不同。

4.4 一次完整的取证实录

说个我印象比较深的例子。某次测试环境报告某个服务 NodePort 连不上,A 同学查了半天 YAML 没发现问题。我按流程走了一遍:先kubectl get endpointslices看到端点是两个地址,正常;ss -lntp进容器看监听,0.0.0.0:8080,正常;节点上iptables -t nat -nvL KUBE-SERVICES有对应规则,正常。三条都正常,那就只剩网络了。

抓包一看,只有 SYN 没有回包。这时候我想起 Service 配置里externalTrafficPolicy: Local,而客户端打的那台节点上恰好没有这个服务的 Pod。Local 模式下,NodePort 只在本节点有后端时才转发,否则直接丢弃,且不返回任何错误。换一台有 Pod 的节点测,立刻通了。整个过程从抓包到定位不到十分钟,但如果一开始就扎进 YAML 里翻,可能要耗一下午。

这个案例我的体会是:排查的效率取决于你有没有一条固定顺序的验证路径,而不是取决于你有多熟悉某个组件。路径对了,问题自己会浮出来。

5. 把这类问题挡在发布之前

5.1 探针配置的三条硬规矩

前面讲过探针导致端点为空的问题,这里给三条我一直在用的规矩。第一,启动慢的服务必须配 startupProbe,用它的failureThreshold × periodSeconds覆盖冷启动,别让 liveness 在启动期误杀。第二,readinessProbe 的探测路径要探真实依赖,比如数据库连接是否就绪,而不是简单返回 200,否则 Pod Ready 了但请求全 500,等于把问题推给了上游。第三,livenessProbe 不要探外部依赖,数据库挂了不该导致所有 Pod 被重启,那是雪崩。

这三条背后的逻辑是一样的:三种探针职责不同,混用会把局部故障放大成全局故障。我在实际项目里见过 liveness 探数据库导致整个服务集体重启的案例,恢复花了不少时间。

5.2 Service 与工作负载的自检清单

发布前花两分钟过一遍这几个点,能挡掉大部分低级问题:

检查项期望值常见错误
Service selector与 Pod labels 完全一致拼写差异、多余空格
targetPort与 Pod 实际监听端口一致依赖默认值、写成 containerPort
多端口 name每个端口唯一且有名字缺 name 导致校验失败
端点数量与副本数一致探针未过、镜像启动失败
网络策略显式放行监控与探针来源只写拒绝没写允许

这份清单我贴在团队 wiki 上很久了,新同学照着走一遍,问"为什么连不上"的次数明显下降。

5.3 一条命令做链路巡检

手工逐段查太慢,我写了个粗糙但好用的巡检脚本思路:给定命名空间、Service 名、端口,依次输出端点数量、Pod Ready 状态、Pod 监听地址,最后在一台 Pod 里nc -zv目标 ClusterIP。四个输出并排看,异常项一眼就能挑出来。核心就是把前面那张判读表自动化,不追求多优雅,能省时间就行。

关键是要把巡检嵌进发布流水线,作为发布后的验证步骤,而不是等用户报障才去查。这一步做了之后,"上线之后连不上"这类问题的平均恢复时间能压到很低。

5.4 最后聊几句实际体会

这套排查方法我用下来最大的感受是:大部分"网络问题"最后都不是网络问题。真正涉及交换机、路由、丢包的占比很低,绝大多数是配置对不上、探针没过、端点为空、监听绑错地址这几类。它们共同的特点是"看起来一切正常,就是连不上",因为 Kubernetes 把很多信息分散在 API 对象里,不主动去查就永远不知道。

我现在的习惯是遇到 telnet 不通,先不急着手,先在纸上默写那条链路,然后从最右边开始打点。这个动作熟练之后,五分钟能走完,比临时翻文档快得多。另外提醒一句,kubectl port-forward能通千万别当成服务正常的证据,它走的是 kubelet 那条路径,跟 Service 转发完全是两回事,这个坑我踩过,也见过不少人踩。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进