Linux内核中conntrack模块使用哈希表来存储连接跟踪条目,当哈希表条目达到上限时,系统会将新分配conntrack条目的数据包DROP掉,从而导致网络受到影响。此时,日志中会记录:
1 | nf_conntrack: table full, dropping packet |
哈希表条目上限由参数net.netfilter.nf_conntrack_max设置。
网上文章对这个问题的解决方法往往是调大该参数。但在涉及多个network namespace的场景下,不能简单的这样做,还是要根据自身场景分析清楚具体原因。
根据CentOS7 3.10.0-957版本内核源码,实际上每个network namespace的conntrack哈希表是独立的。在表示network namespace的结构体net中的成员ct表示conntrack相关信息:
1 | struct net { |
netns_ct结构中保存有独立的哈希表相关信息:
1 | struct netns_ct { |
nf_conntrack模块的加载函数nf_conntrack_standalone_init注册了pernet操作:
1 | static struct pernet_operations nf_conntrack_net_ops = { |
在每个network namespace创建时会执行函数nf_conntrack_pernet_init, 该函数中会调用函数nf_conntrack_init_net创建该namespace独有的conntrack表:
1 | net->ct.slabname = kasprintf(GFP_KERNEL, "nf_conntrack_%p", net); |
在分配conntrack条目时,会调用函数__nf_conntrack_alloc函数:
1 | if (nf_conntrack_max && |
nf_conntrack_max参数对应sysctl参数:
1 | net.netfilter.nf_conntrack_max |
这里会比较namespace独有的conntrack条目数和nf_conntrack_max参数值,如果达到了上限值,则会执行early_drop或导致数据包被丢弃。也就是说nf_conntrack_max表示的实际是每个namespace中conntrack条目的上限。
nf_conntrack_max本身是一个全局变量,并没有实现不同namespace之间的隔离:
1 | static struct ctl_table nf_ct_sysctl_table[] = { |
也就是修改该参数会影响所有的namespace。
并且,nf_conntrack_max表示的是哈希表中的元素上限,它的默认取值是根据哈希表的bucket的平均长度来计算的:
1 | nf_conntrack_max = max_factor * nf_conntrack_htable_size; |
当把nf_conntrack_max调大时,会导致哈希表bucket的平均长度变大,从而导致哈希表查找性能下降。因而还要考虑同步调整bucket的个数。
bucket个数变量nf_conntrack_htable_size也是全局的,并没有namespace隔离,在CentOS7上不能由sysctl变量修改,只能从内核模块参数修改:
1 | module_param_call(hashsize, nf_conntrack_set_hashsize, param_get_uint, |
修改参数nf_conntrack_htable_size时,会调用函数nf_conntrack_set_hashsize:
1 | int nf_conntrack_set_hashsize(const char *val, struct kernel_param *kp) |
这个函数会根据新的bucket个数创建新的哈希表,并将原来的conntrack条目迁移到新的哈希表。但是,实现上只处理init_net的哈希表。
其他namespace的conntrack表并没有进行调整。
因而,即使调整了nf_conntrack_htable_size, 其他的非init_net的namespace中的conntrack哈希表也不会调整。从而导致bucket的平均长度变大,性能下降。
再来看CentOS8 4.18.0-348.7.1版本内核的情况。
社区版本的这个commit:
将系统上所有namespace的conntrack条目使用一个哈希表存储。CentOS8是包含这个提交。
这样能够解决上面说的非init_net中,修改nf_conntrack_htable_size参数而不重新构建哈希表的问题。但__nf_conntrack_alloc函数依然使用的是namespace的conntrack条目数与nf_conntrack_max进行比较进行上限判断:
1 | if (nf_conntrack_max && |
如果系统上有多个namespace, 每个namespace中的条目都很多,但并未超过nf_conntrack_max值限制,但实际上conntrack哈希表的平均bucket长度则会与namespace正相关,会非常长,导致conntrack表性能下降。
对于我们已知可以跳过连接跳踪的场景,可以通过跳过没有必要的连接跟踪逻辑,从而避免conntrack表条目达到上限。
在连接跟踪入口函数nf_conntrack_in中会根据sk_buff结构来判断是否需要连接跟踪:
1 | tmpl = nf_ct_get(skb, &ctinfo); |
因而我们可以在netfilter框架中注册函数,在nf_conntrack_in执行前通过修改数据包跳过连接跟踪:
1 | nf_ct_set(skb, NULL, IP_CT_UNTRACKED); |
参考: