☰
Kubernetes连接超时排查:conntrack表满导致偶发故障
2026/10/9 12:42:08 网站建设 项目流程

1. 问题背景:一次“看起来很玄”的连接超时

那天下午线上突然报障,说某个内部系统通过浏览器访问一直转圈,最后提示连接超时。一开始大家都没太当回事,因为这类问题在Docker和Kubernetes环境里太常见了,无非就是Service没配好、Pod重启了、Ingress规则写错,或者镜像拉取失败导致实例没起来。可等到我实际打开监控面板一看,Pod状态全是Running,Service的Endpoints也正常,Ingress没有报错,日志也没有明显的5xx,这就有点意思了——一切看起来都正常,偏偏客户端就是连不上。

更有意思的是,这个超时不是百分之百复现,而是间歇性的。同一个接口,刷新几次,前两次能通,第三次就卡住,等个几十秒又自己恢复。这就很折磨人,因为这类“偶发超时”比“持续不可用”要难查得多。你盯着它的时候它好好的,你一转身它又超时了。用户那边反馈是“无法解释的连接超时”,但我心里清楚,系统网络没有那么多玄学,任何一个超时背后一定有链路上的某一跳丢了包、拒了包,或者缓冲区满了。

这篇文章把我的排查过程完整捋了一遍,包括思路、命令、踩过的坑,以及最终定位到的根因。内容偏实战,适合正在用Docker和Kubernetes跑业务、又不太清楚网络链路内部机制的同学参考。如果你是刚接触容器网络,这篇文章也能帮你建立一套排障框架——遇到超时先怀疑哪一层、怎么用工具验证、哪些参数最容易背锅。

2. 先把问题边界搞清楚:从哪里开始查最不浪费时间

2.1 复现现象并区分“不可达”与“超时”的差别

接到报障后,我第一步不是立刻看代码、翻日志,而是先自己复现一遍,并且把“复现方式”固定下来。这里有个很重要的经验:偶发问题的排查,第一步永远不是猜原因,而是先把复现率降下来,至少要能稳定复现,或者把出现条件压缩到一个可操作的范围内。

我写了一个极简的探测脚本,用curl循环请求目标地址,记录每次的返回码和耗时。脚本长这样:

#!/bin/bash for i in $(seq 1 200); do code=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 --max-time 10 http://demo.internal.svc:8080/api/health) time_total=$(curl -s -o /dev/null -w "%{time_total}" --connect-timeout 5 --max-time 10 http://demo.internal.svc:8080/api/health) echo "$(date '+%H:%M:%S') request-$i code=$code time=$time_total" >> /tmp/curl_check.log sleep 1 done

跑了两分钟,统计结果:200个请求里,有11次直接卡到超时,约5.5%的超时率。超时发生的时候,curl报的是“Connection timed out”,而不是“Connection refused”,这两个有本质区别:

  • Connection refused:说明对端收到请求但主动拒绝了,通常是端口没监听、防火墙规则重置、Pod没起来。
  • Connection timed out:说明数据包发出去了,但对端没有任何响应,中间某个环节把包吞了,或者对端处理流程卡死。

这个区别直接决定了排查方向。一开始如果误把“超时”当成“拒绝”去查应用状态,很容易兜圈子。我是直接确认了超时现象和拒绝无关后,才把重心放到网络链路上。

2.2 画出访问链路:从客户端到Pod经历了哪几跳

在Kubernetes里,一个HTTP请求从外部进入到Pod内,至少经过四跳:

客户端 -> 节点/负载均衡器 -> Ingress Controller -> Service -> Pod

每一跳都可能成为超时的元凶。所以在动手之前,我先把整个链路的每一层拆出来,然后逐层验证。

链路画出来之后,我发现这次环境的特殊性在于:Ingress Controller本身以Deployment方式运行在Kubernetes集群内,前面的负载均衡是云平台提供的四层LB,LB后端直接指向节点上的NodePort端口。请求到达节点后由kube-proxy转发到Ingress Pod,再由Ingress转发到业务Service,最终到业务Pod。

每一层就是一个“嫌疑点”。按照经验,排查顺序应从下往上:先确认客户端到节点通不通,再确认节点到Ingress Pod通不通,然后确认Ingress到Service通不通,最后看业务Pod自身处理是否正常。下面就是我实际执行的过程。

2.3 节点层验证:TCP握手能不能完成

我先在客户端所在的机器上做了一次telnet连通性测试,目标地址是LB的IP和端口,目的是确认“客户端到集群入口”这一跳是否正常。结果显示TCP连接能够建立,端口是开放的,但偶尔会出现一次连接建立后立即无响应的情况。这个“偶尔”非常关键——如果TCP握手都能完成但应用层没反应,那问题大概率不在LB,而在更后面的链路。

随后我登录到承载Ingress的节点上,直接用节点IP加NodePort测试,这一跳也没有问题。到这里,已经把客户端到节点的链路基本洗清了。问题范围收窄到了集群内部:要么是Ingress Controller到Service这一段的转发故障,要么是业务Pod接收流量后处理异常。

3. 第一轮排查:Service、DNS、Ingress的常见嫌疑逐个排除

3.1 Service的Endpoints到底指向哪里

既然问题范围收窄到了集群内部,我首先检查业务Service。命令很简单:

kubectl get svc -n demo kubectl get endpoints -n demo

输出结果让我愣了一下:Endpoints里有三个Pod的IP,看起来完全正常。但我注意到其中两个Pod IP对应的节点是某一台机器,第三个Pod IP对应的是另一台机器。这本来也没什么,K8s本来就允许Pod调度到不同节点。可我下意识觉得这可能和超时有关,于是多看了一眼:跨节点的Pod访问,和同节点Pod访问,走的是完全不同的数据通路。

同一节点上的Pod互访,一般通过本机的cni网桥直接转发,不涉及跨节点隧道。跨节点访问,则需要经过overlay网络隧道封装,也就是常说的VXLAN或IPIP这类隧道协议。隧道传输对MTU、内核参数、网络设备都有额外要求,任何一个环节出问题,都可能表现为偶发超时。

我接连做了几组对比测试:从Ingress Pod里curl业务Service的ClusterIP,再从Ingress Pod里直接curl业务Pod的IP。结果很有意思——直接curl Pod IP成功,curl ClusterIP也成功,但两者的成功率有一点差异。由于样本量不够,这个差异当时没有引起足够重视,我犯了一个典型错误:轻信了单次测试的成功结果,没有继续做压测放大问题,结果错过了最接近真相的一条线索。

3.2 DNS解析异常排查:ClusterIP解析时好时坏

第二层嫌疑是DNS。Kubernetes里的服务通过Service名称访问,需要经过集群DNS解析。如果CoreDNS出现波动,或者Pod的resolv.conf配置不对,就会表现为“访问服务时好时坏”。但如果DNS解析本身超时,报错应该是“Could not resolve host”而不是“Connection timed out”。所以直觉上我觉得DNS可能性不大。

不过为了严谨,我还是检查了CoreDNS的Pod状态和日志,然后直接在Ingress Pod里用nslookup解析业务Service的完整域名:

nslookup demo-svc.demo.svc.cluster.local

解析结果正常,返回了ClusterIP。我又连续解析了20次,没有发现解析失败的情况。于是DNS这条线暂时排除,但我保留了“继续关注”的状态,因为有一种场景很隐蔽:CoreDNS解析正常,但conntrack表被占满,导致到CoreDNS的UDP DNS查询包偶发被丢弃,客户端表现就是“DNS查询超时但服务正常”。这个场景在后面居然真的碰到了类似的问题形态,先按下不表。

3.3 Ingress Controller与后端端的连接复用问题

第三层是Ingress Controller。我检查了Ingress规则:

kubectl get ingress -n demo -o yaml

规则里Backend指向的是业务Service的8080端口,看起来也没问题。Ingress Controller的日志里只有一些正常的HTTP访问记录,没有报错。为了深入一层,我通过Ingress访问业务服务,同时观察Ingress Controller的访问日志。

这里出现了一个很值得留意的现象:超时发生时,Ingress访问日志里根本没有对应的请求记录。

这就奇怪了。通常应用层超时,Ingress至少会记录一次请求,哪怕返回5xx,也会在日志里有迹可循。如果日志空白的,说明请求压根没有从Ingress转发到后端,或者后端建立的连接在转发前就已经出了问题。

基于这个现象,我当时的判断是:Ingress这一层基本洗清,问题可能出在Ingress和后端Pod建立连接的过程中。或者是TCP连接池复用了一个已失效的连接,或者是出方向流量被丢。于是我开始往更深一层——网络栈和内核参数——排查。

4. 第二轮深挖:抓包、conntrack、MTU才是真正的“大坑”

4.1 在节点上抓包:SYN发出去了,SYN-ACK没人理

到这里,我决定上抓包工具。这一步可以说是整个排查过程的转折点,因为之前我们一直在看“表象”,只有抓包能看到数据包在哪个节点上消失。

