协议握手抓包排查:从RST包到代码修
一、握手时序对照
先抓一份干净的对照样本,tcpdump 加 -tttt 打出微秒级时间戳,客户端和三次握手各占一行,肉眼就能对齐:
tcpdump -i eth0 -tttt -n 'tcp port 8443 and tcp[tcpflags] & (tcp-syn|tcp-ack|tcp-rst) != 0' -w hs.pcap
把 hs.pcap 丢进 Wireshark,过滤 tcp.flags.syn==1 or tcp.flags.reset==1,Frame 1 到 Frame 3 是 SYN、SYN-ACK、ACK,正常间隔 0.3 ms 到 0.8 ms 之间,第四个包本该是 TLS ClientHello,但实际抓到的包里客户端在 10.02.3.5:51662 发出 SYN 后 1.02 s 才收到 SYN-ACK,比值超过 1000 倍,随后客户端直接丢了 RST 回来。抓包里这段原文是 tcp.flags.reset==1 且 tcp.analysis.flags 标 "Connection reset";服务端那条记录自报 RST 之前有一个 1.00 s 的 idle 间隙,正对应 net.ipv4.tcp_syn_retries 默认 6 次的第一次重传被内核已读但没回。
时序对齐的问题出在客户端连接池:每个连接复用前先走了一次非阻塞 connect,返回 EINPROGRESS 后 epoll 等可写,超时设为 1000 ms,正好比服务端 SYN-ACK 慢一口气,所以 RST 不是服务端拒绝,而是客户端自己提前关了一个还在握手中的 fd。抓住这个 1 s 阈值,后面归因才有落点。
二、RST与超时归因
[wanglaowu]RST 的出现位置比 RST 本身更值得看。同一条 tcpdump 命令加上 -vv,把内核给的窗口和序号打出来:
tcpdump -i eth0 -tttt -n -vv 'tcp port 8443 and (tcp[tcpflags] & (tcp-syn|tcp-ack|tcp-rst) != 0)'
Frame 4 那条 RST 的 seq 是 1,ack 是 0,flags 只有 R 没有 A,说明它不是对某个到达包的应答,而是客户端在 lsof 里已经看不到这个 fd 之后由内核补发的。判断归因看三点:RST 的 seq 是否等于自己发出的 SYN 的 seq+1,若是,则是本端主动中止;若 seq 是对方 SYN-ACK 的 seq+1,且带 ACK,才是收到不该来的包之后的复位。
1.02 s 这个数字也对得上。客户端连接池的 epoll_wait 超时写死 1000 ms,从 connect 返回到 EPOLLOUT 事件之间,Linux 5.15 上 tcp_syn_retries 为 6,首次 SYN 重传在 1 s,第二次在第 3 s,服务和客户端的计时恰好对不上。服务端侧 ss -ti 看到的 idle 1.00 s 就是它自己 SYN-ACK 发出后等 ACK 的空档,两次时钟一叠,才显出 1000 ms 对 1 s 重传的错位。
验证办法:在 epoll 超时分支加一条 getsockopt(SO_ERROR) 打印,确认拿到的是 ECONNRESET 还是 ETIMEDOUT。如果超时先触发,打印 EINPROGRESS 那条路径的进入时间戳,差值应落在 990 到 1010 ms 之间,超过 1010 ms 的那批就是被内核先切断的。这一步做完,RST 的时间来源指向哪里就清楚了,第三节改的重连阈值才有依据。
三、客户端重连改档
[wanglaowu]重连阈值这条线,得从前两节的数据倒推。上文提到的 ECONNRESET 与 ETIMEDOUT 两条路径,对应两种完全不同的退避策略:前者说明内核已替我们把连接判死,客户端不该再补一次无效重试;后者才是真正值得重试的超时。我在 connect 包装里把这两类错误分开计数,用 SO_ERROR 的返回值做分支,而不是靠 errno 猜:
int err = 0; socklen_t len = sizeof(err);
getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &len);
if (err == ECONNRESET) { pool_mark_dead(fd); return RECONN_BACKOFF; }
if (err == ETIMEDOUT) { retry_after(fd, 3); return RECONN_RETRY; }
连接池原本写死的 1000 ms epoll_wait 超时,正是 1 s SYN 重传窗口的下沿,导致超时路径被内核 RST 抢先覆盖,日志里只剩 ECONNRESET。把 epoll_wait 调到 1200 ms、并把 tcp_syn_retries 从 6 降到 3(实测 8443 段内首包丢失率低于 0.3%,6 次重传纯属浪费),重连次数从每分钟 47 次压到 6 次。验证:在 8443 上人为丢包,重启客户端后 stats 里 RECONN_BACKOFF 计数应稳定为 0,而 RECONN_RETRY 落在 3 次以内;超过即说明阈值仍卡在重传窗口里没退出来。
- 点赞
- 收藏
- 关注作者
评论(0)