1. 前言
本文排查的问题是经典的 TCP 队列溢出问题,因 TCP 队列问题在操作系统层面没有显著的指标异样,容易被疏忽,故把排查过程分享给大家。
2. 问题形容
A 服务调用 B 服务接口超时,B 服务主机 IOWAIT 高,具体超时状况分为两种:
- A 服务的申请在 B 服务日志中可查到,但 B 服务的响应工夫超过了 A 服务的期待超时工夫 3S。
- A 服务的申请在 B 服务日志中无奈查到。
3. 问题剖析
此种超时申请集中在很短的一段时间(通常在 2 分钟之内),过后便恢复正常,所以很难抓到问题现场剖析起因,只能搭建测试环境,A 服务继续申请 B 服务,在 B 服务主机上通过 DD 命令写入大量数据造成主机 IOWAIT 高,同时通过 TCPDUMP 在两端抓包剖析。
局部服务超时日志:
- 服务 A:Get http://xxx&id=593930: net/http: request canceled (Client.Timeout exceeded while awaiting headers)
- 服务 B: “GET xxx&id=593930 HTTP/1.1” 200 64 “-” “Go-http-client/1.1” “-” “-” 165000(单位微秒)
服务 A 发动申请 3S 后没有收到服务 B 响应,断开连接,服务 B 日志显示解决时长为 0.165S,远低于 3S,服务 A 侧看服务 B 的响应工夫为网络传输工夫、TCP 队列排队工夫及服务 B 利用程序处理工夫之和,因为是内网测试,网络传输工夫能够疏忽,次要排查方向应为 TCP 队列排队工夫。
4. 抓包数据分析
情景 1:服务 A 及服务 B 均有连贯日志打印。
服务 A 端数据包剖析:
09:51:43.966553000 服务 A 发动 GET 申请的数据包如下:
图 1:服务 A 发动 GET 申请
09:51:46.966653000 服务 A 发动 GET 申请 3s(即服务 A 设置的期待超时时长)后,因未收到服务 B 响应,服务 A 向服务 B 发动 FIN 被动断开连接。
图 2:服务 A 期待超时被动断开连接
09:51:59.958195000 服务 A 发动 http 申请 16s 后收到服务 B 的 http 响应报文,因服务 A 已被动敞开该连贯,故间接回复 RST。
图 3: 服务 B16s 后响应
服务 B 端数据包剖析:
09:51:44.062095000 服务 B 收到服务 A 发送的 http 申请包。
图 4:服务 B 收到服务 A 的申请
09:51:59.936169000 服务 B 响应服务 A,服务 B 从接管到 http 申请报文至响应 http 申请总用时约为 15s 多,但服务 B 打印的日志响应时长约为 0.165s。
图 5:服务 B15S 后响应
图 6:服务 B 日志显示响应工夫 0.165s
情景 2:服务 A 有连贯日志,服务 B 无连贯日志。
服务 A 端数据包剖析:
09:51:43.973791000 服务 A 向服务 B 发送一个 http 申请数据包,随后收到服务 B 重传的第二次握手的 syn+ack 包,超过 3s 未收到服务 B 的 http 响应后断开连接。
图 7:服务 B 重传 syn+ack
服务 B 端数据包剖析:
服务 B 重传了第二次握手的 syn+ack 包,收到服务 A 的 http 申请,服务 B 疏忽,未响应,服务 A 期待超时后断开了连贯。
图 8: 服务 B 疏忽服务 A 申请
5. 根因剖析
TCP 在三次握手过程中内核会保护两个队列:
- 半连贯队列,即 SYN 队列
- 全连贯队列,即 ACCEPT 队列
图 9:TCP 队列
TCP 三次握手过程中,第一次握手 server 收到 client 的 syn 后,内核会把该连贯存储到半连贯队列中,同时回复 syn+ack 给 client(第二次握手),第三次握手时 server 收到 client 的 ack,如果此时全连贯队列未满,内核会把连贯从半连贯队列移除,并将其增加到 accept 队列,期待利用过程调用 accept 函数取出连贯,如果全连贯队列已满,内核的行为取决于内核参数 tcp_abort_on_overflow:
- tcp_abort_on_overflow=0,server 会抛弃 client 的 ack。
- tcp_abort_on_overflow=1,server 会发送 reset 包给 client。
默认值是 0。
情景 1 的抓包数据显示连贯曾经进入全连贯队列,然而服务 B 日志显示的连接时间晚了 15S 多,阐明连贯在队列里期待了 15S 后才被利用解决。
情景 2 的抓包数据显示全连贯队列已溢出,内核依据 tcp_abort_on_overflow 的值为 0 抛弃了服务 A 的 ack,超过了服务 A 的超时等待时间。
论断:服务 B 主机在 IO 达到瓶颈的状况下,零碎 CPU 工夫次要耗费在期待 IO 响应及解决软中断上,服务 B 应用程序获取的 CPU 工夫无限,无奈及时调用 accept 函数把连贯取出并解决,导致 TCP 全队列溢出或队列等待时间过长,超过了服务 A 的超时工夫。
6. 如何察看和调整 tcp 全队列
图 10: TCP 全队列察看办法
当连贯处于 listen 状态时:
- Recv-Q:目前全连贯队列的大小
- Send-Q:目前全连贯最大队列长度
当 Recv-Q > Send- Q 时示意全队列溢出,可通过执行 netstat -s | grep “overflowed” 命令察看溢出状况,查看累计溢出次数,如果需察看一段时间内的全队列溢出状况,倡议应用监控零碎采集数据,比方 prometheus。
图 11: TCP 队列溢出监控
TCP 全连贯队列最大值取决于 min(somaxconn, backlog),其中:
- somaxconn 可通过内核参数 /proc/sys/net/core/somaxconn 设置,默认值是 128。
- backlog 是 listen(int sockfd, int backlog) 函数中的 backlog 大小,Nginx 默认值是 511,能够通过批改配置文件设置其长度。
7. 结语
本次问题,因为服务对成功率要求很高,所以先通过调大服务 B 主机 /proc/sys/net/core/somaxconn 参数值及服务 A 的超时工夫来缓解超时问题,临时保障了接口成功率。但要从根本上解决问题,仍需解决诱因 io 瓶颈,因为服务 B 主机挂载的共享 sas 存储集群上有其余客户的主机偶然 io 很大,影响了整个集群的性能。为解决此问题,更换为独享的 ssd 盘,并通过 blktrace+fio 剖析,将 io 调度算法批改为 noop,io 性能显著晋升,TCP 队列溢出问题也随之解决。
原文链接 本文为阿里云原创内容,未经容许不得转载。