独悲需要忍受,快乐需要分享
对Linux协议栈多次perf的结果,我无法忍受conntrack的性能,然而它的功能是如此强大,以至于我无法对其割舍,我想自己实现一个快速流表,但是我不得不抛弃依赖于conntrack的诸多功能,比如state match,Linux NAT等,诚然,我虽然对NAT也是抱怨太多,但不管怎样,不是还有很多人在用它吗。
       曾经,我针对conntrack查找做过一个基于离线统计的优化,其思路很简单,就使用动态的计算模式代替统一的hash算法。我事先会对经过该BOX的所有五元组进行采样记录,然后离线分析这些数据,比如将五元组拼接成一个32源IP地址+32位目标IP地址+8位协议号+16位源端口+16位目标端口的104位的长串(在我的实现中,我忽略了源端口,因为它是一个易变量,值得被我任性地忽略),然后根据hash桶的大小,比如说是N,以logN位为一个窗口大小在104位的串上滑动,找出相异数量最大的区间,以此区间为模区间,这样就可以将数据流均匀分布在各个hash桶中,如果数据流过多导致冲突链表过长,可以建立多维嵌套hash,把这个hash表倒过来看,它是多么像一棵平衡N叉树啊,N叉Trie树不就是这回事吗?这里的hash函数就是“取某些位”,这又一次展示了Trie与hash的统一。
       以上的优化虽然优美,但是却还是复杂了,这个优化思路是我从硬件cache的设计思路中借鉴的。但是和硬件cache比如CPU cache相比,软件的类似方式效果大打折扣,原因在于软件处理hash冲突的时候只能遍历或者查找,而硬件却可以同时进行。请学校里面的不要认为是算法不够优越,这是物理本质决定的。硬件使用的是门电路,流动的是电流,而电流是像水流一样并行连通的,软件使用的逻辑,流动的是步骤,这就是算法,算法就是一系列的逻辑步骤的组合,当然,也有很多复杂的所谓并行算法,但是据我所知,很多效果并不好,复杂带来了更多的复杂,最终经不起继续复杂,只好作罢,另外,这么简单个事儿,搞复杂算法有点大炮打苍蝇了。

nf_conntrack的简单优化-增加一个cache

如果什么东西和后续的处理速率不匹配,成为了瓶颈,那么就增加一个cache来平滑这种差异。CPU cache就是利用了这个思路。对于nf_conntrack的效率问题,我们也应该使用相同的思路。但是具体怎么做,还是需要起码的一些哪怕是定性的分析。
       如果你用tcpdump抓包,就会发现,结果几乎总是一连串连续被抓取的数据包属于同一个五元组数据流,但是也不绝对,有时会有一个数据包插入到一个流中,一个很合理的抓包结果可能是下面这个样子:
数据流a 正方向
数据流a 正方向
数据流a 反方向
数据流a 正方向
数据流c 正方向
数据流a 反方向
数据流a 发方向
数据流b 反方向
数据流b 正方向
数据流 正方向
....
看出规律了吗?数据包到达BOX遵循非严格意义上的时间局部性,也就是属于一个流的数据包会持续到达。至于空间局部性,很多人都说不明显,但是如果你仔细分析数据流a,b,c,d...的源/目标IP元组,你会发现它们的空间局部性,这是TCAM硬件转发表设计的根本原则。TCAM中“取某些位”中“某些位”说明这些位是空间上最分散的局部,这是一种对空间局部性的逆向运用,比如核心传输网上,你会发现大量的IP都是去往北美或者北欧的。
       我本希望在本文中用数学和统计学来阐述这一规律,但是这个行为实在不适合在一篇大众博客中进行,当有人面试我的时候问到我这个问题,我也只能匆匆几句话带过,然后如果需要,我会用电邮的方式来深入解析,但是对于一篇博客,这种方式显得卖弄了,而且会失去很多读者,自然也就没有人为我提意见了。博客中最重要的就是快速给出结果,也就是该怎么做。言归正传。
       如果说上述基于“空间局部性逆向利用”的“取某些位hash”的优化是原自“效率来自规则”这个定律的话,那么规则的代价就是复杂化,这个复杂化让我无法继续。还有一个比这个定律更加普适的原则就是“效率来自简单”,我喜欢简单的东西和简单的人,这次,我再次证明了我的正确。在继续之前,我会先简单描述一下nf_conntrack的瓶颈到底在哪。
1.nf_conntrack的正反向tuple使用一个hash表,插入,删除,修改操作需要全局的lock进行保护,这会赞成大量的串行化操作。
2.nf_conntrack的hash表使用了jhash算法,这种算法操作步骤太多,如果conntrack数量少,hash操作将会消耗巨大的性能。
[Tips:如果你了解密码学中的DES/AES等对称加密算法,就会明白,替换,倒置,异或操作可数据完成最佳混淆,使得输出与输出无关,从而达到最佳散列,然而这效果的代价就是操作复杂化了,加解密效率问题多在此,这种操作是如此规则(各种盒)以至于完全可以用硬件电路实现,可是如果没有这种硬件使用CPU的话,这种操作是极其消耗CPU的,jhash也是如此,虽然不很。]
3.nf_conntrack表在多个CPU间是全局的,这会涉及到数据同步的问题,虽然可以通过RCU最大限度缓解,但万一有人写它们呢。

鉴于以上,逐步击破,解决方案就有了。

1.cache的构建基于每CPU一个,这完全符合cache的本地化设计原则,CPU cache不也如此吗。
2.cache尽可能小,保存最有可能命中的数据流项,同时保证cache缺失的代价不至于过大。

3.建立一个合理的cache替换自适应原则,保证在位者谋其职,不思进取者自退位的原则


我的设计思路就是以上这些,在逐步落实的过程中,我起初只保留了一个cache项,也就是最后一次在conntrack hash表中被找到的那个项,这完全符合时间局部性,然而在我测试的时候发现,如果网络中有诸如ICMP这类慢速流的话,cache抖动会非常厉害,和TCP流比起来,ICMP太慢,但是按照排队原则,它终究会插队到一个TCP流中间,造成cache替换,为了避免这种令人悲哀的情况,我为conntrack项,即conn结构体加入了时间戳字段,每次hash查找到的时候,用当时的jiffers减去该时间戳字段,同时更新这个字段为当前jiffers,只有当这个差值小于一个预定值的时候,才会执行cache替换,这个值可以通过网络带宽加权获得。
       但是这样就完美了吗?远不!考虑到CPU cache的设计,我发现conntrack cache完全不同,对于CPU,由于虚拟内存机制,cache里面保存的肯定来自同一个进程的地址空间(不考虑更复杂的CPU cache原理...),因此除非发生分支跳转或者函数调用,时间局部性是一定的。但是对于网络数据包,完全是排队论统计决定的,所有的数据包的命名空间就是全世界的IP地址集合,指不定哪一会儿就会有任意流的数据包插入进来。最常见的一种情况就是数据流切换,比如数据流a和数据流b的发送速率,经过的网络带宽实力相当,它们很有可能交替到达,或者间隔两三个数据包交替到达,这种情况下,你要照顾谁呢?这就是第三个原则:效率来自公平。

因此,我的最终设计是以下的样子:

1.cache是一个链表,该链表的长度是一个值得微调的参数
cache链表太短:流项频繁在conntrack hash表和cache中跳动被替换。
cache链表太长:对待无法命中cache的流项,cache缺失代价太高。
胜者原则:胜者通吃。 凡有的,还要加给他叫他多余。没有的,连他所有的也要夺过来。(《马太福音》)均衡原则1-针对胜者:遍历cache链表的时间不能比标准hash计算+遍历冲突链表的时间更长(平均情况)。
均衡原则2-针对败者:如果遍历了链表没有命中,虽然损失了些不该损失的时间,但是把这种损失维持在一个可以接受的范围内。