我在业务Pod所在节点上,分别针对几个场景抓包:

tcpdump -i any host 10.244.2.15 and port 8080 -nn -w ingress_to_pod.pcap

同时让测试脚本继续循环发请求。抓到超时的那一瞬间,停掉抓包,然后用Wireshark打开保存的文件,重点看TCP握手过程。

结果非常清晰,也让我精神一振:客户端(Ingress Controller Pod所在节点)发了好几次SYN,目的端口8080,而业务Pod所在的宿主机始终没有回SYN-ACK。TCP握手卡在了第一次重传阶段。数据包有没有送到Pod对应容器的eth0,我没法直接看到——因为容器内的抓包需要进入Pod的network namespace,得用nsenter进入对应命名空间。

我改用另一个方法:在节点上看veth对。每个Pod对应一对虚拟网卡,宿主机侧叫vethxxxx,容器内叫eth0。只要在宿主机侧抓veth的流量,如果SYN到达了veth,说明内核已经把包送达到了Pod的虚拟网卡入口;如果连veth上都没有SYN,说明包在更早的位置,比如路由或iptables阶段就被丢弃了。

抓包结论是:veth入口能看到SYN,但容器侧没有回包。也就是包已经到达了容器网卡门口,但容器内的网络栈没有处理。这一下范围又缩小了一半:问题出现在进入Pod网卡之后、Socket层之前。要么是容器内的socket backlog满了,要么是iptables的INPUT链丢包,要么是nginx/业务应用本身没有accept新连接。

4.2 追查容器内socket状态:backlog队列溢出

进入Pod内查看socket状态:

kubectl exec -it demo-pod-xxxx -n demo -- /bin/bash ss -tlnp

服务正常监听8080,LISTEN状态的socket后面显示Recv-Q的值,通过socket statistics能看到溢出计数。这里有个关键点:内核版本不同,ss显示backlog溢出信息的方式不同。传统的ss -lnt不直接显示drop计数,需要看/proc/net/netstat里的ListenDrops和ListenOverflows:

cat /proc/net/netstat | grep -i listen

果然!ListenDrops数值在快速上涨。这就是问题的一部分:应用进程接受新连接的速度跟不上连接建立速度,导致内核协议栈开始丢弃SYN。为什么应用会突然来不及accept?这个表现和“偶发超时”完全吻合。

但这里又冒出一个矛盾点:业务应用平时压力并不大,QPS很低,怎么会把accept队列打满?我重新看了一眼业务Pod所在节点的整体情况,才发现真正的凶手躲在背后——同一台物理节点上跑了很多高并发的Pod,它们共同争抢节点的文件描述符、进程调度和网络栈资源。当节点整体负载升高时,业务Pod里的进程虽然只是“轻量容器”,但它宿主机上的CPU和内存争抢,直接导致应用进程无法及时accept新连接。

4.3 内核参数背锅:nf_conntrack表项塞满才是真凶

在查看节点内核参数时,我顺手执行了:

sysctl net.netfilter.nf_conntrack_max sysctl net.netfilter.nf_conntrack_count

结果让我眉头一皱:count已经非常接近max了。这台节点的conntrack表几乎被打满。

nf_conntrack是Linux内核的“连接跟踪”模块,负责记录每一个经过协议栈的连接条目。容器网络依赖NAT、iptables、负载均衡,这些全都建立在conntrack之上。一旦conntrack表满,新连接的数据包会被静默丢弃。这正好解释了前面的现象:SYN到达veth入口,但容器内socket层根本收不到——因为网络包在conntrack阶段就被丢掉了,根本没有进入后面的协议栈处理流程。conntrack的丢包不像防火墙REJECT那样会回一个RST,它是完全静默的。

这也是为什么问题“无法解释”:应用层看一切正常,抓包能看到包进来,但就是没有回包。它不像端口冲突那么显眼,也不会打印错误日志,是一种非常隐蔽的丢包形式。

重要提醒:nf_conntrack_max的默认值在不同发行版、不同内核版本上不一样,常见的是131072(12.8万条左右)。看起来很大,但在高并发、多Pod、频繁短连接的容器环境下,几分钟就能占满。排查时不要凭记忆猜默认值,直接执行sysctl net.netfilter.nf_conntrack_max看实际配置。

4.4 为什么之前没发现:日志、监控和直觉的陷阱

到这里,真相已经浮出水面:conntrack表被打满,导致新连接建不起来,应用侧表现为连接超时。

