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); |
参考: