高并发引起的网络不通定位实战

背景

最近许多客户投诉设备定时升级失败。和客户沟通后收集了部分的日志,发现其升级失败的共同原因都是connect超时。我是负责维护客户端升级模块的,但短时间内大量曾经正常的用户出相同网络问题,我觉得应该找服务器那头,所以找了服务器那头开始定位扯皮。

分析

客户端错误日志

我们的设备使用HTTPS连接升级服务器,connect失败代表在HTTP、TLS之前,TCP的握手失败了。TCP握手失败并非罕见现象,其可能由网络不稳等多种原因引起,但大量设备多次重试仍然失败,首先需要怀疑的就是服务器本身的网络容量不足了。

connect超时代表客户端发送的SYN包没有响应,服务器既没有正常回应SYN-ACK包,也没有发送RST来拒绝连接。

服务器日志与计数

可能1:半连接队列满

登录服务器,查看运行时间,发现服务器并没有重启过,说明服务器没有因为意外关机而导致不回复TCP握手。查看NGINX日志,并没有发现记录异常。

基于已有信息,我决定先查看TCP半连接队列是否有问题,半连接队列满导致的失败通常不会有任何日志。通过命令sudo sysctl net.ipv4.tcp_syncookies,发现SYN Cookie被启用,此时便排除半连接队列满导致的问题的可能性了,开启了SYN Cookie的情况下,半连接队列满并不会导致无法连接。

可能2:全连接队列满

除了半连接队列满,全连接队列满也是一个问题。全连接队列类似半连接队列,这里存储了已经完成握手,但还尚未被应用accept的TCP连接。与半连接队列不同,全连接队列并没有SYN Cookie这样的机制缓解队列满造成的问题。如果全连接队列满的情况下继续握手,新完成握手的连接就无处存放,所以全连接队列满时Linux内核同样会直接丢弃SYN数据包。不过,由于全连接队列满通常只是暂时的,所以Linux内核也不会发送RST包拒绝连接,从而让客户端能够重试。由于全连接队列满而丢弃的包,也无法被应用的日志记录。

通过netstat -s可以查询内核的网络子系统的计数信息。这之中有三个计数可以反映系统曾因全连接队列满而丢弃过握手包。

1
2
3
4
1398511 times the listen queue of a socket overflowed (TcpExtListenOverflows)
1465234 SYNs to LISTEN sockets dropped (TcpExtListenDrops)

TCPReqQFullDrop: 0

可以看到,TcpExtListenDrops表示大量的握手包被丢弃,且TCPReqQFullDrop为0表示没有因为半连接满而丢弃的。而TcpExtListenOverflows则表示由于全连接队列满而丢弃的握手包。显然,这个系统上出现过大量的全连接队列满导致的握手包丢包,且绝大多数SYN丢包都是全连接队列满导致的。

定时升级通常是国内的凌晨两点到四点,白天几乎没什么用户,只能先列出一些监控点。

导致并发能力不足的瓶颈有很多,包括CPU性能不足、内存不够导致频繁swap、软件设计不好有大循环(NGINX很好,但我司的是openresty,写的lua可能不行)、cgroup配额配置失误、意外的阻塞操作等等。

公司的升级服务器有三台,都是8核16G的,每个服务器开了4个Nginx工作进程,worker_connections为65535,并且没有出现accept失败相关日志,所以我推断并非连接超限或CPU性能不足导致(公司应该也没在中国卖出去过超60万台有升级License的设备)。

通过systemd-cgls查看配额,发现并没有为NGINX的service配置限制。

这是我全程会议上远程同事桌面操作的,卡的一批,很多快捷键都用不了。之所以这么热心,一个是因为这是少见的终端高并发实战经验,另一个是因为解决客户问题算我的OKR。

服务器监控

为了排查问题,我先让同事调整到7个工作进程,然后定时使用ss -ltn来监控全连接队列情况,使用ss -s监控TCP连接总数量,并用 pidstat -u -p $(pgrep -d,) 1监控nginx的CPU使用,通过ps -o pid,state,pcpu,rss,maj_flt,min_flt,comm -p $(pgrep nginx)监控内存占用、缺页与运行状态。

虽然说升级服务器不是以2C标准做的大规模开放的公共服务器,但是好歹也是个大厂,居然菜到连个普罗米修斯之类的监控都没有,我真是无法理解。当然也可能因为我不是他们部门的,所以没接触到。不过考虑到要我跨部门去定位,那我估计他们是没有,或者有也看不懂。

不直接在生产环境下使用perf工具是因为perf工具会trace进程,让本来并发就处理不好的状态雪上加霜。

由于部署在公网云平台上,我也不是管理升级服务器的部门的,所以没法直接从公司内部找个压测工具上压力,不然找个空闲时间,一个ab或者hey打个压力,再perf一下不就搞定了。

