协议栈处理中的conntrack HASH查找/Bloom过滤/CACHE查找/大包与小包/分层处理风格
1.路由CACHE的优势与劣势
分级存储体系已经存在好多年了。其精髓在于“将最快的存储器最小化。将最慢的存储器最大化”,这样的结果就使资源利用率的最大化。既提高了訪问效率,又节省了资源。这是全部的CACHE设计的基本原则。对于内存訪问,差点儿全部的CPU都内置了一级cache。二级cache,亲和力好的几个核心甚至设计了三级cache乃至四级cache,然后才是物理内存,然后是经过精密优化的磁盘交换分区,最后是远程的存储器。这些存储空间逐级变大,訪问开销也逐级变大,构成了一个金字塔型的存储体系。如此的设计要想获得最大的功效。其主要是利用了局部性原则,往大了说就是自然界普遍的列维飞行原则。这个原则不但在内存訪问上有效,并且人类的文明就是这样的列维飞行原则构建起来的,我不想从南方古猿说到最后的北美移民潮以及西进运动,因此本段就此打住。
推而广之。协议栈的路由表就是为了查找而生,且不说Cisco的Express Forward(CEF体系),单说路由cache就够了。
早期的Linux(迄至2.6.39以往。不谈3.X),在系统路由表至上又构建了一个路由cache,每个cache表项都是一次成功的路由查询结果,内置一个超时过期时间。Linux路由cache设计的前提是:去往一个目标地址的数据包会连续到来。
这也是局部性原理的一种推广。可是和内存訪问以及单一目的地的移民浪潮不同,Linux作为一台路由器并非为一个数据流服务的,而Linux的路由cache中保存的是SourceIP/DestinationIP值对,因此一条路由表中的项将会扩展出N条路由cache项:
1.1.1.0/24-192.168.1.254==>{(2.2.2.1,1.1.1.1,192.168.1.254),(2.2.2.2,1.1.1.1,192.168.1.254),(2.2.2.3,1.1.1.2,192.168.1.254),(2.2.2.3,1.1.1.10,192.168.1.254)...}
假设同一时候过境的基于源IP/目标IP的二元组数据巨大的话(特别是在骨干路由器场景下),路由cache的数目将远远超过路由表项的数目,我在想。路由cache相较于路由表的查询优势体如今哪里呢?难道会有如此高效的路由cache查询算法吗?假设有的话,为何不直接应用于最长前缀匹配的路由表查找呢?非常明白的是,路由表查找远远没有路由cache查找更严格。
路由cache的思想是好的,可是确实不适合软件实现,假设有TCAM硬件的话,也许能够实现并行的多维向量精确匹配,但对于软件实现,因为协议栈天生的多CPU核心利用的弊端,最高效的做法就是分级hash了,而即便是最高效的多级hash实现也不是免费的午餐。假设考虑到异常流量。非常easy将你的路由cache表撑到爆,比方构造海量不同的源IP地址訪问不同的目标IP地址就可以。
TCAM的原理就不讲了,网上资料多的是。
2.conntrack的hash查找
Netfilter中有一个conntrack模块。它能够建立随意五元组的连接跟踪。可是假设来了一个数据包,就必须进行一次查找。以便将它关联到某一个连接跟踪上。这个查找的效率高低,直接影响了这台设备的品质。查找算法非常简单,首先依据五元组计算一个hash。然后在该hash值定为的冲突链表上遍历,直到找到一个精确匹配的,假设没有则新建。谈到hash。假设只从理论上看,它是一个极其高效的时间/空间相平衡的算法,不考虑空间浪费,它能够经过常数的时间找到结果,还有一个极端,它却退化成了一个单纯的链表。查找时间复杂度和遍历一致。真实的hash算法的时间复杂度介于二者之间。因此Linux在实现conntrack的时候。本应该像Trie路由树那样。动态调整hashsize的大小。即进行适当的rehash操作,以将每条冲突链表上的元素的平均数量保持在一个确定的范围内。这样就能够保持良好的伸缩性和可扩展性,而你的代价不过消耗了内存。
可是Linux似乎没有这么做。
屡次想优化nf_conntrack的查找性能,但总是受制于以下一个理想。即“理想情况下。conntrack的查找只须要做一次hash计算。然后遍历一个元素或者个位数个元素的链表”,假设实现了这个理想。还用费劲优化吗?我假设冲突链表的平均元素个数为10。那么只要10000个hash桶,就能容纳10*10000个连接,效率已经非常高了。可是理想毕竟不过理想。hash值计算的输入是五元组,算法是固定的。因此假设这个hash算法不够好的话,计算结果的散列程度和输入是高度相关的,理论和事实均证明。涉及到查找的hash算法都无法做到理想情况,要想使得hash的输出和输入无关,就要採用对称密钥学中的操作概念:置换,替换,混淆,扩散等,能够參考一下DES/AES的操作步骤,就能够看到这依旧不是一顿免费的午餐!
hash查找须要的是高效,假设一个算法针对输入的散列程度已经足够好了。那就到此为止了。假设散列程度非常不好。那么冲突链表的长度就会有的特别长有的特别短,这对于一个公平调度系统,对性能的影响是非常大的。
3.基于Bloom过滤的转发表
假设说Linux的路由cache必定下课,那么总得有个替代方案了。其实,我并不仅针对Linux,而是在讨论一般场景。只要是软件实现的路由cache,必定要考虑cache查找的效率问题。cache只在条目数目固定且数目非常小的前提下。才会体现出对慢速路径的优势,否则剩下的就不过思想了。假设扯到硬件,那怎么折腾都是OK的,可是此时我肯定不谈硬件。
我希望使用纯软件来优化转发效率。
记住一件事是重要的,那就是:你不能将总体的每个部分的性能都提升,正如永动机不可能一样,你必须将不重要的部分能力倾注到须要优化的部分。
优化的基准就是,区分一个高速路经和慢速路径。首先查找高速路经,若失败则进入慢速路径查询。同一时候将结果添加高速路经。高速路经非常快,前提是它是排它的,容量小且有限的,就像罗马共和国时期的公民权一样。是要靠奋斗而取得的,到了卡拉卡拉时期。公民权成了既得权,优势自然就没了。使用Bloom能够做到传统路由cache的高效替代方案。
对每个下一跳维护一个固定大小(可是执行期能够动态调整)的Bloom过滤器,数据包到来,首先遍历全部下一跳的Bloom过滤器,结果无非有三种:
a.唯独一个Bloom过滤器返回值为1;
b.有多个Bloom过滤器的返回值为1。
c.没有一个Bloom过滤器的返回值为1;
对于a而言,直接发往那个下一跳就可以。对于b而言,说明存在False Positive。此时说明凡是返回1的Bloom过滤器都有可能指示正确的结果,要么退回慢速路径,要么在全部可能的结果之间广播,对于c而言。此时必须退回到慢速路径。
请注意,我们当然希望结果是a。此时的计算非常之快,N个hash计算就可以,至于计算方式,能够在一个处理器串行,也能够多个处理器并行。
对于Bloom过滤器而言。为了方便删除元素,故使用带有counter的int型占位而不是不过一个表示0和1的bit位。为了防止导致False Positive的结果一直会False Positive,全部的Bloom过滤器在内存中都要有一个后备存储。保存元素的链表,每间隔一段时间,在后台更新hash算法,进行Bloom的update操作。
我并不赞成再引入新的层次。比方再引入一个cache层。因为那会添加维护量。须要进行复杂性管理。解铃还需系铃人,既然为了避免同一IP地址对的计算结果同样,那就改变算法而不是引入新层,在同一层次。代偿是不必要的。
总而言之,Bloom过滤器的hash算法一定要保持静止,所占空间一定要小巧。
4.大包还是小包
OpenVPN在虚拟网卡模拟了巨型帧,为了提高加密/解密的吞吐和降低系统调用开销。可是将巨型帧用于真实的物理链路一定好吗?小包灵活,这也是分组交换的真谛,那么大包呢?尾大不掉,一个无限大的包就是电路交换流了。它会长期占领链路。
就像集卡或者挖掘机一样。好在现实中使能巨型帧的链路它真的有能力传输巨型帧(说明它的路比較宽!
),这就没有什么问题。
可是,假设拥有非常宽的路,就一定要传输巨型帧吗?假设跑小帧岂不效率更好?双向10车道对于跑集卡是没有什么问题。可是跑轿跑预计更好。假设非要运货,那么运输机的吞吐量尽管不比货轮。可是延时却小非常多。
其实。在一段出发后的链路上。即便你传输了巨型帧,也不保在途中被拆分。
因此基于能耗。分组交换效率,分片/重组开销考虑,巨型帧算是没有什么优势了,我之所以在OpenVPN中模拟了巨型帧。是因为在OpenVPN链路上我能够全然控制一切,尽管这须要途径物理网卡以及虚拟网卡的数据帧均是巨型帧,可是我能通过測试证明。针对这些巨型帧的分片/重组开销远远大于OpenVPN的小包加密/解密以及系统调用开销。
那么。巨型帧的初衷是什么?只为了抵消掉分组交换的优点??其实,这是针对主机的优化,特别是执行Windows主机。这样的主机一般都是处在网络的边缘。作为端到端的终点处理。因此对于到来的数据帧,假设中断过于频繁,势必会降低应用层的处理性能,毕竟总的资源是有限的。而巨型帧能够降低中断的数量。
我以前说,巨型帧是合时宜的,此话也不绝对,对于高性能单一链路而言,这是对的。对于途径窄带链路的数据网络而言,巨型帧平添了开销。且在最后一公里的链路上堵路。
中间节点的分片/重组可能并不只一次,只要须要分析协议头的地方。均须要重组分片,比方状态NAT,比方防火墙。比方数据流分类...对于中断的频度,眼下的千兆卡,万兆卡均能够在网卡芯片内进行分片,重组。然后再中断CPU。同一时候也能够积累数据帧到一定量,然后中断CPU一次。接下来改中断为轮询。细节请參看Intel IGB的e1000e驱动程序readme。
5.协议栈的分层处理风格
协议栈的设计是分层的,这并不意味着协议栈的实现也必须是分层的。早期的实现,或者受早期实现影响的实现。比方UNIX的流模块。比方Windows的NDIS框架。都是严格依照分层原则实现的,这样的实现的优势是调用者能够在不论什么既有的点插入不论什么处理逻辑,不用显式的设置HOOK点。对于Windows的NDIS框架。只要你调用的API复合NDIS框架的约定,过滤驱动能够随意实现。不只在内核态处理,即便是socket层次,Windows也提供了SPI的LSP机制,除了大家都认可的BSD socket接口,TCP实现。UDP实现。IP实现等是内置的外,其他的你都是能够随意外接的,即便是TCP/IP栈,也不是必须的,假设你看网卡的属性。你会发现TCP/IP协议是能够单独安装卸载的,假设你安装了VMWare。那么就会自己主动安装VMWare桥接协议。总之,一切都是能够灵活安装的,没有内置的HOOK点。你能够随意在层间插入自己的处理模块。
这样的设计旨在方便开发人员实现自己的逻辑,开发人员只须要了解相关接口就能够在任何位置插入自己的逻辑。开发人员甚至能够根本就不懂网络协议栈处理逻辑。当然。这样的设计的缺点也是显而易见的,因为只开放了相关的API而并未开放细节,那么你便无法对未开放可是你真的须要的接口进行调用。因此开发人员非常难实现协议栈层次之间的随意组合。
Linux的协议栈设计就全然不是分层的,而是全然基于回调的,它并不区分协议处理,小port驱动等,因为全部的回调都是注冊好排好序的,因此假设你想在两个回调之间插入一个HOOK,你就必须先解除之前的链接关系。Linux採用了第二种方式来处理这样的情形,它同意开发人员自行组织各个层次之间的相互调用。比方你能够在一个hard_xmit回调函数内部调用还有一个hard_xmit回调,你也能够在一个recv里面调用xmit或者随意的xmit..这样的随意组合是递归的,看似混乱,实则灵活。
你会发现,Linux的bridge。bonding。vlan都是这样的方式实现的,这些机制并没有像NDIS那样插入一个过滤层NDIS驱动,而是直接组合各种xmit。Linux的这样的实现风格要求开发人员对网络协议栈处理逻辑细节十分熟悉。他们在一个函数中拿到的是一个拥有网络协议栈语义的数据单元而不不过一段buffer。
除了协议层的边界,在协议层处理的内部。Linux定义了若干个HOOK点,Netfilter是一个非常重要的框架,基于它能够实现非常棒的防火墙。注意Netfilter并非只用于防火墙,而是协议栈内置的一套框架,它能够实现随意的数据包QUEUE,STOLEN等。在此基础上能够实现IPSec VPN而无需插入不论什么层次。美中不足的是,Linux对于socket的HOOK机制比較少。
假设你想HOOK住connect操作,就必须在OUTPUT chain上去match TCP的syn标志...
以负载均衡的实现为样例,对于NDIS,须要实现一个中间层过滤驱动,而对于Linux,除了IPVS,使用bonding的lb模式也是非常方便的。
6.大便骑沟便沟内
针对早期的水冲式公共厕所,此处从略...几种答案。几多理由。