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队列溢出问题也随之解决。
我们是阿里云智能全球技术服务-SRE团队,我们致力成为一个以技术为基础、面向服务、保障业务系统高可用的工程师团队;提供专业、体系化的SRE服务,帮助广大客户更好地使用云、基于云构建更加稳定可靠的业务系统,提升业务稳定性。我们期望能够分享更多帮助企业客户上云、用好云,让客户云上业务运行更加稳定可靠的技术,您可用钉钉扫描下方二维码,加入阿里云SRE技术学院钉钉圈子,和更多云上人交流关于云平台的那些事。