效果:数据流到达速率越快就越容易以极低的代价命中cache,数据流达到速率越慢越不容易命中cache,然而也不用付出高昂的代价。

2.基于时间戳的cache替换原则
只有连续的数据包到达时间间隔小于某个动态计算好的值的时候,才会执行cache替换。

我的中间步骤测试代码如下:

//修改net/netfilter/nf_conntrack_core.c
//Email:marywangran@126.com//1.定义
#define A
#ifdef A
/** MAX_CACHE动态计算原则:* cache链表长度 = 平均冲突链表长度/3, 其中:* 平均冲突链表长度 = net.nf_conntrack_max/net.netfilter.nf_conntrack_buckets* 3 = 经验值**/
#define MAX_CACHE   4struct conntrack_cache {struct nf_conntrack_tuple_hash *caches[MAX_CACHE];
};DEFINE_PER_CPU(struct conntrack_cache, conntrack_cache);#endif//2.修改resolve_normal_ct
static inline struct nf_conn *
resolve_normal_ct(struct net *net,struct sk_buff *skb,unsigned int dataoff,u_int16_t l3num,u_int8_t protonum,struct nf_conntrack_l3proto *l3proto,struct nf_conntrack_l4proto *l4proto,int *set_reply,enum ip_conntrack_info *ctinfo)
{struct nf_conntrack_tuple tuple;struct nf_conntrack_tuple_hash *h;struct nf_conn *ct;
#ifdef Aint i;struct conntrack_cache *cache;
#endifif (!nf_ct_get_tuple(skb, skb_network_offset(skb),dataoff, l3num, protonum, &tuple, l3proto,l4proto)) {pr_debug("resolve_normal_ct: Can't get tuple\n");return NULL;}#ifdef Acache = &__get_cpu_var(conntrack_cache);rcu_read_lock();if (0 /* 优化3 */) {goto slowpath;}for (i = 0; i < MAX_CACHE; i++) {struct nf_conntrack_tuple_hash  *ch = cache->caches[i];struct nf_conntrack_tuple_hash  *ch0 = cache->caches[0];if (ch && nf_ct_tuple_equal(&tuple, &ch->tuple)) {ct = nf_ct_tuplehash_to_ctrack(ch);if (unlikely(nf_ct_is_dying(ct) ||!atomic_inc_not_zero(&ct->ct_general.use))) {h = NULL;goto slowpath;}else {if (unlikely(!nf_ct_tuple_equal(&tuple, &ch->tuple))) {nf_ct_put(ct);h = NULL;goto slowpath;}}
/*************************************** 优化1简介 *****************************************/
/* 并非直接提升到第一个,而是根据两次cache命中的间隔酌情提升,提升的步数与时间间隔成反比   */
/* 这就避免了cache队列本身的剧烈抖动。事实上,命中的时间间隔如果能加权历史间隔值,效果更好 */
/*******************************************************************************************//** 基于时间局部性提升命中项的优先级*/if (i > 0 /* && 优化1 */) {cache->caches[0] = ch;cache->caches[i] = ch0;}h = ch;}}ct = NULL;slowpath:rcu_read_unlock();if (!h)
#endif/* look for tuple match */h = nf_conntrack_find_get(net, &tuple);if (!h) {h = init_conntrack(net, &tuple, l3proto, l4proto, skb, dataoff);if (!h)return NULL;if (IS_ERR(h))return (void *)h;}
#ifdef Aelse {int j;struct nf_conn *ctp;struct nf_conntrack_tuple_hash  *chp;/*********************** 优化2简介 **************************/
/* 只有连续两个数据包到达的时间间隔小于n时才会执行cache替换 */
/* 这是为了避免诸如ICMP之类的慢速流导致的cache抖动          */
/************************************************************/if (0 /* 优化2 */) {goto skip;}/************************** 优化3简介 *****************************/
/* 只有在总的conntrack数量大于hash bucket数量的4倍时才启用cache   */
/* 因为conntrack数量小的话,经过一次hash运算就可以一次定位,      */
/* 或者经过遍历很短的冲突链表即可定位,使用cache反而降低了性能    */
/******************************************************************/if (0 /* 优化3 */) {goto skip;}ct = nf_ct_tuplehash_to_ctrack(h);nf_conntrack_get(&ct->ct_general);chp = cache->caches[MAX_CACHE-1];for (j = MAX_CACHE-1; j > 0; j--) {cache->caches[j] = cache->caches[j-1];}cache->caches[0] = h;if (chp) {ctp = nf_ct_tuplehash_to_ctrack(chp);nf_conntrack_put(&ctp->ct_general);}}
skip:if (!ct) {ct = nf_ct_tuplehash_to_ctrack(h);}
#elsect = nf_ct_tuplehash_to_ctrack(h);
#endif/* It exists; we have (non-exclusive) reference. */if (NF_CT_DIRECTION(h) == IP_CT_DIR_REPLY) {*ctinfo = IP_CT_ESTABLISHED + IP_CT_IS_REPLY;/* Please set reply bit if this packet OK */*set_reply = 1;} else {/* Once we've had two way comms, always ESTABLISHED. */if (test_bit(IPS_SEEN_REPLY_BIT, &ct->status)) {pr_debug("nf_conntrack_in: normal packet for %p\n", ct);*ctinfo = IP_CT_ESTABLISHED;} else if (test_bit(IPS_EXPECTED_BIT, &ct->status)) {pr_debug("nf_conntrack_in: related packet for %p\n",ct);*ctinfo = IP_CT_RELATED;} else {pr_debug("nf_conntrack_in: new packet for %p\n", ct);*ctinfo = IP_CT_NEW;}*set_reply = 0;}skb->nfct = &ct->ct_general;skb->nfctinfo = *ctinfo;return ct;
}//2.修改nf_conntrack_init
int nf_conntrack_init(struct net *net)
{int ret;
#ifdef Aint i;
#endifif (net_eq(net, &init_net)) {ret = nf_conntrack_init_init_net();if (ret < 0)goto out_init_net;}ret = nf_conntrack_init_net(net);if (ret < 0)goto out_net;if (net_eq(net, &init_net)) {/* For use by REJECT target */rcu_assign_pointer(ip_ct_attach, nf_conntrack_attach);rcu_assign_pointer(nf_ct_destroy, destroy_conntrack);/* Howto get NAT offsets */rcu_assign_pointer(nf_ct_nat_offset, NULL);}
#ifdef A/* 初始化每CPU的conntrack cache队列 */for_each_possible_cpu(i) {int j;struct conntrack_cache *cache;cache = &per_cpu(conntrack_cache, i);for (j = 0; j < MAX_CACHE; j++) {cache->caches[j] = NULL;}}#endifreturn 0;out_net:if (net_eq(net, &init_net))nf_conntrack_cleanup_init_net();
out_init_net:return ret;
}

