Keep learning, keep living...

0%

关于参数net.netfilter.nf_conntrack_max

Linux内核中conntrack模块使用哈希表来存储连接跟踪条目,当哈希表条目达到上限时,系统会将新分配conntrack条目的数据包DROP掉,从而导致网络受到影响。此时,日志中会记录:

1
nf_conntrack: table full, dropping packet

哈希表条目上限由参数net.netfilter.nf_conntrack_max设置。

网上文章对这个问题的解决方法往往是调大该参数。但在涉及多个network namespace的场景下,不能简单的这样做,还是要根据自身场景分析清楚具体原因。

根据CentOS7 3.10.0-957版本内核源码,实际上每个network namespaceconntrack哈希表是独立的。在表示network namespace的结构体net中的成员ct表示conntrack相关信息:

1
2
3
4
5
struct net {
...
struct netns_ct ct;
...
}

netns_ct结构中保存有独立的哈希表相关信息:

1
2
3
4
5
6
7
8
9
struct netns_ct {
atomic_t count;
...
unsigned int htable_size;
RH_KABI_DEPRECATE(seqcount_t, generation)
struct kmem_cache *nf_conntrack_cachep;
struct hlist_nulls_head *hash;
...
}

nf_conntrack模块的加载函数nf_conntrack_standalone_init注册了pernet操作:

1
2
3
4
static struct pernet_operations nf_conntrack_net_ops = {
.init = nf_conntrack_pernet_init,
.exit_batch = nf_conntrack_pernet_exit,
};

在每个network namespace创建时会执行函数nf_conntrack_pernet_init, 该函数中会调用函数nf_conntrack_init_net创建该namespace独有的conntrack表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
net->ct.slabname = kasprintf(GFP_KERNEL, "nf_conntrack_%p", net);
if (!net->ct.slabname)
goto err_slabname;

net->ct.nf_conntrack_cachep = kmem_cache_create(net->ct.slabname,
sizeof(struct nf_conn), 0,
SLAB_DESTROY_BY_RCU, NULL);
if (!net->ct.nf_conntrack_cachep) {
printk(KERN_ERR "Unable to create nf_conn slab cache\n");
goto err_cache;
}

net->ct.htable_size = nf_conntrack_htable_size;
net->ct.hash = nf_ct_alloc_hashtable(&net->ct.htable_size, 1);
if (!net->ct.hash) {
printk(KERN_ERR "Unable to create nf_conntrack_hash\n");
goto err_hash;
}

在分配conntrack条目时,会调用函数__nf_conntrack_alloc函数:

1
2
3
4
5
6
7
8
if (nf_conntrack_max &&
unlikely(atomic_read(&net->ct.count) > nf_conntrack_max)) {
if (!early_drop(net, hash)) {
atomic_dec(&net->ct.count);
net_warn_ratelimited("nf_conntrack: table full, dropping packet\n");
return ERR_PTR(-ENOMEM);
}
}

nf_conntrack_max参数对应sysctl参数:

1
net.netfilter.nf_conntrack_max

这里会比较namespace独有的conntrack条目数和nf_conntrack_max参数值,如果达到了上限值,则会执行early_drop或导致数据包被丢弃。也就是说nf_conntrack_max表示的实际是每个namespaceconntrack条目的上限。

nf_conntrack_max本身是一个全局变量,并没有实现不同namespace之间的隔离:

1
2
3
4
5
6
7
8
9
static struct ctl_table nf_ct_sysctl_table[] = {
{
.procname = "nf_conntrack_max",
.data = &nf_conntrack_max,
.maxlen = sizeof(int),
.mode = 0644,
.proc_handler = proc_dointvec,
},
...

也就是修改该参数会影响所有的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
2
module_param_call(hashsize, nf_conntrack_set_hashsize, param_get_uint,
&nf_conntrack_htable_size, 0600);

修改参数nf_conntrack_htable_size时,会调用函数nf_conntrack_set_hashsize:

1
2
3
4
5
6
7
8
9
10
int nf_conntrack_set_hashsize(const char *val, struct kernel_param *kp)
{
int i, bucket, rc;
unsigned int hashsize, old_size;
struct hlist_nulls_head *hash, *old_hash;
struct nf_conntrack_tuple_hash *h;
struct nf_conn *ct;

if (current->nsproxy->net_ns != &init_net)
return -EOPNOTSUPP;

这个函数会根据新的bucket个数创建新的哈希表,并将原来的conntrack条目迁移到新的哈希表。但是,实现上只处理init_net的哈希表。

其他namespaceconntrack表并没有进行调整。

因而,即使调整了nf_conntrack_htable_size, 其他的非init_netnamespace中的conntrack哈希表也不会调整。从而导致bucket的平均长度变大,性能下降。

再来看CentOS8 4.18.0-348.7.1版本内核的情况。

社区版本的这个commit:

将系统上所有namespaceconntrack条目使用一个哈希表存储。CentOS8是包含这个提交。

这样能够解决上面说的非init_net中,修改nf_conntrack_htable_size参数而不重新构建哈希表的问题。但__nf_conntrack_alloc函数依然使用的是namespaceconntrack条目数与nf_conntrack_max进行比较进行上限判断:

1
2
3
4
5
6
7
8
9
10
if (nf_conntrack_max &&
unlikely(atomic_read(&net->ct.count) > nf_conntrack_max)) {
if (!early_drop(net, hash)) {
if (!conntrack_gc_work.early_drop)
conntrack_gc_work.early_drop = true;
atomic_dec(&net->ct.count);
net_warn_ratelimited("nf_conntrack: table full, dropping packet\n");
return ERR_PTR(-ENOMEM);
}
}

如果系统上有多个namespace, 每个namespace中的条目都很多,但并未超过nf_conntrack_max值限制,但实际上conntrack哈希表的平均bucket长度则会与namespace正相关,会非常长,导致conntrack表性能下降。

对于我们已知可以跳过连接跳踪的场景,可以通过跳过没有必要的连接跟踪逻辑,从而避免conntrack表条目达到上限。

在连接跟踪入口函数nf_conntrack_in中会根据sk_buff结构来判断是否需要连接跟踪:

1
2
3
4
5
6
7
8
tmpl = nf_ct_get(skb, &ctinfo);
if (tmpl || ctinfo == IP_CT_UNTRACKED) {
/* Previously seen (loopback or untracked)? Ignore. */
if ((tmpl && !nf_ct_is_template(tmpl)) ||
ctinfo == IP_CT_UNTRACKED)
return NF_ACCEPT;
skb->_nfct = 0;
}

因而我们可以在netfilter框架中注册函数,在nf_conntrack_in执行前通过修改数据包跳过连接跟踪:

1
nf_ct_set(skb, NULL, IP_CT_UNTRACKED);

参考: