背景:在标准的 Web 架构网站中,我们通常都会使用类 Nginx 来作为整个流量以及静态资源的入口,为了满足整个入口的高可用性,比较简单和通用的方案就是 Nginx 使用 NFS 之类的共享存储方案。 但是在过往的经验中,我们经常发现在 Nginx 服务过程中可能出现一个访问异常的 case,本篇文章就提供几个针对该场景的排查思路。
Nginx 提供的 URL 无法访问,典型的表现是 使用 Nginx 作为静态页时,图片无法正常加载,或者登录到 Nginx 所在的计算资源后,发现在数据盘目录无法执行 df 或 ls 之类的操作。
2.1 丢包排查
# tcpdump监听eth0端口
$ tcpdump -i eth0 -nnnn -ttt port 24568
# 模拟正常请求来监听tcp传输
# 观察抓包结果
$ nc -u 24568 172.16.1.21 80
# 注意: 此时如果丢包可能和防火墙,iptable有关系
2.2 查看系统丢包统计
# 还是有丢包的
$ netstat -s | grep -E 'overflow|drop'
60 dropped because of missing route
1 ICMP packets dropped because they were out-of-window
9234 times the listen queue of a socket overflowed
9388 SYNs to LISTEN sockets dropped
2.3 回顾半连接与全连接
Linux 进程监听端口时,内核会给它对应的 socket 分配两个队列:
syn queue: 半连接队列。server 收到 SYN 后,连接会先进入 SYN_RCVD 状态,并放入 syn queue,此队列的包对应还没有完全建立好的连接(TCP 三次握手还没完成accept queue: 全连接队列。当 TCP 三次握手完成之后,连接会进入 ESTABELISHED 状态并从 syn queue 移到 accept queue,等待被进程调用 accept() 系统调用 “拿走”注意: 这两个队列的连接都还没有真正被应用层接收到,当进程调用 accept () 后,连接才会被应用层处理,具体到我们这个问题的场景就是 nginx 处理 HTTP 请求。
在写 server 端应用时,它们的底层在监听端口时最终都会调用 listen() 系统调用,处理新请求时都会先调用 accept() 系统调用来获取新的连接,然后再处理请求,只是有各自不同的封装而已
// 调用 listen 监听端口
l, err := net.Listen("tcp", ":80")
if err != nil {
panic(err)
}
for {
// 不断调用 accept 获取新连接,如果 accept queue 为空就一直阻塞
conn, err := l.Accept()
if err != nil {
log.Println("accept error:", err)
continue
}
// 每来一个新连接意味着一个新请求,启动协程处理请求
go handle(conn)
}
2.4 思考 Linux 中对 syn 和 accept 队列的控制
内核既然给监听端口的 socket 分配了 syn queue 与 accept queue 两个队列,那它们有大小限制吗?可以无限往里面塞数据吗?
当然不行!资源是有限的,尤其是在内核态,所以需要限制一下这两个队列的大小。
listen 的系统调用是如下定义的:
# man listen
int listen(int sockfd, int backlog)
结论:
backlog 参数同时指定了 socket 的 syn queue 与 accept queue 大小net.core.somaxconn 的值,即: max accept queue size = min(backlog, net.core.somaxconn)net.ipv4.tcp_syncookies=1),当 syn queue 满了,server 还是可以继续接收 SYN 包并回复 SYN+ACK 给 client,只是不会存入 syn queue 了。因为会利用一套巧妙的 syncookies 算法机制生成隐藏信息写入响应的 SYN+ACK 包中,等 client 回 ACK 时,server 再利用 syncookies 算法校验报文,校验通过后三次握手就顺利完成了。所以如果启用了 syncookies,syn queue 的逻辑大小是没有限制的net.core.somaxconn 外,还会受到 net.ipv4.tcp_max_syn_backlog 的限制,即:max syn queue size = min(backlog, net.core.somaxconn, net.ipv4.tcp_max_syn_backlog)2.5 队列溢出
毫无疑问,在队列大小有限制的情况下,如果队列满了,再有新连接过来肯定就有问题。
在具体的 Linux 源码中有如下定义:
ListenDrops 计数器 + 1ListenOverflows 和 ListenDrops 计数器 + 1注意: 通过 netstat -s 看到的丢包统计,对应的就是 ListenOverflows 和 ListenDrops 计数器.
也可以使用 nstat -az 命令查看具体的参数
$ nstat -az | grep -E 'TcpExtListenOverflows|TcpExtListenDrops'
TcpExtListenOverflows 9234 0.0
TcpExtListenDrops 9388 0.0
注意: 对于低版本的内核而言当 accept queue 满了,并不会完全丢弃 SYN 包,而是对 SYN 限速。
2.6 Nginx 中的影响
通过容器中抓包和 netstat -s 统计,其实发现肯定是丢包了,再根据前面的分析,推测是 syn queue 或 accept queue 满了
# 查看syncookie(开启)
$ cat /proc/sys/net/ipv4/tcp_syncookies
1
确认启用了 syncookies,所以 syn queue 大小没有限制,不会因为 syn queue 满而丢包,并且即便没开启 syncookies,syn queue 有大小限制,队列满了也不会使 ListenOverflows+1
从计数器结果来看,ListenOverflows 和 ListenDrops 的值差别不大,所以推测很有可能是 accept queue 满了,因为当 accept queue 满了会丢 SYN 包,并且同时将 ListenOverflows 和 ListenDrops 计数器分别 +1.
# 验证下accept queue 是不是满了
$ ss -lnt
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 511 *:8888 *:*
LISTEN 0 1024 *:80 *:*
LISTEN 0 128 *:22 *:*
LISTEN 0 128 [::]:8090 [::]:*
LISTEN 0 128 [::]:4520 [::]:*
LISTEN 0 128 [::]:9999 [::]:*
LISTEN 0 511 [::]:80 [::]:*
注意:
LISTEN 状态,Send-Q 表示 accept queue 的最大限制大小,Recv-Q 表示实际大小ESTABELISHED 状态,Send-Q 和 Recv-Q 分别表示发送和接收数据包的 buffer所以当如下 accept queue 状态时,说明 accept queue 溢出了,如果再收到 SYN 包就会丢弃掉
$ ss -lnt
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 129 128 *:80 *:*
导致 accept queue 满的原因一般都是因为进程调用 accept() 太慢了,导致大量连接不能被及时 “拿走”。
那么什么情况下进程调用 accept () 会很慢呢?猜测可能是进程连接负载高,处理不过来。
那负载高,具体可能是 cpu繁忙导致,也可能是 IO慢导致,当文件 IO 慢时就会有很多 IO WAIT,此时会占用 cpu 时间片,影响 cpu 干其他活。
2.7 somaxconn 的默认值很小
注意: Send-Q 的值默认受 net.core.somaxconn 参数影响,需要注意的是,容器内部的该值默认是 128(受限于 netns)
其次,需要注意的是,一般只有进程一般在启动的时候才会调用 listen(),因此需要在进程启动之前设置该值。
但是发现设置完成之后,nginx 可能不生效。
原因是 nginx 在 listen() 时,并没有读取 somaxconn 作为 backlog 默认值传入,它有自己的默认值,也支持在配置里改。
ngx_http_core_module 的官方文档中定义的值为 511
当然也可以设置 nginx 的配置来生效:
listen 80 default backlog=1024;
不论是虚拟机还是容器场景下,在使用 nginx 来支撑高并发的业务时,记得要同时调整下 net.core.somaxconn 参数和 nginx 配置中的 backlog 值来整体优化,同时尽可能避免使用 NFS 之类的共享存储 (可能会产生 IO 影响)
原文作者:云原生运维技术;如有侵权,请联系管理员删除。