
1. 从一个让人抓狂的神奇妙妙通信小问题说起做后端开发或者运维的朋友大概率都遇到过这种场景服务在本地跑得好好的日志干干净净接口响应飞快结果一上测试环境或者生产环境通信就开始抽风。有时候是连接超时有时候是数据包丢失有时候是请求发出去了但对方死活收不到更离谱的是重启一下服务又好了过几个小时再犯。这种时好时坏、毫无规律的通信故障我管它叫神奇妙妙通信小问题——名字听着可爱实际能把人逼疯。这类问题的核心领域其实非常明确就是网络通信的稳定性排查涉及TCP/IP协议栈、连接池管理、超时重试机制、DNS解析、负载均衡、防火墙策略、MTU分片等多个技术点。它解决的不是某个具体业务逻辑的bug而是服务与服务之间说话时出现的各种意外。适合谁来参考我认为三类人最需要一是刚接手线上服务、对网络排查还没形成系统方法的后端新人二是经常被重启就好了这类问题折磨的运维工程师三是做微服务架构、服务间调用频繁的开发者。哪怕你只写过简单的HTTP请求这篇文章里的排查思路也能帮你少熬几个通宵。我写这篇东西的起因是最近帮一个朋友排查了一个持续两周的通信故障。现象特别典型某个内部服务每隔几小时就会报一批连接超时但监控显示带宽、CPU、内存都正常重启服务后恢复过一阵又复发。最后定位到的原因说出来你可能不信——是连接池里有一批僵尸连接没有被正确回收而底层TCP的keepalive参数又设得太保守。整个过程踩了不少坑也积累了一些通用性很强的排查手法索性整理成一篇完整的经验分享。2. 通信故障的整体排查思路与方案选型2.1 为什么通信问题这么难查通信问题和普通的代码bug有一个本质区别它的触发条件往往不在你的代码里。代码bug你可以断点调试、可以复现但通信问题涉及两端甚至多端的环境中间还隔着操作系统、网卡、交换机、防火墙、负载均衡器。任何一个环节的参数不一致都可能导致神奇妙妙的现象。我总结下来通信故障难查主要有三个原因。第一是偶发性很多问题只在特定时间窗口、特定并发量、特定网络路径下才出现你盯着看的时候它偏偏不犯。第二是信息不对称客户端看到的报错和服务端看到的日志往往对不上客户端说连接超时服务端可能压根没收到请求也可能收到了但回包丢了。第三是默认参数陷阱操作系统和各类中间件都有一堆默认参数这些默认值在开发环境够用到了生产环境的高并发、长连接场景下就成了隐患。2.2 排查方案的整体设计面对这类问题我的整体思路是先分层、再定位、后验证。分层是指把通信链路拆成几个独立的层应用层连接池、超时配置、重试逻辑、传输层TCP参数、keepalive、端口耗尽、网络层DNS、路由、MTU、基础设施层防火墙、负载均衡、NAT。定位是指通过日志、抓包、监控指标把问题缩小到某一层。验证是指改完参数后要有明确的观测手段确认问题真的解决了而不是重启后暂时没犯。为什么选择这个思路而不是上来就抓包因为抓包成本高、数据量大而且很多应用层的问题抓包根本看不出来。先用分层法把范围缩小能省掉大量无效劳动。比如你先看应用日志里报的是connection timeout还是connection reset这两个错误指向的方向完全不同timeout通常是链路不通或对端没响应reset通常是对端主动断开或中间设备拦截。2.3 工具选型的考量排查通信问题常用的工具有几类我按使用频率排个序。第一类是系统自带命令比如ss、netstat、ping、traceroute、telnet这些工具零依赖、随时可用适合快速判断连通性和连接状态。第二类是抓包工具比如tcpdump配合 Wireshark 分析适合深入看协议交互细节。第三类是应用层诊断工具比如curl的详细模式、各类语言的连接池监控。第四类是监控平台看历史趋势和聚合指标。我的建议是日常排查优先用第一类和第三类只有在确认是协议层问题或者需要给网络团队提供证据时才上抓包。因为抓包文件动辄几百MB分析起来很耗时而且生产环境抓包本身也有风险可能影响性能。工具选型的原则是够用就好别为了炫技上重武器。3. 核心细节解析与实操要点3.1 连接池最容易被忽视的重灾区连接池是通信问题的头号嫌疑犯没有之一。我见过太多案例问题根源都在连接池配置上。连接池的核心参数就那么几个最大连接数、最小空闲连接数、连接最大存活时间、空闲连接超时时间、获取连接的超时时间。但就是这几个参数配错了就是灾难。先说最大连接数。很多人觉得设大点没坏处实际上设太大反而危险。假设你的服务有100个实例每个实例连接池最大200那对下游服务的总连接数就是20000。如果下游服务的承载能力只有5000那多出来的连接要么被拒绝要么排队表现出来就是超时。所以最大连接数要结合下游承载能力和实例数量反推公式是单实例最大连接数 下游可承载总连接数 / 实例数 * 安全系数一般取0.7到0.8。再说连接最大存活时间。这个参数决定了连接用多久后被强制回收重建。设得太长连接可能因为中间设备比如防火墙、NAT网关的超时而被悄悄断开但连接池不知道还以为连接可用拿出去用就报错。设得太短频繁重建连接又增加开销。我的经验值是比中间设备的空闲超时时间短一些比如中间设备通常30分钟断空闲连接那连接最大存活时间设20到25分钟比较稳妥。注意连接池的空闲连接超时和连接最大存活时间是两个不同的概念。前者是连接闲着多久被回收后者是连接从创建算起活多久被回收。很多故障是因为只配了前者没配后者导致长期存活但间歇使用的连接被中间设备断掉。3.2 TCP keepalive操作系统的隐藏开关TCP keepalive是操作系统层面的保活机制用来探测连接是否还活着。它有三个核心参数以Linux为例tcp_keepalive_time连接空闲多久后开始探测默认7200秒、tcp_keepalive_intvl探测间隔默认75秒、tcp_keepalive_probes探测次数默认9次。按默认值算一条死连接要经过 7200 75*9 7875秒也就是两个多小时才能被系统发现并关闭。这个默认值在长连接场景下简直是灾难。你的应用以为连接还在实际上对端早就没了请求发出去石沉大海只能等应用层的超时。所以生产环境一般要把这几个参数调小比如tcp_keepalive_time设300秒tcp_keepalive_intvl设30秒tcp_keepalive_probes设3次这样最多 300 30*3 390秒就能发现死连接。但这里有个坑TCP keepalive是操作系统级别的改了对所有应用生效。如果你不能改系统参数那就得在应用层自己做心跳。另外keepalive探测包本身很小但如果探测太频繁在连接数巨大的场景下也会产生可观的额外流量这个要权衡。3.3 超时配置层层设防的艺术超时配置的核心原则是层层递进、内短外长。什么意思假设调用链是 A - B - C那A调用B的超时应该大于B调用C的超时加上B处理业务的时间。否则会出现A已经超时放弃了B还在等C最后B拿到结果发现没人要了白白浪费资源。具体怎么算假设B调用C的超时是1秒B处理业务逻辑平均耗时200毫秒那A调用B的超时至少应该是1.2秒再留点余量设1.5秒。如果A调用B的超时也设1秒那B稍微慢一点A就超时了但实际上B可能马上就能返回结果这就造成了假超时。还有一个容易忽略的点是连接超时和读取超时要分开设。连接超时是建立TCP连接的时间通常很短设1到3秒足够。读取超时是等待对端返回数据的时间要根据业务复杂度设可能几秒到几十秒。很多人图省事只设一个总超时结果要么连接阶段等太久要么读取阶段不够用。3.4 DNS解析那个你以为不会有问题的环节DNS解析出问题的时候表现往往很迷惑。比如服务启动时正常运行一段时间后突然大量超时重启后又好了。这很可能是因为DNS缓存过期后重新解析失败或者解析到了错误的IP。Java应用有个经典的坑JVM会缓存DNS解析结果而且默认是永久缓存除非设置了networkaddress.cache.ttl。这意味着如果你的服务依赖的域名IP变了JVM可能还在用旧IP直到重启。解决办法是在JVM启动参数里设置-Dnetworkaddress.cache.ttl60让缓存60秒过期。另一个坑是DNS解析超时。如果DNS服务器响应慢解析请求会阻塞表现出来就是连接建立慢。这个可以通过在/etc/resolv.conf里配置多个DNS服务器、设置合理的options timeout和options attempts来缓解。4. 实操过程与核心环节实现4.1 第一步快速判断故障类型拿到一个通信故障我第一步永远是看错误信息的具体措辞。不同的错误关键词指向完全不同的方向我整理了一个速查表错误关键词可能原因优先排查方向Connection timed out链路不通、对端未响应、防火墙丢包网络连通性、防火墙规则Connection refused对端端口未监听、服务未启动对端服务状态、端口监听Connection reset by peer对端主动断开、中间设备拦截对端日志、中间设备策略Read timed out对端处理慢、网络延迟高对端性能、网络质量No route to host路由问题、网段不通路由表、网段配置Too many open files文件描述符耗尽系统限制、连接泄漏这个表是我踩了无数坑总结出来的基本上看到错误信息就能把范围缩小一半。比如你看到Connection reset by peer那就别去查防火墙丢包了重点看对端为什么主动断开。4.2 第二步用ss命令看连接状态ss命令是排查连接问题的利器比netstat快得多。我常用的几个组合# 查看所有TCP连接的状态统计 ss -s # 查看特定端口的连接详情 ss -tnp | grep :8080 # 统计各状态的连接数 ss -tan | awk NR1 {print $1} | sort | uniq -c | sort -rn重点看几个指标TIME_WAIT的数量、CLOSE_WAIT的数量、ESTABLISHED的数量。TIME_WAIT多说明连接频繁建立关闭可能是短连接用太多或者连接池没生效。CLOSE_WAIT多说明应用没有正确关闭连接是连接泄漏的典型信号。ESTABLISHED数量如果远超预期说明连接池配置可能有问题。我遇到过一个案例ss -s显示TIME_WAIT有3万多个导致新连接无法建立因为本地端口耗尽。原因是应用用了短连接且QPS很高每个请求都新建连接。解决办法是改用连接池把短连接变成长连接复用。4.3 第三步抓包定位协议层问题如果前两步没找到原因那就得上抓包了。抓包的关键是抓对地方、抓对时机、抓对过滤条件。我一般这样操作# 抓取特定主机和端口的包限制包数量避免文件过大 tcpdump -i eth0 -nn host 192.168.1.100 and port 8080 -c 1000 -w /tmp/capture.pcap # 只看TCP握手和挥手相关的包 tcpdump -i eth0 -nn tcp[tcpflags] (tcp-syn|tcp-fin|tcp-rst) ! 0抓完包用Wireshark分析重点看几个东西三次握手是否完成、是否有RST包、重传次数、窗口大小。如果看到大量重传说明网络质量差或者MTU有问题。如果看到RST看是谁发的对端发RST通常是对端应用崩溃或端口关闭中间设备发RST通常是策略拦截。提示生产环境抓包要控制包数量和文件大小建议加-c限制包数加-w写到文件而不是直接打印。抓包本身对性能有影响尽量在低峰期操作抓完及时停止。4.4 第四步参数调整与验证定位到问题后调整参数要一次只改一个变量改完观察一段时间再改下一个。这样万一问题恶化你能知道是哪个改动导致的。调整后要有明确的验证手段比如改连接池参数后观察ss -s里ESTABLISHED数量的变化改TCP keepalive后观察死连接被发现的时间是否缩短改超时配置后观察超时错误的数量是否下降我一般会观察至少一个完整的业务周期比如一天因为很多通信问题有周期性可能跟业务高峰、定时任务、日志切割等操作相关。只看一两个小时就下结论很容易误判。5. 常见问题与排查技巧实录5.1 那些年我踩过的坑坑一重启大法掩盖了真问题。很多人遇到通信故障第一反应是重启服务重启后确实好了但过几小时又犯。这是因为重启只是清空了连接池和缓存没有解决根本原因。我的建议是重启可以但重启前一定要先抓现场信息比如ss -s的输出、应用日志、当时的连接数。这些信息重启后就没了是定位问题的关键证据。坑二只盯应用日志不看系统日志。应用日志报连接超时但系统日志dmesg里可能有nf_conntrack: table full, dropping packet这样的信息说明是连接跟踪表满了导致丢包。这种问题在应用层怎么调都没用得调系统参数。所以排查时一定要dmesg | tail看一眼。坑三忽略中间设备。应用和服务之间可能隔着负载均衡、防火墙、NAT网关。这些设备都有自己的连接超时和空闲回收策略。我遇到过一个案例应用连接池设的连接存活时间是1小时但中间的负载均衡器30分钟就断空闲连接导致连接池里的连接有一半是死的。解决办法是把连接存活时间调到比负载均衡器的空闲超时短。坑四并发测试环境太干净。开发环境通常没有防火墙、没有负载均衡、网络延迟极低所以通信问题很难复现。我的做法是在测试环境模拟生产环境的网络拓扑至少加上负载均衡和防火墙这样能提前发现大部分问题。5.2 常见问题速查表现象可能原因排查命令解决方向间歇性超时重启恢复连接池僵尸连接ss -tnp看连接状态调小连接存活时间、开TCP keepalive大量CLOSE_WAIT应用未关闭连接ss -tan state close-wait检查代码连接释放逻辑大量TIME_WAIT短连接过多ss -tan state time-wait改用连接池、调内核参数连接建立慢DNS解析慢dig测试解析耗时配DNS缓存、多DNS服务器特定时间超时定时任务或备份影响对比监控时间线错峰执行、限流大包丢失小包正常MTU不匹配ping -M do -s 1472调整MTU或MSS连接数上不去文件描述符限制ulimit -n调大限制、检查泄漏5.3 独家避坑技巧第一个技巧是给连接池加监控。很多连接池都提供了活跃连接数、空闲连接数、等待获取连接的线程数等指标把这些指标接入监控平台设置告警阈值。比如等待获取连接的线程数持续大于0说明连接池不够用了这时候就该扩容或者调大连接数而不是等用户报障。第二个技巧是定期做连接健康检查。对于长连接可以在应用层定期发心跳包发现连接不可用就主动重建。这样比等TCP keepalive发现要快得多。心跳间隔一般设30秒到1分钟太频繁浪费资源太稀疏发现不及时。第三个技巧是用混沌工程主动验证。与其等生产环境出问题不如在测试环境主动制造网络故障比如用tc命令模拟网络延迟和丢包看应用的容错能力。这样能提前发现超时配置不合理、重试逻辑有缺陷等问题。# 模拟100ms网络延迟 tc qdisc add dev eth0 root netem delay 100ms # 模拟10%丢包 tc qdisc add dev eth0 root netem loss 10% # 清除模拟 tc qdisc del dev eth0 root第四个技巧是记录故障时间线。每次通信故障都记录下发生时间、持续时长、影响范围、当时的操作。积累多了你会发现规律比如每次都是整点出问题那可能跟定时任务有关每次都是发布后出问题那可能跟配置变更有关。这种规律靠单次排查是发现不了的。6. 从根上减少通信故障的架构建议排查是治标架构设计才是治本。我在实际项目里总结了几条能显著减少通信故障的设计原则。第一条是尽量用长连接加连接池避免频繁建立连接。短连接在高并发下会产生大量TIME_WAIT耗尽本地端口而且每次握手都有开销。第二条是所有跨服务调用都必须设超时而且超时要分层设置内层短外层长。没有超时的调用就像没有刹车的车一旦对端卡住调用方也会被拖死。第三条是重试要谨慎。重试能提高成功率但也会放大故障。如果对端已经过载你的重试只会让它更过载。我的做法是只对幂等操作重试重试次数不超过2次重试间隔用指数退避而且重试要在超时预算内。比如总超时1秒第一次调用用了600毫秒失败那重试最多只能再等400毫秒。第四条是做好熔断和降级。当某个下游服务的错误率超过阈值时自动熔断不再调用直接走降级逻辑。这样能防止故障扩散给下游恢复的时间。熔断阈值一般设错误率50%、持续10秒以上熔断后每隔一段时间放少量请求探测恢复了就关闭熔断。第五条是监控要覆盖通信的各个环节。不只是监控应用的成功率还要监控连接池状态、TCP连接状态、DNS解析耗时、网络延迟。这些指标能帮你在用户报障之前发现问题。我一般会设几个关键告警连接池等待线程数大于0持续1分钟、CLOSE_WAIT连接数超过阈值、DNS解析耗时超过100毫秒。最后再分享一个我个人特别受用的习惯每次排查完通信故障都写一份简短的复盘记录现象、排查过程、根因、解决办法。这份复盘不用给别人看就是给自己积累经验。我攒了几年下来现在遇到新的通信问题翻翻以前的复盘往往能快速找到相似案例和排查方向。通信问题看着千变万化但底层的原因来来去去就那么几类见得多了就有感觉了。