回头看,这个案例里没有哪个环节是真正“玄学”的,但它确实够曲折。有几个原因让问题显得“无法解释”:

第一,conntrack满了之后的表现是偶发性的。表项是动态创建和释放的,短连接请求一多,表项快速上涨,接近上限时新包被丢;等老连接超时释放后,又有余量,新连接又能成功。所以表现为“时通时不通”。

第二,conntrack表满不会在应用日志里留下任何痕迹。业务应用一切正常,Ingress日志也没有报错,监控面板上看起来堆满了“正常”的数据,掩盖了内核级的故障。

第三,直觉陷阱。遇到网络问题第一反应是查Service、查DNS、查Ingress,这些环节确实常见,但这次问题的根源在更底层。如果不是抓包,很难想到去查conntrack表。

5. 修复与验证:调整内核参数后,问题是否真的消失

5.1 临时放通与永久调优

找到根因后,修复反而简单了。先用一条命令临时扩大conntrack上限:

sysctl -w net.netfilter.nf_conntrack_max=524288

同时缩短conntrack的超时时间,让早该失效的连接尽快回收:

sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established=86400 sysctl -w net.netfilter.nf_conntrack_tcp_timeout_time_wait=60

注意:默认的nf_conntrack_tcp_timeout_established是43200秒(12小时)。如果业务是大量短连接,这个超时会堆积大量TIME_WAIT状态的conntrack条目。把它缩短到86400秒虽然已经是24小时,但对多数业务足够了。实际调整要根据业务连接特征来,不能盲目调小,否则在线长连接可能被内核提前标记失效。

永久生效需要写入/etc/sysctl.conf或/etc/sysctl.d/99-conntrack.conf:

cat >> /etc/sysctl.d/99-conntrack.conf <<EOF net.netfilter.nf_conntrack_max = 524288 net.netfilter.nf_conntrack_tcp_timeout_established = 86400 net.netfilter.nf_conntrack_tcp_timeout_time_wait = 60 EOF sysctl --system

还要注意:修改conntrack_max的同时,要关注内存消耗。每个conntrack条目大约占用约300字节内存(视内核版本略有差异),524288条约占用约150-180MB。在小内存节点上,盲目调大容易引发OOM,所以调整要和节点内存规划配合起来。

另外要一并检查的是和conntrack关系紧密的nf_conntrack_buckets。如果哈希表太小,即使max调大了,哈希冲突严重也会影响性能。一般建议buckets约为max的1/4,可以这样调整:

echo 131072 > /sys/module/nf_conntrack/parameters/hashsize

这个参数需要在加载模块时设置,或者通过修改modprobe配置实现。生产环境如果需要调整,建议放到节点初始化脚本或系统镜像里,别等出问题了再临时改。

5.2 业务侧的连接池和超时参数一并优化

内核参数调整只是解决“内核丢包”的问题。要让系统更健壮,业务侧也得配合调整。我这次还做了三件事:

第一,把业务应用的长连接超时时间从默认值调低,避免大量空闲TCP连接长时间占用conntrack表项。很多应用默认的keepalive超时时间非常长,容器环境下这种连接最容易堆积。

第二,给Ingress Controller和后端服务之间启用了连接复用。Nginx/Uvicorn/Tomcat等组件都有keepalive相关配置,开启后同一TCP连接可以承载多个HTTP请求,减少新连接数量,也就减少conntrack表项的波动。

第三,在客户端侧把连接超时时间设置成合理的短值,并且增加重试机制。这样即使遇到偶发丢包,客户端也能快速失败重试,而不是长时间卡住,用户体验会好很多。

修复后,我又跑了一遍之前的200次循环curl测试:超时次数直接归零。观察了24小时,监控面板上的连接成功率稳定在99.99%以上。

5.3 验证时的另一处隐藏发现:MTU过大导致跨节点丢包

在修复过程中,还有一个小插曲。我顺手用ping -M do -s 1472测试了跨节点Pod的通信:

ping -M do -s 1472 10.244.3.5

在部分节点上,大包直接不通。这说明集群的overlay网络对MTU的适配存在问题。如果TCP连接协商的MSS过大,超过链路能承载的MTU,又无法正常分片,就会出现“小包能通、大包超时”的经典问题。这次的主故障虽然和conntrack有关,但如果MTU不修正,后续可能还会出现新的偶发超时。

我检查了各节点Docker0网桥和CNI插件的MTU配置:

ip link show docker0 ip link show flannel.1