由于公司内网有限制,所以接下来我会以我自己的电脑为例,解析核心输出。下面的输出只做输出内容参考,并非我同一时间获得,也非公司真实环境。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# ss -s 的输出
Total: 1430
TCP: 1672 (estab 732, closed 476, orphaned 2, timewait 476)

Transport Total IP IPv6
RAW 0 0 0
UDP 5 4 1
TCP 1196 1196 0
INET 1201 1200 1
FRAG 0 0 0

# ss -ltn 的输出
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 513 511 0.0.0.0:443 0.0.0.0:*
LISTEN 0 511 0.0.0.0:80 0.0.0.0:*

# pidstat -u -p $(pgrep -d, nginx) 1的输出
00:40:22 UID PID %usr %system %guest %wait %CPU CPU Command
00:40:23 0 42216 0.00 0.00 0.00 0.00 0.00 3 nginx
00:40:23 0 52817 0.00 0.00 0.00 0.00 0.00 1 nginx

# ps -o pid,state,pcpu,rss,maj_flt,min_flt,comm -p $(pgrep nginx) 的输出
PID S %CPU RSS MAJFL MINFL COMMAND
956 S 0.0 2028 0 29 nginx
957 D 0.7 20076 6 4096 nginx

由于只有pidstat会自己输出时间,所以其他的命令使用date -Iseconds来显示秒级时间,设计为每五秒执行一次。

ss -s主要看处于estab状态的TCP连接数量,它和worker_connections是强相关的,并占用文件描述符,我这里是732,压力处于正常范围内。

ss -ltn用于观察TCP全连接队列的积压情况,它的Recv-Q代表积压的全连接(即已完成握手,尚未accept的TCP连接)。

pidstat -ur -p pid1,pid2 1用于观察CPU的占用情况,主要观察进程占用的用户态时间和内核态时间,用于确认是否是CPU性能不足导致的问题。

ps -o pid,state,pcpu,rss,maj_flt,min_flt,comm -p $(pgrep nginx)的输出用于观察内存是否超限,是否有过大量缺页导致频繁与磁盘交互,以及观察高并发状态下进程是否处于R状态占用CPU。

分析监控日志

又经过一晚上,发现果然即使将NGINX工作进程数调整到7仍然会有积压,于是写命令匹配时间分析情况。发现在全连接队列满时,NGINX进程的CPU占用率并没有接近100,用户态内核态都不高,且TCP estab状态的连接也仅有几千,完全没有到达上限。

感谢AI,以前要是直接在服务器写命令去处理字符串起码得几十分钟到一个小时才能搜明白,现在只需要让AI写几分钟就能搞定了。

通过检查全连接积压时的CPU和内存占用,发现CPU和内存占用较低,完全处于低负载状态,剩余性能非常充足。但观察ps的输出,发现状态有时为D,这说明NGINX进入了不可中断的休眠。这种休眠通常只有在读硬盘文件时才会出现,此时便定位到了是读文件阻塞导致的。由于磁盘文件的读写无法配置为非阻塞,所以一旦磁盘IO慢,就导致整个NGINX的所有工作都慢。然而通常的磁盘IO是会被内核缓存的,我们的文件也并不是很大,好几个文件加起来才几百MB,设备内存16G,这些文件应该早就进了页缓存了才对。而且我们的磁盘是固态,很难出现长期阻塞的问题。

准确地说,磁盘文件的描述符是可以配置为非阻塞的,但是在绝大多数文件系统上,配置为非阻塞都并没有意义。

具体问题分析

已经定位到磁盘IO慢的问题,接下来就该看看为什么没有被内核缓存,以及具体问题的根因了。给服务器安了个vmtouch工具,看了一下那几个静态文件,发现被缓存的大小为0,手动尝试 cat /mnt/.../update_xxx.zip > /dev/null测试了一下,发现缓存还是0,又vmtouch -f file尝试强制加载,缓存大小仍然为0.

此时我已经觉得是文件系统的问题或挂载的问题了,通过mount命令一看,发现这里挂载的是一个基于FUSE的网络对象存储,每次读该文件都会向网络请求,就导致在并发稍微一高的情况下就出现阻塞占不满CPU的情况。

在这里继续感谢AI,我还是问AI才知道有vmtouch这种可以查看页缓存的工具的。

解决

询问了一下,发现最近把升级包的存放位置从本地磁盘换成了oss的挂载,于是让他们撤了oss的挂载,用本地磁盘解决。过了几天没有再接到相关问题了,问题就此解决。

其实这个oss挂载就是最近换的,就算我不去积极定位,只要多催催他们,估计也就晚个一天两天他们也能发现然后解决,毕竟他们有这个问题的一手信息。

另一个角度,这段时间肯定还会有更多用户提单,其实都能算OKR的,要是我不这么着急去定位,应该还能多出个三两个问题解决数量的OKR。但是现网实战经验这玩意确实是错过了就没有了,如果是2C的大厂,这种问题恐怕根本轮不到我一个开发参与定位,就连接触到公网服务器都不太可能。

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2019-2025 Ytyan

请我喝杯咖啡吧~