ITPub博客

首页 > 数据库 > 数据库开发技术 > Nginx 使用 NFS 共享存储作为高可用方案的问题和排查过程

Nginx 使用 NFS 共享存储作为高可用方案的问题和排查过程

原创 数据库开发技术 作者:云原生运维技术 时间:2023-02-14 19:30:00 0 删除 编辑

背景:在标准的 Web 架构网站中,我们通常都会使用类 Nginx 来作为整个流量以及静态资源的入口,为了满足整个入口的高可用性,比较简单和通用的方案就是 Nginx 使用 NFS 之类的共享存储方案。 但是在过往的经验中,我们经常发现在 Nginx 服务过程中可能出现一个访问异常的 case,本篇文章就提供几个针对该场景的排查思路。

1 问题现象

Nginx 提供的 URL 无法访问,典型的表现是 使用 Nginx 作为静态页时,图片无法正常加载,或者登录到 Nginx 所在的计算资源后,发现在数据盘目录无法执行 df 或 ls 之类的操作。

2 排查过程

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)

结论:

  • listen 的 backlog 参数同时指定了 socket 的 syn queue 与 accept queue 大小
  • accept queue 最大不能超过 net.core.somaxconn 的值,即: max accept queue size = min(backlog, net.core.somaxconn)
  • 如果启用了 syncookies (net.ipv4.tcp_syncookies=1),当 syn queue 满了,server 还是可以继续接收 SYN 包并回复 SYN+ACK 给 client,只是不会存入 syn queue 了。因为会利用一套巧妙的 syncookies 算法机制生成隐藏信息写入响应的 SYN+ACK 包中,等 client 回 ACK 时,server 再利用 syncookies 算法校验报文,校验通过后三次握手就顺利完成了。所以如果启用了 syncookies,syn queue 的逻辑大小是没有限制的
  • syncookies 通常都是启用了的,所以一般不用担心 syn queue 满了导致丢包。(为防止 SYN Flood 攻击,填满 server 端的 syn queue)
  • 如果 syncookies 没有启用,syn queue 的大小就有限制,除了跟 accept 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 源码中有如下定义:

  • 如果 syn queue 满了并且没有开启 syncookies 就丢包,并将 ListenDrops 计数器 + 1
  • 如果 accept queue 满了也会丢包,并将 ListenOverflows 和 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;

3 总结

不论是虚拟机还是容器场景下,在使用 nginx 来支撑高并发的业务时,记得要同时调整下 net.core.somaxconn 参数和 nginx 配置中的 backlog 值来整体优化,同时尽可能避免使用 NFS 之类的共享存储 (可能会产生 IO 影响)


原文作者:云原生运维技术;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2017-01-18

  • 博文量
    53
  • 访问量
    20337