ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用netstat读懂TCP状态机:从黑盒调参到精准定位

用netstat读懂TCP状态机:从黑盒调参到精准定位 做服务端开发的人八成都有过这种经历线上接口突然开始超时整体吞吐往下掉你打开终端一通操作sysctl把backlog调大keepalive时间改短重试次数加一加然后重启服务碰运气。运气好了恢复正常运气不好问题照旧甚至更糟。我干了这么多年网络相关的活儿对这种“黑盒调参”深恶痛绝。真正的问题几乎从来不在参数大小上而是你对 TCP 连接的当前状态一无所知。netstat就是那个让你睁开眼的工具而Socket 源码则能告诉你每一行调用背后TCP 状态机到底切换到了哪一步。这篇就把我从“瞎调参数”到“直接定位状态机卡点”的完整思路和实操过程展开说说适合被连接问题折磨过的后端开发者、对网络原理停留在八股文阶段但想真正上手排查的人以及所有写 Socket 代码却不清楚connect()之后发生了什么的朋友。1. 为什么你看到的连接数调参总是像在碰运气1.1 黑盒调参的典型困境先描述一个我遇到过的真实场景。某个内部 RPC 服务单机连接数稳定在两千左右某天突然出现大量connect timeout下游服务方说他们什么都没改我们这边netstat一看连接数还是两千但ESTABLISHED状态占比明显下降SYN_SENT和CLOSE_WAIT冒出来一堆。当时有同事的第一反应是“把/proc/sys/net/ipv4/tcp_syn_retries调大一点”但我追问了一句调大这个参数解决的是什么问题他答不上来只知道网上说这个参数管重试。这就是典型的黑盒调参你看到的是一堆可调的 knob但你不清楚它们各自对应 TCP 状态机的哪个环节。tcp_syn_retries管的是SYN_SENT 状态下的重发次数如果你的问题根本不在于 SYN 发出去没人回而是对端已经回包但本地因为半连接队列满而丢弃那你调这个参数一点用处没有。状态机不知道改参数就是在掷骰子。还有更常见的TIME_WAIT问题。很多人一看到netstat里有大量TIME_WAIT就开始慌各种搜索“减少 TIME_WAIT 的优化”把tcp_tw_reuse、tcp_tw_recycle全打开。事实上 Linux 4.12 之后tcp_tw_recycle已经被移除不少老文章里的操作根本就是对着空气调参。TIME_WAIT这个状态是 TCP 状态机里主动关闭方必须停留的终点站目的是防止旧连接的延迟报文串到新连接里。大量TIME_WAIT在服务器主动关闭连接的高频短连接场景下是正常的你要做的是开启SO_REUSEADDR让新连接能复用这些端口而不是试图消灭状态机里的合法状态。1.2 把黑盒变白盒的完整路线图我的破局思路其实很简单任何一次连接异常都能在状态机的某个状态上找到对应的停留点先把停留点找到再谈调参。就像汽车仪表盘告诉你发动机故障灯亮了你不可能先想着换轮胎你得知道是哪个气缸缺火。TCP 状态机就是那台发动机的仪表盘而netstat就是读取仪表盘的工具。要真正“拒绝黑盒”你需要一条完整路线第一步用netstat -anp看清楚当前所有连接处于什么状态统计分布第二步结合对端行为的描述判断这个状态意味着本地协议栈处于哪个阶段第三步从 Socket 源码级别的调用链去复现和验证为什么状态会停在这里第四步针对这个具体状态去找对应内核参数而不是乱调。最后这步往往最轻松因为当你定位到状态以后对应的参数几乎是白送的。我实际做排查的时候一半时间花在“搞清楚现在连接在哪个状态”上剩下的一半时间都在用源码和抓包确认“为什么停在这个状态”。2. netstat 实战用一分钟读懂当前所有 TCP 连接状态2.1 三条最常用的命令组合很多人用netstat就只会一个netstat -an然后被满屏的输出吓住。我平时固定用三组命令按需取用。第一组是总览状态分布快速锁定异常状态的量级netstat -tan | awk /^tcp/ {print $6} | sort | uniq -c | sort -rn这条命令把netstat -tan输出里的第六列也就是 State 列做统计排序。执行完你会看到类似这样的结果1500 ESTABLISHED 300 TIME_WAIT 20 CLOSE_WAIT 3 SYN_RCVD一眼就能看出 CLOSE_WAIT 有 20 个这就是疑点。没有这条统计命令时你靠肉眼在上万条连接里数状态效率太低。第二组是查具体连接的细节定位到进程和端口netstat -anp | grep 8080-p显示 PID 和进程名这是定位“哪个进程占用了端口”“哪个进程在大量建连”的关键。没有 root 权限时-p可能显示不了别的进程的信息但在排查自己服务的端口时通常够用。如果你要盯着某个连接的变化还可以配合watch命令让它每秒刷新。第三组是看协议级别的统计信息netstat -s这组输出包含 TCP 层各种计数器的累计值比如active connection openings、passive connection openings、failed connection attempts、connection resets received等。这些计数器不会直接显示当前状态但它们记录了协议栈历史上发生过的异常事件。比如你看到SYNs to LISTEN sockets dropped说明有 SYN 因为 accept 队列满而丢失这比你在状态列表里找SYN_RCVD更直接地揭示了问题根源。新版系统里netstat可能不是默认安装的通常ss是它的替代品参数类似。但netstat作为经典工具老系统、嵌入式环境里仍然随处可见而且输出格式稳定我建议两个都掌握这篇文章以netstat为主线。2.2 输出字段逐列拆解netstat -anp的输出长这样Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp 1 0 127.0.0.1:8080 127.0.0.1:54321 ESTABLISHED 12345/java tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 6789/sshd tcp 0 0 192.168.1.10:54321 192.168.1.20:3306 SYN_SENT 23456/python逐列拆解。Proto是协议类型TCP 或 UDP。Recv-Q和Send-Q在不同状态下含义不同这是最容易被误解的一列。在ESTABLISHED状态下Recv-Q表示 socket 接收缓冲区里等待应用读取的字节数Send-Q表示发送缓冲区里还没被对端确认的字节数。如果Recv-Q持续大于 0 且不减少说明应用层没有及时read()程序堵住了如果Send-Q持续增长说明对端窗口为零或者网络拥塞数据发不出去。在LISTEN状态下两个值的含义完全不同Recv-Q表示当前 accept 队列里已完成三次握手、等待accept()的连接数量Send-Q表示 backlog 参数设置的最大排队连接数。我见过不少人把 LISTEN 状态下的 Recv-Q 当成缓冲区大小来排查方向完全跑偏。Local Address和Foreign Address分别是本地和远端的 IP 加端口。State就是当前连接所处的 TCP 状态LISTEN、SYN_SENT、ESTABLISHED、CLOSE_WAIT、TIME_WAIT这些字符串就是状态机的对外映射。PID/Program name是持有这个 socket 的进程没有-p参数时这一列是空的。2.3 状态分布怎么看一眼锁定异常点状态分布统计出来以后怎么判断哪些是异常ESTABLISHED是正常工作的连接占比应该最高。LISTEN是端口在监听一个端口只会有一行。TIME_WAIT和CLOSE_WAIT是两个最容易被误判的状态。前者是主动关闭方在等待 2MSL 超时通常是正常的量越大说明连接关闭越频繁尤其是短连接场景后者是收到对端 FIN 后自己还没调用close()如果大量堆积说明应用层代码的关闭逻辑有 bugsocket 泄漏了这是真正需要警惕的。SYN_SENT表示连接请求发出去了但还没收到对端 SYN-ACK。大量SYN_SENT要么是对端不可达要么是对端的半连接队列全满无法响应新连接。SYN_RCVD表示收到对端 SYN 并回复了 SYN-ACK但三次握手还没完成。如果 SYN_RCVD 大量堆积本地服务大概率是 accept 队列满或者对端不响应最后的 ACK。FIN_WAIT_1和FIN_WAIT_2出现在主动关闭方发出 FIN 之后正常会很快消失如果长时间停留可能是对端一直不回复或半关闭状态没处理干净。LAST_ACK是被动关闭方发出 FIN 后等待对端 ACK。我把这些状态的排查优先级列成一个表格方便你对照状态正常性常见原因优先排查动作TIME_WAIT 堆积通常正常主动关闭太频繁开启 SO_REUSEADDRCLOSE_WAIT 堆积异常应用未调用 close()检查连接关闭代码SYN_RCVD 堆积异常accept 队列满调大 backlog加快 acceptSYN_SENT 堆积异常对端不可达或过载检查网络连通性和对端负载FIN_WAIT_1/2 停留可能异常对端不响应 FIN抓包确认对端行为3. TCP 状态机全链路拆解三次握手与四次挥手的完整轨迹3.1 三次握手中两端到底经历了哪些状态TCP 状态机听起来玄乎其实用一句话概括连接双方各自用一组状态来记录自己对当前连接进展的认知每次收发报文都会触发状态变化。三次握手就是这段旅程的开头。客户端视角调用connect()后内核发一个 SYN 报文本地状态从CLOSED进入SYN_SENT意思是“我发了连接请求在等回应”。如果对端回了一个 SYNACK客户端内核会回复一个 ACK同时将状态变为ESTABLISHED此时connect()调用成功返回应用拿到一个可读写的 socket。整个过程中客户端只经历了CLOSED → SYN_SENT → ESTABLISHED三个状态简洁明了。服务端视角稍微复杂一点。服务端在LISTEN状态上等 SYN。内核收到 SYN 后先判断半连接队列还有没有空间有空间就创建出一个 socket状态置为SYN_RCVD回复 SYNACK然后把连接放进半连接队列。注意这时应用层的accept()还没有被调用连接也还没完全建立。当服务端收到客户端回复的那个 ACK三次握手完成内核把这个连接从半连接队列移动到 accept 队列状态变为ESTABLISHED。这时accept()才能从队列里取到这条连接返回给应用。所以服务端的完整路径是LISTEN → SYN_RCVD → ESTABLISHED。这里有个特别关键的认知accept()返回的连接一定是已经完成三次握手的。netstat 里 LISTEN 状态的 Recv-Q 如果大于 0意味着 accept 队列里有等待应用取走的已完成连接内核协议栈上这个连接的 TCP 状态其实已经是 ESTABLISHED 了。很多人在排查时看到 LISTEN 下 Recv-Q 大就以为握手没完成实际上问题出在应用层accept()不及时这是完全不同的两回事。3.2 四次挥手从 FIN_WAIT_1 到 TIME_WAIT 的每一站拆除连接比建立连接复杂得多状态数量也更多。四次挥手的双方各有两个状态要经历再加一个双方都要经过的公共状态。以客户端主动关闭为例。应用调用close()内核发送 FIN 报文客户端状态进入FIN_WAIT_1意思是“我发了终止请求在等对方的 ACK”。服务端收到 FIN 后TCP 协议栈回复一个 ACK同时状态进入CLOSE_WAIT这意味着“我知道你要关了但我这边应用层还没有调用 close 关闭我的写方向”。客户端收到这个 ACK状态从FIN_WAIT_1进入FIN_WAIT_2。关键点来了FIN_WAIT_2是个危险状态因为服务端什么时候发送它的 FIN 完全取决于服务端应用层什么时候调用close()。如果服务端有连接泄漏一直不 close客户端就会永远停留在FIN_WAIT_2。有些人看到大量FIN_WAIT_2以为正常但如果是长连接场景这通常说明对端没有正常完成关闭流程。服务端应用最终调用了close()内核发出 FIN服务端状态进入LAST_ACK。客户端收到这个 FIN先回复 ACK然后状态进入TIME_WAIT服务端收到 ACK状态从LAST_ACK进入CLOSED。此时服务端这边的连接彻底结束而客户端还要在TIME_WAIT状态里停留 2MSLLinux 上通常为 60 秒等待网络中可能残留的延迟报文彻底消失然后才进入CLOSED。主动关闭方ESTABLISHED → FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT → CLOSED。被动关闭方ESTABLISHED → CLOSE_WAIT → LAST_ACK → CLOSED。双方各自四步中间还穿插着 ACK 报文的触发网上的八股文背得再熟不如自己抓一次挥手过程。3.3 状态转换写在哪个函数里内核态的状态机用户态看到的netstat状态本质上是内核 socket 结构体里的一个枚举字段。Linux 内核在include/net/tcp_states.h里定义了 TCP 的所有状态而状态迁移的源码核心是tcp_set_state()这个函数它负责改变连接状态并触发对应的事件回调。比如建立连接时tcp_v4_connect()中会调用tcp_set_state(sk, TCP_SYN_SENT)三次握手完成时tcp_rcv_state_process()里根据收到的报文类型决定是否调用tcp_finish_connect()把状态从TCP_SYN_SENT改成TCP_ESTABLISHED。这些细节不需要你逐行读内核代码但你至少要形成一种意识每一个状态变化都对应内核源码里一行明确的状态赋值语句。我们平时调的那些sysctl参数很多就是影响这些函数里某个分支判断的阈值。理解了这层关系你调参的时候就知道自己在调什么改net.ipv4.tcp_syn_retries改的是tcp_connect()里的重传计数改net.core.somaxconn改的是inet_listen()里 accept 队列的长度上限。参数只是状态机运行过程中的一些调节旋钮而不是魔法开关。4. 用 Socket 源码验证状态机从 connect() 到 close() 的调用链条4.1 Python socket.connect() 一行代码背后的状态之旅用户态写 Socket 的代码每一行看似简单背后都是状态机上的一次长途旅行。拿 Python 举例我们最常写的socket.connect()最终会调用 glibc 的connect()系统调用内核进入tcp_v4_connect()这个函数第一步就是tcp_set_state(sk, TCP_SYN_SENT)然后发出 SYN 报文之后进程就阻塞在内核等待状态变为TCP_ESTABLISHED。如果对端一直不回应SYN 会重传状态停留在SYN_SENT等到超时上限connect()抛出一个Connection timed out异常。你看自己写过的代码import socket s socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(3) try: s.connect((192.168.1.100, 8080)) except socket.timeout: print(connect timeout)这个timeout异常的本质是内核在SYN_SENT状态停留超过了你的超时设置。排查这种问题最直观的办法就是先 ping 一下对端确认网络通不通然后netstat -anp | grep 192.168.1.100:8080看有没有SYN_SENT残留再确认对端进程是否在监听、监听队列是否满了。再进一步如果listen()的 backlog 设置得比系统的somaxconn还大内核会静默地只取两者中较小的值。很多人在应用层设了listen(1024)以为队列就是 1024其实系统somaxconn默认是 4096通常没问题但如果你在容器环境里看到listen的Send-Q显示为 128而完全不是自己设置的值八成是容器里的预设参数把它限制住了。netstat -tan里LISTEN状态的Send-Q列会直接告诉你内核实际使用的 backlog 值这个信息值得你每次部署完服务都看一眼。4.2 listen/accept 的队列与 SYN_RCVD 的秘密服务端代码里最常见的三行是bind()、listen()、accept()但很少有人真正意识到listen()执行的瞬间内核只是在 socket 上设置了监听状态和队列参数并没有开始接受任何连接。直到accept()被调用应用才真正从 accept 队列里取连接。我之前排查过一个高频新建连接的服务现象是客户端偶发连接超时服务端 CPU 不高连接数也不高但netstat -tan里LISTEN状态的Recv-Q偶尔跳到几十而Send-Q是 128。这说明 accept 队列短暂满了新的连接完成三次握手后无法进入队列内核只能丢弃后续的 SYN对端就会经历SYN_SENT → 超时重传 → 最终失败。这种问题的根源不是backlog小而是accept()取连接的速率跟不上建连速率。代码里一般会改成while True循环里反复accept()或者用select、epoll处理多路复用。如果你看到LISTEN的Recv-Q长期不为 0第一反应应该是检查应用是不是在同步阻塞式accept的处理循环里卡了太久而不是急着调大 backlog。内核里对应的是tcp_v4_syn_recv_sock()到tcp_child_process()这串调用链收到 ACK 之后把请求 socket 从半连接队列迁移到 accept 队列。半连接队列满了以后新 SYN 会被直接丢弃这在netstat -s里有对应计数SYNs to LISTEN sockets dropped。这个数字如果持续增长基本可以确定半连接队列打满了。半连接队列的大小由tcp_max_syn_backlog和somaxconn共同决定取较小值默认情况下两者都是 4096 量级一般情况下不会轻易打满打满了说明对端在猛发 SYN或者发生了 SYN 攻击。4.3 代码里的边界条件非阻塞 connect 与连接超时如果只是写同步阻塞式 Socket状态机的变化对我们的感知是黑盒化的connect()要么成功要么异常中间过程对开发完全透明。但一旦用非阻塞模式开发者就必须亲自面对状态机这也更能体现“白盒”的意义。用 Python 写一个非阻塞连接的例子import socket import time s socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setblocking(False) try: s.connect((192.168.1.100, 8080)) except BlockingIOError: # 这里不是失败connect 正在进行 pass # 手动等待连接完成轮询 SO_ERROR time.sleep(1) err s.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) if err 0: print(connected) else: print(connect failed, errno:, err)connect()返回BlockingIOError不代表失败它只是告诉你内核已经进入SYN_SENT正在等待完成。之后你得用select/poll/epoll去监听这个 socket 的可写事件然后通过getsockopt(SO_ERROR)拿到连接结果。这个模式下状态机的SYN_SENT → ESTABLISHED迁移完全由开发者控制你可以在等待过程中设置自己的超时计时器而不是依赖内核默认的 127 秒超时。如果你用非阻塞模式但忘了检查SO_ERROR就会出现一种奇怪的现象连接明明已经建立了但你还在傻傻等待白白消耗资源。这其实是状态机和用户态逻辑脱节导致的问题用netstat看会发现那一堆连接早就ESTABLISHED了。5. 实操构造场景复现 TIME_WAIT、SYN_RCVD 堆积与端口复用5.1 场景一主动关闭连接观察 TIME_WAIT 产生与回收理论讲再多不如动手复现。我先说怎么观察TIME_WAIT。随便写一个客户端脚本主动连接一个本地服务然后立刻主动关闭连接循环多次import socket import time for i in range(100): s socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((127.0.0.1, 8080)) s.close() time.sleep(0.1)执行完以后在另一个终端跑netstat -tan | grep 8080 | head -20你会看到大量127.0.0.1源地址的TIME_WAIT连接本地端口号各不相同对端是服务端的 8080 端口。这些连接会存在大约 60 秒。这里有个细节TIME_WAIT刷新的 2MSL 是相对连接关闭时间计的所以只要持续有新连接关闭老连接会在 60 秒后逐步消失。你观察到的稳定数量通常等于 60 秒内的关闭连接数。如果你想验证端口复用的影响可以在脚本里不设置SO_REUSEADDR让本地端口固定在一个客户端端口。比如强制bind((127.0.0.1, 40000))以后再去连接服务端第一次连接关闭后立刻再用同样端口发起第二次连接你会收到一个OSError: [Errno 98] Address already in use。这是因为 40000 端口上还停留着前一个连接的TIME_WAIT状态内核默认不允许新连接复用。解决代码就是s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)允许新连接绑定处于TIME_WAIT状态的端口。出现address already in use的时候用netstat -anp | grep 40000能看到为TIME_WAIT。这种场景在线上极其常见尤其是 Java 的 NIO 客户端快速重连时报错信息经常就是那个读者输入里提到的“failed to create server shutdown socket on address [localhost] and port...”本质上就是同一个状态机卡点。5.2 场景二用 Socket 源码复现“地址已在使用”这个场景可以连起来做。本地写一个简单服务端import socket srv socket.socket(socket.AF_INET, socket.SOCK_STREAM) srv.bind((127.0.0.1, 8080)) srv.listen(128) while True: conn, addr srv.accept() # 收到连接后不处理等客户端自己关闭 conn.recv(1024) conn.send(bhello) conn.close()然后客户端快速重连不设置SO_REUSEADDRimport socket import time for i in range(200): c socket.socket(socket.AF_INET, socket.SOCK_STREAM) c.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) c.bind((127.0.0.1, 40000)) c.connect((127.0.0.1, 8080)) c.send(bping) c.recv(100) c.close() time.sleep(0.1)这里如果你去掉SO_REUSEADDR那一行第二次循环大概率就会碰到Address already in use。原因就是客户端主动关闭后本地端口 40000 上残留了TIME_WAIT状态内核默认拒绝再次绑定。打开SO_REUSEADDR以后即使TIME_WAIT存在内核也允许新 socket 绑定同一端口但这不等于端口就能无限快速复用仅限于绑定阶段。每次成功重连后连接关闭TIME_WAIT又会重新累计。这个过程中netstat -anp | grep 40000能看到TIME_WAIT和ESTABLISHED交替出现。从这里你能理解为什么很多高性能网络库会建议客户端使用随机端口而不是固定端口固定端口重连必然撞上TIME_WAIT随机端口虽然仍会产生TIME_WAIT但不会阻塞新连接。5.3 场景三SYN_RCVD 堆积到底意味着什么SYN_RCVD堆积比CLOSE_WAIT更隐蔽因为它往往不直接表现为业务报错而是表现为莫名的延迟和连接失败。用一个简单的方式模拟服务端listen(1)也就是 accept 队列只有 1 个名额然后客户端疯狂发连接请求内核三次握手完成后把连接塞进 accept 队列队列满后续连接只能停在半完成队列状态为SYN_RCVD如果半连接队列也满新 SYN 直接被丢。在这种场景下执行netstat -tan | grep 8080一定可以看到若干SYN_RCVD而且对端的端口各不相同。它们的共同点是服务端已经发出了 SYN-ACK但最后那个 ACK 或者没收到或者收到了但 accept 队列已经满没法迁移。线上遇到SYN_RCVD堆积且数量持续不降我的建议是第一看netstat -s里有没有SYNs to LISTEN sockets dropped如果有说明 accept 队列或者半连接队列满了第二看服务端进程 CPU 和线程状态是不是accept()循环被阻塞或根本没有启动第三如果不是自己的服务那就要考虑对端是不是在伪装大量 SYN 请求。不要一上来就改somaxconn大多数情况下是应用层accept()不够快而不是队列配置小。有一次我排查类似的场景发现一个坑服务端用的是多线程模型每个连接开一个线程去处理但线程池没设上限连接一多线程疯狂创建内存飙升GC 卡顿accept()循环被拖慢Recv-Q 不断堆积。这时候调任何 backlog 参数都没用真正的问题是线程模型不适合高并发短连接。状态机只是表现根源在应用层的并发策略。6. 常见问题排查与速查手册6.1 排查流程模板把前面所有内容浓缩成一套流程我每次排查 TCP 连接问题都按这个顺序走基本不会漏。第一步查看状态分布。执行netstat -tan | awk /^tcp/ {print $6} | sort | uniq -c | sort -rn看有没有异常状态大量存在。第二步锁定具体连接。对异常状态执行netstat -anp | grep 异常状态或按端口过滤找到对应的 PID 和进程。第三步结合对端和代码行为判断状态卡点原因。第四步查看netstat -s里的协议计数确认有没有丢包、超时、重置等累计事件。第五步只有在确认是协议栈参数导致的卡点时才去改内核参数并且一次只改一个改完继续观察状态分布而不是凭感觉一次性调一堆。举一个实际例子。我遇到过ESTABLISHED连接大量存在但Recv-Q持续增长的情况现象是客户端叫超时服务端netstat -anp | grep 8080显示很多ESTABLISHED但 Recv-Q 从 100 涨到 10 万不下降。这时候状态机本身没问题握手都完成了卡点完全在应用层读数据不够快。我去查应用日志发现消费线程阻塞在 MySQL 查询上SQL 里有全表扫描把数据库打满了消息积压socket 读缓冲越堆越大。那次排查经验让我明白状态机正常不代表应用健康Recv-Q和Send-Q这两个指标是连接层到应用层之间唯一的信息通道必须重视。6.2 状态问题对照速查表以下是我这些年总结出来的对照表大部分连接问题都可以套进去现象排查命令可能原因处理方向CLOSE_WAIT 大量堆积netstat -tan | grep CLOSE_WAIT应用未调用 close()修资源释放逻辑查线程阻塞TIME_WAIT 大量存在netstat -tan | grep TIME_WAIT | wc -l主动关闭连接频率高开 SO_REUSEADDR优化连接复用FIN_WAIT_2 大量存在netstat -tan | grep FIN_WAIT_2对端不关闭连接检查对端应用 close()设置半关闭超时SYN_RCVD 堆积netstat -tan | grep SYN_RCVD半连接队列满检查 accept 循环和 somaxconnLISTEN 下 Recv-Q 大netstat -tan | grep LISTENaccept() 不及时优化事件循环和多路复用ESTABLISHED 下 Recv-Q 大netstat -tan | grep ESTABLISHED应用不读数据排查消费线程阻塞或 gc 停顿ESTABLISHED 下 Send-Q 大netstat -tan | grep ESTABLISHED对端不读数据或网络丢包抓包确认 TCP 窗口和重传connect 报 address in usenetstat -anp | grepTIME_WAIT 占用端口开 SO_REUSEADDR 或换端口这张表不是死规矩关键在于建立“状态 队列指标 → 应用行为 → 参数调整”的映射逻辑。6.3 我看线程几组后再补充几个独家建议第一个建议把netstat和strace配合使用。netstat告诉你连接现在在哪里strace告诉你应用在系统调用上卡了多久。曾经有个问题应用显示大量ESTABLISHED但业务没反应我用netstat看到连接都在再用strace -p pid发现进程全部阻塞在futex上线程池不够用和网络协议栈完全无关。如果你只盯着 TCP 状态容易被表象带偏。第二个建议做到“状态机日志化”。在业务代码里对连接建立和关闭的关键路径加日志输出local_address、peer_address、socket_fd这样在配合netstat定位时你能快速把状态和业务行为联系起来。没有日志配合的netstat排查相当于拿着地图但不知道自己在哪个城市。第三个建议改动任何内核参数前先备份。修改/etc/sysctl.conf里net.*的参数执行sysctl -p之后至少有半天观察时间确认状态分布有没有向健康方向变化。不要追求一次改完通常tcp_tw_reuse、tcp_fastopen这类参数打开以后对业务的影响要经过高峰期才能验证短时间看不出问题不代表没问题。最后说一点个人体会。很多人把网络问题当成玄学觉得调参数靠运气但当你真的把netstat输出和 TCP 状态机一帧一帧对上号再回头读 Socket 源码里那几行状态赋值语句会发现所有网络排查都变成了解谜题问题会把你带到某个具体状态状态会告诉你哪一步没走成于是答案自然浮出水面。我的经验是别急着调参先花 10 分钟把状态分布看明白这 10 分钟往往能省下后面几个小时的瞎折腾。这套方法论我从 Nginx 调优、Java 长连接池排查、到 Python asyncio 服务一个一个验证过来现在只要有 TCP 异常第一件事永远是打开终端跑那条状态统计命令先看清楚状态机走到了哪再谈下一步。
返回列表