
1. 问题背景一次“看起来很玄”的连接超时那天下午线上突然报障说某个内部系统通过浏览器访问一直转圈最后提示连接超时。一开始大家都没太当回事因为这类问题在Docker和Kubernetes环境里太常见了无非就是Service没配好、Pod重启了、Ingress规则写错或者镜像拉取失败导致实例没起来。可等到我实际打开监控面板一看Pod状态全是RunningService的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集群内前面的负载均衡是云平台提供的四层LBLB后端直接指向节点上的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服务正常监听8080LISTEN状态的socket后面显示Recv-Q的值通过socket statistics能看到溢出计数。这里有个关键点内核版本不同ss显示backlog溢出信息的方式不同。传统的ss -lnt不直接显示drop计数需要看/proc/net/netstat里的ListenDrops和ListenOverflowscat /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的默认值在不同发行版、不同内核版本上不一样常见的是13107212.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_max524288同时缩短conntrack的超时时间让早该失效的连接尽快回收sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established86400 sysctl -w net.netfilter.nf_conntrack_tcp_timeout_time_wait60注意默认的nf_conntrack_tcp_timeout_established是43200秒12小时。如果业务是大量短连接这个超时会堆积大量TIME_WAIT状态的conntrack条目。把它缩短到86400秒虽然已经是24小时但对多数业务足够了。实际调整要根据业务连接特征来不能盲目调小否则在线长连接可能被内核提前标记失效。永久生效需要写入/etc/sysctl.conf或/etc/sysctl.d/99-conntrack.confcat /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 out3. 链路分层验证kubectl get svc/endpoints/ingress确认配置层正常4. 检查DNS解析kubectl logs CoreDNS / nslookup解析失败则DNS背锅5. 观察应用日志kubectl logs 业务Pod有请求记录但不回包嫌疑在网络层6. 节点抓包tcpdump Wireshark确认SYN是否到达、是否回SYN-ACK7. 对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协议栈等多个层面的耦合。任何一个环节不看都不会有完整答案。我个人最深的体会是真正难的不是技术本身而是排查问题时的耐心和系统化方法。遇到“无法解释”的问题永远相信底层物理定律——包没有被应用丢弃就是被内核丢弃内核没有丢弃就是在网络上丢了。只要一层层往下挖总能找到那个静默的凶手。希望这份排障笔记能帮你在下次遇到类似问题时少走一些弯路更快地找到真相。