发现flannel.1的MTU是1450,而veth对应Pod通信的MTU是1500。隧道接口MTU比物理网卡小是正常的,问题出在部分自定义网络没有同步调整MTU,导致跨网段通信时payload过大。解决办法是把Pod网络的MTU统一改成1450,或者确保物理网络支持更大的帧。这不是本次故障的根因,但属于典型的“隐患型”问题,顺手处理掉是值得的。

6. 从这场排障中沉淀的经验:连接超时的排查路径与工具表

6.1 一套可直接照抄的排查顺序

这次排查我从头到尾经历了三天,中间还走了不少弯路。如果把经验压缩成一套可复用的流程,大概是下面这个顺序:

排查步骤关键命令/工具核心判断依据
1. 压测复现curl循环脚本固定复现方式,记录超时率
2. 区分拒绝与超时telnet / curl报错Connection refused vs timed out
3. 链路分层验证kubectl get svc/endpoints/ingress确认配置层正常
4. 检查DNS解析kubectl logs CoreDNS / nslookup解析失败则DNS背锅
5. 观察应用日志kubectl logs 业务Pod有请求记录但不回包,嫌疑在网络层
6. 节点抓包tcpdump + Wireshark确认SYN是否到达、是否回SYN-ACK
7. 对socket队列ss -tlnp / /proc/net/netstatListenDrops、ListenOverflows上涨
8. 查内核参数sysctl nf_conntrack_count/maxcount接近max就是表满
9. 查MTUping -M do -s 1472大包不通说明MTU不匹配

这套流程不是万能的,但覆盖了绝大多数“连接超时”问题的排查面,足够支撑你在遇到类似问题时不再乱猜。

6.2 给后来者的几条避坑心得

这次排障我踩了不少坑,有几条心得值得写下来:

第一,不要在小样本下轻易下结论。我最初用单次curl成功就判断“Ingress到Service没问题”,结果漏掉了潜在问题。偶发问题必须用循环压测放大频率,样本量至少100次以上,结论才可信。

第二,conntrack是容器网络排障的盲区。很多人调了一辈子iptables,却没意识到conntrack表满导致的静默丢包。这个表的增长速度和节点的并发连接数强相关,容器环境因为NAT和Service转发频繁,conntrack表项消耗速度远超传统物理机。建议每个Kubernetes节点都要把conntrack count纳入监控。

第三,抓包要抓对位置。一开始我在Ingress Controller Pod所在节点上抓包,效果一般。真正的突破是在业务Pod所在节点的veth上抓到SYN有进无出。容器网络的排障,节点上抓包优于容器内抓包,veth入口优于物理网卡入口。

第四,修改内核参数要评估副作用。增大conntrack_max会额外占用内存,缩短timeout会影响长连接。每个调整都要问一句:调整之后,是我的在线连接受影响,还是我主动回收的僵尸连接受影响了?

第五,日志没有报错不代表没有故障。大部分网络层的“静默丢包”都是没有日志的。像conntrack表满、MTU不匹配、socket backlog溢出,这些都不会写入业务日志。监控体系一定要包含内核网络栈的关键指标,否则你会一次次栽在同一个坑里。

6.3 监控补位:让“疑难杂症”提前现形

最后,我把conntrack指标加入了节点监控的告警规则里:

node_netstat_nf_conntrack_count / node_netstat_nf_conntrack_max > 0.8

也建议把ListenDrops和ListenOverflows作为应用Pod的配套指标,如果业务Pod的LISTEN socket出现持续上涨的drop数,说明应用处理连接的能力达到了瓶颈,需要扩容或优化accept逻辑。有了这层监控兜底,以后再出现类似问题,我至少能在告警阶段就抓住线索,而不是等到业务方报障后再从零开始排查。

7. 最后说几句实在话

这次排障让我对容器网络的“黑盒”属性有了更深的敬畏。表面看起来是Docker和Kubernetes环境里的一次偶发连接超时,实际涉及的是Linux内核连接跟踪、pod网络命名空间、节点资源争抢、TCP协议栈等多个层面的耦合。任何一个环节不看,都不会有完整答案。

我个人最深的体会是:真正难的不是技术本身,而是排查问题时的耐心和系统化方法。遇到“无法解释”的问题,永远相信底层物理定律——包没有被应用丢弃,就是被内核丢弃;内核没有丢弃,就是在网络上丢了。只要一层层往下挖,总能找到那个静默的凶手。希望这份排障笔记能帮你在下次遇到类似问题时,少走一些弯路,更快地找到真相。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询