希望看到的人有机会测试一下。效果和疑问可以直接发送到代码注释中所示的邮箱。

一个Netfilter nf_conntrack流表查找的优化-为conntrack增加一个per cpu cache相关推荐

  1. 数据结构与算法(8-1)顺序表查找及优化

    目录 一.顺序表查找 二.顺序表查找优化(重点) 总代码 一.顺序表查找 从头到尾或从尾到头查找. //顺序表查找(需要判断两次) int ListSearch(char ch) {for (int ...

  2. 顺序表查找及其优化(Java)

    顺序表查找(线性查找): 1 private static void Ordersearch(int[] arr,int num) { 2 for (int i = 0; i < arr.len ...

  3. 一个站点不够学?那就在用Python增加一个采集目标,一派话题广场+某金融论坛话题广场爬虫

    本次的目标站点原计划是一个比较简单的站点,后来发现有点太简单了,就额外增加了一个案例,学一个赠一个,本篇博客核心用到的技术依旧是队列 queue 技术. 目标站点[一派话题广场]分析 本篇博客的第一个 ...

  4. OpenFlow协议Open Flow交换机跟流表(FlowTable)

    Open Flow协议.Open Flow交换机跟流表(FlowTable) 传统网络:传统网络的缺陷或者催生SDN的背景请参考链接https://www.cnblogs.com/031602523l ...

  5. Openlab实验平台实验--使用Postman下发流表

    任务目的 1.掌握OpenFlow流表相关知识,理解SDN网络中L2,L3,L4层流表的概念. 2.学习并熟练掌握Postman工具下发L2,L3,L4层流表. 任务环境 注:系统默认的账户为root ...

  6. ovs 流表version

    插入分类器的流表有一个version字段,类型为struct versions,用来标记此流表在哪个version添加到分类器,如果流表被删除后标记在哪个version被删除. typedef uin ...

  7. OpenDayLight+Mininet+Postman下发流表实验

    OpenDayLight+Mininet+Postman下发流表实验 VM实验环境 笔记本环境 Tips1 Tips2 任务目的 任务内容 实验原理 一. 流表结构 二. 匹配域解析流程 三. Ope ...

  8. 哈希表查找失败的平均查找长度_你还应该知道的哈希冲突解决策略

    本文首发于 vivo互联网技术 微信公众号 链接:https://mp.weixin.qq.com/s/5vxYoeARG1nC7Z0xTYXELA 作者:Xuegui Chen 哈希是一种通过对数据 ...

  9. Open vSwitch流表应用实战

    本文参考:Open vSwitch流表应用实战 一个通过改变流表下发而实现的互相通信实验. 实验目的: 掌握Open vSwitch下发流表操作: 掌握添加.删除流表命令以及设备通信的原理. 原理:. ...

最新文章

  1. Socket recv()之前进行select代码
  2. socket编程实现回声客户端
  3. 面试字节跳动Android工程师该怎么准备?深度解析,值得收藏
  4. 连接上linux上的ip在哪个文件夹,linux – 当IP别名时,操作系统如何确定哪个IP地址将用作出站TCP / IP连接的源?...
  5. 百度网盘空间调整:这类用户2TB变100GB!
  6. 大数据_Flink_数据处理_运行时架构2_作业提交流程_抽象架构---Flink工作笔记0017
  7. javascript获取随机rgb颜色和十六进制颜色的方法
  8. python 批量替换srt文本_Python 实战 | srt字幕文件转换txt文本文件
  9. 关于Keil4 C51版本可以编译但是无法完成编译的问题解决
  10. Microsoft Edge浏览器设置编码方式
  11. English--consonant_摩擦音
  12. JAVA 05 输入年份判断生肖
  13. 洪恩教育披露2020年报:学习服务收入大增,营销费、负债规模攀升
  14. javascript 实现table展开折叠
  15. vue2与vue3的实例销毁,有什么区别。
  16. 使用 acme.sh 签发 SSL证书失败
  17. 【html转pdf】html页面导出为pdf文件,纯html版本,简单实现pdf转换【html2canvas+jspdf】
  18. 全国离线地图矢量数据
  19. 腾讯8分钟产品课腾讯产品心法整理
  20. Django入门-6:视图-中间件、CSRF

热门文章

  1. 计算机存储单位换算规则
  2. javascript秘密花园
  3. tomcat 停止时提示警告信息WARNING: Problem with directory [/usr/share/tomcat8/shared]
  4. Redis 慢查询 命令 slowlog
  5. mysql源码剖析–通信协议分析
  6. ppt转换成word的几种方法
  7. PHP服务计时器,JS计时器
  8. C语言编程对一个逆波兰式进行求值,算式与逆波兰式
  9. Tigase8.1.2安装配置
  10. 【C语言】题集 of ⑤