nfconntrack全局锁的优化

2019-04-10 15:03:15 浏览数 (2)

nfconntrack是netfilter中的重要模块,很多netfilter的功能都依赖于这个模块,如NAT等。而利用linux来构建的网络设备,可以说,其80%的功能都依赖于nfconntrack实现的会话管理。所以,会话管理的性能优劣会对网络设备的性能产生直接的影响。

在网络设备中,对性能影响比较显著的就是锁的使用。可以说,网络设备的性能优化,很大程度上都是对于锁的优化。

nfconntrack中一个最重要的锁,就是全局的nf_conntrack_lock。这个锁的作用是保护全局会话表。当CPU尝试用当前数据包skb进行会话匹配,或者准备插入新的会话时,都需要对nf_conntrack_lock进行上锁。试想,在大流量的环境中,每个经过设备的数据包都要进行会话匹配,在多核的情况下,对这个锁的竞争是非常激烈的。

这是commiter做这个优化时,使用perf对内核进行的profile,可以清楚的看到对这个锁的spin_lock占用了大量的CPU,是性能的主要瓶颈。 —— 笔者以前从事的就是网络设备的开发工作,之前一直没有想到nfconntrack居然使用了这么长时间的一个全局锁。对于设备厂商来说,这个是早就应该进行的优化。

当确定nf_conntrack_lock全局锁为性能瓶颈时,我们应该怎样优化呢?这个问题可以一般化为,如何优化一个锁?最理想的情况,就是去掉这个锁。实现这个目的,一般可以使用空间换时间,或者使用无锁算法。对于会话表来说,锁是充分且必要的。因为必须要保证会话在会话表中的唯一性,查询和插入必须是原子的,不可中断的。因此必须使用锁来保证安全。第二条路,就是减小锁的粒度。这个也是这个优化的主要思想。

netfilter的会话,被分为两个部分,一个是original tuple,另外一个是reply tuple,两个tuple分别要插入两个不同的hash表中,用于两个方向上的匹配。现在要减小锁的粒度,最直观的想法是,将以前的全局锁,变成基于桶的锁,一个hash桶就使用一个锁。这个想法,从思路上是没有问题的,但是对于会话表来说,其桶的个数一般很大。

通过上面的命令,可以得到linux默认的nfconntrack的桶的个数。对于网络设备来说,有时候还要加大桶的个数。如果每个桶一个锁的话,会消耗掉不少内存。

这个commit采用了一个折中的办法。每个锁负责保障多个桶的安全,这样就不需要一个桶对应一个锁了,这样既提高了并发性,又没有增加太多内存消耗。

其定义了一个nf_contrack_locks数组,用户可以根据自己的环境,定义不同的CONNTRACK_LOCKS大小。

当使用多个锁来保护会话表后,就容易引入deadlock问题。试想,如果有两把锁A和B,分别用于保护不同的hash表的桶的集合。假设有两个tuple,它们被插入到两个不同的hash表的桶中。其中original tuple所在的桶,由锁A保护,reply tuple所在的桶,由锁B保护。如果两个方向的数据包同时到达两个不同的CPU1和2,则CPU1已经获得了锁A,并尝试获得锁B,而CPU已经获得锁B,并尝试获得锁A。这时,两个CPU都拿到一个锁,并尝试获得对方的锁,但肯定又无法拿到,于是这两个CPU就死锁了,系统也就hang住了。这个问题是在多锁环境下,经常遇到的问题。解决的方法就是:一定要保证上锁的顺序,并保持一致。

下面看看这个commit是如何解决这个问题的。

  1. 创建了一个新的函数nf_conntrack_double_lock用于封装上锁动作,保证所有代码使用一致的策略。
  2. 对于两个锁,每次上锁的时候,都是先获取索引较小的锁,再获取索引较大的锁。
  3. 判断两个锁的索引是否一致,避免对同一个锁进行两次上锁的操作。

通过上面3个技巧,就保证了新的nf_conntrack_locks,不会产生死锁。

对于nf_conntrack_lock全局锁优化的commit是93bb0ceb75be,感兴趣的同学可以自己阅读这个patch。没记错的话,这个commit是在3.18内核版本中引入的。—— 真没想到,这么晚才有人优化这个锁:(

PS:后面会继续分析对nfcontrack中锁的优化。

0 人点赞