解决恶心的 Nf_conntrack: Table Full 问题

相信有一定 Linux 服务器运维经验的人肯定见过这个问题:dmesg 或者 /var/log/messages 里大片地输出类似这样的日志:

1
2
Dec  8 11:22:29 product08 kernel: nf_conntrack: table full, dropping packet.
Dec  8 11:22:29 product08 kernel: nf_conntrack: table full, dropping packet.

同时服务器上的各种网络服务耗时大幅上升,各种 timed out,各种丢包,完全无法正常提供服务。

随着我们流量的提升,最近又开始被这个问题虐了。几个月前第一次遇到此问题的时候,我们使用了提高 nf_conntrack table size 的方法解决的,事实证明这是个治标不治本的方法,流量上来了,改得再大最后还是会爆掉。况且 table 太大,占用的内存也会很多。

今天再次研究了一下这个问题,换了另一种更好的方案,尝试解决掉它。简单地说,方案就是在 iptables raw 表里增加规则,将无需跟踪状态的包标记为 NOTRACK,这样就无需耗费 nf_conntrack table entry 来记录其状态了。

1. nf_conntrack 是干嘛的

nf_conntrack(在老版本的 Linux 内核中叫 ip_conntrack)是一个内核模块,用于跟踪一个连接的状态的。连接状态跟踪可以供其他模块使用,最常见的两个使用场景是 iptables 的 natstate 模块。

iptables 的 nat 通过规则来修改目的/源地址,但光修改地址不行,我们还需要能让回来的包能路由到最初的来源主机。这就需要借助 nf_conntrack 来找到原来那个连接的记录才行。

state 模块则是直接使用 nf_conntrack 里记录的连接的状态来匹配用户定义的相关规则。例如下面这条 INPUT 规则用于放行 80 端口上的状态为 NEW 的连接上的包。

1
iptables -A INPUT -p tcp -m state --state NEW -m tcp --dport 80 -j ACCEPT

2. 能否完全禁用 nf_conntrack

能否禁用 nf_conntrack 要看服务器干啥了,如果没有使用 nat 模块和 state 模块是可以禁用掉它的,这样做一劳永逸,再也不用被这个恶心的问题折磨了。

但是我发现 RHEL 默认的 iptables 规则里都是用到了 state 模块的:

1
2
3
4
5
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -p icmp -j ACCEPT
iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -m state --state NEW -m tcp -p tcp --dport 22 -j ACCEPT
iptables -A INPUT -j REJECT --reject-with icmp-host-prohibited

按理说打开端口的规则完全不需要使用 -m state --state NEW,只要匹配了目标端口直接放行就好。我理解这样做是为了优化性能。注意看第一条规则,只要连接状态是 ESTABLISHED 或者 RELATED 直接放行。这样做,只有连接初始化时的包需要经过下面的重重规则去一条条匹配,一旦建立上连接了(代表被放行了),后续的包直接通过第一条规则就放行了,省去了一条条匹配后续规则的麻烦(O(1)复杂度?)。

反之,如果我们不这么写,那连接的每个包都需要经过前面的层层规则过滤之后才能被放行,对性能有影响(O(n)复杂度?)。当然只有 iptables 规则很多的时候,这个问题才能突显,且到底能带来多大的影响,我手头没有测试数据,还不是特别清楚。

保险起见,我们就没有全部禁用掉 nf_conntrack,而是采用了 raw 表来部分地禁用连接跟踪。

3. 哪些连接可以不必跟踪状态

我们先看看怎么通过 raw 表针对部分连接禁用 conntrack。

1
2
3
4
# 针对进入本机的包
iptables -t raw -A PREROUTING -p tcp -m tcp --dport 8080 -j NOTRACK
# 针对从本机出去的包
iptables -t raw -A OUTPUT -p tcp -m tcp --dport 8080 -j NOTRACK

重点就是 -t raw-j NOTRACK,前者指定 table,后者指定 action 为 NOTRACK——不跟踪状态。

那么问题来了,要针对哪些连接应用此规则呢?需要 NAT 的肯定不行,各种 redirect 的连接本质上也是 NAT,也一样不行。除此之外的大部分的普通连接都是可以的。

比如所有 lo 接口上的连接:

1
2
iptables -t raw -A PREROUTING -i lo -j NOTRACK
iptables -t raw -A OUTPUT -o lo -j NOTRACK

在应用和 Nginx 都跑在一台服务器上的情况下,这能禁用掉很多连接的状态跟踪,因为在并发高的时候,Nginx 和 Upstream 的连接也不是个小数目。

再比如针对流量很大的特定端口:

1
2
iptables -t raw -A PREROUTING -p tcp -m tcp --dport 8080 -j NOTRACK
iptables -t raw -A OUTPUT -p tcp -m tcp --sport 8080 -j NOTRACK

需要特别特别注意的是,被 NOTRACK 的包无法匹配上依赖特定状态的其他规则。比如 INPUT 链上假如有这么一条规则:

1
iptables -A INPUT -p tcp -m state --state NEW -m tcp --dport 8080 -j ACCEPT

那么在增加了上面的 NOTRACK 规则后,你会发现这个端口的包都被丢掉了。因为没有了连接状态跟踪,那个 --state NEW 不可能匹配得上。上午我就因为犯了这个错误导致某关键服务宕机了几分钟……

所以要千万小心,一个很好的解决办法是加上这么一条规则(你想 NOTRACK 的,肯定是你要放行的):

1
iptables -A INPUT -m state --state UNTRACKED -j ACCEPT

我们目前使用这种方案解决了目前的 nf_conntrack: table full 问题,只是在流量最大的端口以及 lo 上启用这些规则就已经让该错误没再出现了(在并发没有降低的情况下),再仔细优化下,把更多的端口加进来应该能解决绝大多数情况下的问题了。

对专业运维来说,估计本文说的都不是事,但希望我这半吊子运维的这篇文章能帮助到那些同样要兼职运维工作的苦逼码农们。

Have fun :–)

各种链接

Comments