diff --git a/blog/content/posts/2021_nf_conntrack.zh-cn.md b/blog/content/posts/2021_nf_conntrack.zh-cn.md new file mode 100644 index 0000000..3d8b76b --- /dev/null +++ b/blog/content/posts/2021_nf_conntrack.zh-cn.md @@ -0,0 +1,162 @@ +--- +title: "2021_nf_conntrack" +date: 2021-03-11T16:30:22+08:00 +draft: true +--- +# 内核报错 nf_conntrack: table full, dropping packet + +## 症状 + +服务器负载、内存占用正常,但请求大量超时,服务器/应用访问日志看不到相关请求记录。 + +在 `dmesg` 或 `/var/log/messages` 看到大量以下记录: + +``` +kernel: nf_conntrack: table full, dropping packet. +``` + +## 原因 + +服务器访问量大,内核 netfilter 模块 conntrack 相关参数配置不合理,导致 IP 包被丢掉,连接无法建立。 + +### 详细 + +nf_conntrack 模块在 kernel 2.6.15(2006-01-03 发布)被引入,支持 IPv4 和 IPv6,取代只支持 IPv4 的 ip_connktrack,用于跟踪连接的状态,供其他模块使用。 + +需要 NAT 的服务都会用到它,例如防火墙、Docker 等。以 iptables 的 `nat` 和 `state` 模块为例: + +- `nat`:根据转发规则修改 IP 包的源/目标地址,靠 conntrack 记录才能让返回的包能路由到发请求的机器。 +- `state`:直接用 conntrack 记录的连接状态(`NEW`/`ESTABLISHED`/`RELATED`/`INVALID` 等)来匹配防火墙过滤规则。 + +nf_conntrack 跟踪**所有**网络连接,记录存储在 1 个哈希表里。首先根据五元组算出哈希值,分配一个桶,如果有冲突就在链表上遍历,直到找到一个精确匹配的。如果没有匹配的则新建。 + +即使来自客户端的访问量不多,内部请求多的话照样会塞满哈希表,连接记录会在哈希表里保留一段时间,根据协议和状态有所不同,直到超时都没有收发包就会清除记录。如果服务器比较繁忙,新连接进来的速度远高于释放的速度,把哈希表塞满了,新连接的数据包就会被丢掉。此时 netfilter 变成了一个黑洞, 这发生在 3 层(网络层),应用程序毫无办法。 + +如果有人 DDoS 攻击的话情况更糟,无论是空连接攻击还是简单地用短连接发大量请求都能轻易塞满哈希表。或者更隐蔽点,研究了计算 conntrack hash 值的算法后,构造很多 hash 一致的不同五元组的数据包,让大量记录堆在同一个桶里,使得遍历超长的冲突链表的开销大得难以接受。在当前的内核 conntrack 模块实现中,这是无法避免的(除非关掉不用),因为所有鸡蛋都在一个篮子里面。 + + + +## 诊断 + + conntrack默认最大跟踪65536个连接,查看当前系统设置最大连接数和 + +``` +[root@server ~]# sysctl -a +net.netfilter.nf_conntrack_max = 65536 +net.netfilter.nf_conntrack_buckets = 16384 +[root@server ~]# cat /proc/sys/net/netfilter/nf_conntrack_max +65536 +[root@server ~]# cat /proc/sys/net/netfilter/nf_conntrack_buckets +16384 +``` + +查看当前数 + +```bash +[root@server ~]# cat /proc/net/nf_conntrack +ipv4 2 udp 17 10 src=172.16.0.39 dst=172.16.0.255 sport=43918 dport=21027 [UNREPLIED] src=172.16.0.255 dst=172.16.0.39 sport=21027 dport=43918 mark=0 secmark=0 use=2 +ipv4 2 tcp 6 431999 ESTABLISHED src=172.16.0.99 dst=172.16.0.39 sport=22 dport=42446 src=172.16.0.39 dst=172.16.0.99 sport=42446 dport=22 [ASSURED] mark=0 secmark=0 use=2 +ipv4 2 tcp 6 431432 ESTABLISHED src=172.16.0.39 dst=172.16.0.99 sport=42090 dport=22 src=172.16.0.99 dst=172.16.0.39 sport=22 dport=42090 [ASSURED] mark=0 secmark=0 use=2 +ipv4 2 tcp 6 431281 ESTABLISHED src=172.16.0.39 dst=172.16.0.99 sport=42450 dport=22 src=172.16.0.99 dst=172.16.0.39 sport=22 dport=42450 [ASSURED] mark=0 secmark=0 use=2 +ipv4 2 udp 17 14 src=0.0.0.0 dst=255.255.255.255 sport=68 dport=67 [UNREPLIED] src=255.255.255.255 dst=0.0.0.0 sport=67 dport=68 mark=0 secmark=0 use=2 + +[root@server ~]# wc -l /proc/net/nf_conntrack +6 /proc/net/nf_conntrack +``` + +### 调整内核参数 + +主要设置项: + +- 哈希表扩容(`nf_conntrack_buckets`、`nf_conntrack_max`) +- 让里面的元素尽快释放(超时相关参数) + +#### `nf_conntrack_buckets` 和 `nf_conntrack_max` 的默认值怎么来的 + +根据这篇 08 年的 [wiki](https://wiki.khnet.info/index.php/Conntrack_tuning),`nf_conntrack_max` 的默认值算法为: + +``` +CONNTRACK_MAX = RAMSIZE (in bytes) / 16384 / (ARCH / 32) +``` + +- 其中 `ARCH` 为 CPU 架构,值为 32 或 64。 +- 即:32 位系统使用内存的 1/16384,64 位系统再减半。 +- 对于 64 位 8G 内存的机器:`(8 * 1024^3) / 16384 / (64 / 32) = 262144` + +`nf_conntrack_buckets` 默认值算法为: + +``` +HASHSIZE = CONNTRACK_MAX / 4 +# 比较早的版本是除以 8 +# 这里的 4 或 8 就是每个桶里的链表最大长度 +``` + +- 对于 64 位 8G 内存的机器:`262144 / 4 = 65536` + +#### 给哈希表扩容的影响 + +主要是内存使用增加。32 位系统还要关心内核态的地址空间够不够。 + +netfilter 的哈希表存储在内核态的内存空间,这部分内存不能 swap,操作系统为了兼容 32 位,默认值往往比较保守。 + +- 32 位系统的虚拟地址空间最多 4G,其中内核态最多 1G,通常能用的只有前 896M。 + - 给 netfilter 分配太多地址空间可能会导致其他内核进程不够分配。1 条跟踪记录 300 字节左右,因此当年 `nf_conntrack_max` 默认 65535 条,占 20 多 MB。 +- 64 位系统的虚拟地址空间有 256TB,内核态能用一半,只需要关心物理内存的使用情况。 + +计算内存使用的公式还是来自上面的 wiki: + +``` +size_of_mem_used_by_conntrack (in bytes) = CONNTRACK_MAX * sizeof(struct ip_conntrack) + HASHSIZE * sizeof(struct list_head) +``` + +- ``` + sizeof(struct ip_conntrack) + ``` + + 在不同架构、内核版本、编译选项下不一样。这里按 352 字节算。 + + - 老文章说模块启动时会在 syslog 里打印这个值,但现在没有。 + +- `sizeof(struct list_head) = 2 * size_of_a_pointer`(32 位系统的指针大小是 4 字节,64 位是 8 字节) + +- 64 位系统,8G 内存的机器,按默认 `CONNTRACK_MAX` 为 262144,`HASHSIZE` 为 65536 时:`262144 * 352 + 65536 * 8 = 92798976`(88.5 MB) + +互联网公司的服务器通常内存没那么紧张,可以放开点: + +- `CONNTRACK_MAX` 为 1048576,`HASHSIZE` 为 262144 :`1048576 * 352 + 262144 * 8 = 371195904`(354 MB) + +## 测试 + +### 1.开启http服务 + +测试服务器地址为:172.16.0.99 + +操作系统:rocky42(20181101) + +``` +##python3 默认开启8000端口 +python -m http.server +##python2 +python -m SimpleHTTPServer +``` + +## 2.安装webbench + +1. 下载[github](https://github.com/EZLippi/WebBench) +2. 编译安装 + +``` +sudo make && sudo make install PREFIX=your_path_to_webbench +``` + +## 3.测试 + +``` +webbench -c 5000 -t 120 http://172.16.0.99:8000/ + +-c 并法数 +-t 时间 +``` + + +