导语
背景
分析问题
解决问题
$ cat /proc/interrupts | grep eth | awk '{print $1,$NF}'
77: eth0-0
78: eth0-1
79: eth0-2
80: eth0-3
81: eth0-4
82: eth0-5
83: eth0-6
84: eth0-7
$ echo 0 > /proc/irq/77/smp_affinity_list
$ echo 1 > /proc/irq/78/smp_affinity_list
.....
nstat -za | grep -Ei '(TcpExtListenOverflows|TcpExtListenDrops|TcpExtTCPBacklogDrop)'
TcpExtListenOverflows 0 0.0
TcpExtListenDrops 0 0.0
TcpExtTCPBacklogDrop 0 0.0
很明显这种 case 只需要适当增加 accept-queue 大小就行
acccept-queue = min(nginx backlog, net.core.somaxconn)
nginx backlog 默认 511
net.core.somaxconn 默认 128
如果系统未修改参数,那么默认 accept-queue= 128 ,这个值太小了,一般 2048 比较合适 具体调整如下,网络上介绍的文章比较多
注:nginx backlog 是全局配置 ,一个 port 只需要在一个地方添加即可
案例 2.1 解决方案
# linux kernel
sysctl -w net.core.somaxconn=2048
sysctl -w net.ipv4.tcp_max_syn_backlog=4096
# nginx
server {
listen 80 backlog=2048
.....
}
2.2 syn 包丢失 - TcpExtListenOverflows 不变 & TcpExtListenDrops增大
相比 3.1.1 案例,这个案例 TcpExtListenOverflows 不变,原因更加复杂。当时解决问题的时候也是偶然发现内核有错误堆栈,才分析出具体原因。
具体原因是 TCP 的内存分配采用的是 slab + buddy算法,slab 块不够时,通过buddy 内存分配算法申请内存,如果高并发情况下 buddy 分配的内存刚好用完(此时系统仍然有可用的 free 内存),此时会因为申请不到 buddy 内存而直接丢弃TCP 请求,具体堆栈如下,这个堆栈网上能搜到很多,有其他程序比如 MySQL 也会出现
这个堆栈有几个重要的信息
A、nginx tcp_v4_syn_recv_sock (第一个syn 请求过来) 时申请内存失败
B、TCP 申请内存用的是 slab 算法 (kmem_cache_alloc)
C、kmem_cache_alloc申请失败时,通过 buddy 内存算法来补充 slab 空间
D、申请内存的 buddy order = 1 表示需要申请的内存大小是 2 * 4k = 8k
简单介绍下 buddy 内存算法
buddy 内存情况可以通过 /proc/buddyinfo 来查看
简单说明 第 3 列 556 表示 numa node 0 Normal 区域 order = 2 有 556 块,占用大小是 556 * 2 * 2 * 4K,也就是有556 个连续的16K内存
buddy 内存分配原则是高级别可以给低级别用 ,但是低级别不能给高级别用
比如 order = 1 可用,order = 0 不可用,此时如果申请 order = 0,会从 order = 1 借用1个,拆成2个,1个给申请方,另外一个划归 order = 0
buddy 内存是预分配的,可能在高并发瞬间用完,这个时候就会有内存分配失败的情况。
下面的图是出问题时 nginx 的 buddyinfo 监控
可以发现 Normal 区域 order = 0 有 174851 块,但是 order > 0 都没有可用内存,nginx 刚好要申请的 order = 1,所以申请内存失败,syn 包丢弃
针对这种情况阿里的工程师给了一个解决方案
# numa 在当前node回收内存
sysctl -w vm.zone_reclaim_mode=1
# 调大系统最小内存阈值
512000 =
真实测试下来并没有起到做大作用,只能再进行深入分析。首先发现 Nginx 机器物理内存非常大有 128G,进一步分析发现大部分内存都处于 cache 模式,此时可以联想到大部分内存都因为写巨大的 nginx 日志而处于文件 cache 模式,解决问题的关键变成为尽快释放 cache 缓存。
通过 echo 1 > /proc/sys/vm/drop_caches 释放 cache 会瞬间影响机器负载,还好 linux 有个 vm.extra_free_kbytes 参数,可以控制回收内存的时机
案例 2.2 解决方案
# 设置更大的水位线,让 kswapd 提前启动,
# 比如我们nginx 服务器内存是128G,但是由于写入的日志非常大,大量的日志
# 使得内存都被划分到 cache 区域,只有 kswapd 启动后才会回收
sysctl -w vm.extra_free_kbytes= 1048576
# 调大系统最小内存阈值
1048576 =
2.3 syn 包丢失 - TcpExtTCPBacklogDrop 增大
相比前面2个 案例 3.2.1、3.2.2, 这个更加复杂也难易理解,主要是因为是在多核多进程 lock 时发生,先看下面流程图,相比 3.2.1 的图更加复杂,当某一个核 比如 CPU core 0 处于 accept ->lock 状态, 这个时候,其他 CPU core 收到的 syn 数据包不能直接进入 syn-queue ,而是进入到了 backlog-queue ,backlog-queue 的大小是sk_rcvbuf + sk_sndbuf
假如默认内核参数如下
net.ipv4.tcp_rmem=4096 87380 16777216
net.ipv4.tcp_wmem=4096 65536 16777216
那么默认 backlog-queue = 87380 + 65536 = 152916
看起来还挺大,但是高并发下面其实还是比较小的,backlog 每次进去的数据大小是 800 ~ 1800,不同网卡驱动有些区别,也就是lock 期间最多进入的请求为 152916/{800,1800} = 190 ~ 80,确实有些小。
2种方式增大 backlog-queue
A、调整内核参数 ,修改中间的值 net.ipv4.tcp_rmem & net.ipv4.tcp_wmem
B、nginx 提供了修改的方式,比如如下 server backlog=2048 rcvbuf=131072 sndbuf=131072
修改后的效果,可以看出增大 rcvbuf & sndbuf 还是很有效果 但是有一个机器有些问题,效果不明显
继续分析
上面看到,之所以会产生 TcpExtTCPBacklogDrop 是因为多 CPU core 同时 accept,如果有 1 个 cpu core lock 了 sock,会导致其他 CPU core 收到请求后只能放到 backlog-queue,如果降低lock ,那么就从根本上解决, Nginx 提供了 reuseport 的功能(需要内核支持 SO_REUSEPORT)
无 reuseport 的情况如下图,多个 cpu core 竞争 一个 sock->accept-queue,
使用 reuseport 后如下图,每个cpu core 都分配 1 个一套资源,避免了竞争
开启 reuseport 后,nginx 同一个监听端口会出现多条记录,也就对应多个内核 sock,并且多个 accept-queue,syn-queue,backlog-queue。
效果如下图
2.3 解决方案总结
增加 reuseport 选项
# nginx
server {
listen 80 backlog=2048 rcvbuf=131072 sndbuf=131072 reuseport;
.....
}
3、优化总结
1. 通过绑定网卡中断号到 CPU core ,解决网卡 overruns
2. 调整 backlog 来解决 TcpExtListenOverflows++ & TcpExtListenDrops++
3. 调整 vm.extra_free_kbytes && vm.min_free_kbytes 解决 buddy memory 分配失败的问题导致的 TcpExtListenDrops++
4. nginx 增大 rcvbuf & sndbuf ,并且开启 reuseport 来解决 TcpExtTCPBacklogDrop++
4、内存说明
4.1 backlog 每次进去的数据大小是 800 ~ 1800
backlog 进入的是一个数据包,也就是 1 个 sk_buff,1个 sk_buff会包含 (sizeof sk_buff)+ data (比如 1500 以太包最大值),此时并没有进入 TCP 栈处理。
4.2 申请内存的 buddy order = 1 表示需要申请的内存大小是 2 * 4k = 8k
内核内存的特点就是大小固定(因为结构体都是固定大小),针对这种场景内核设计了 slab 内存算法,将大块内存分割成固定的小块,大块内存采用 buddy 算法,小块内存采用 slab 算法,比如固定申请大小为 2600 大小的内存,就可以采用 2个连续的4K内存 2700 * 3 = 8100 ,这样可以分成3个小块,实际其实更加复杂,可以参考后面的参考文献,slab 的情况可以查看 /proc/slabinfo
slab 情况查看如下
总结
因为篇幅原因每个案例只能简单介绍,继续深挖还可以写很多内容,比如 backlog 为啥 511 有点小。Nginx 调优的地方有很多,本文章只介绍了网络的部分,其中部分是网络上已经很成熟的方案,其他的都是自己通过实践总结的,并且起到作用。调优本身是一个实践的过程,本文讲述的内容都是实践过并且有用的,但是不一定在其他场景100%适用,不过分析过程可以作为参考。
END
阅读推荐