在前几天写的一篇文字中,我描述了一次失败的经历,对于很在乎过程的我,描述下来就是成功。然而,我不得不回退到DxR,研究一下它的本质而不是其算法思 想。之所以失败,是因为我的逆反心理在作怪,我真的没有研究DxR的本质就开始动手,无疑于打一场毫无准备且对对手完全不了解的恶仗,如果不适可而止,其 结果必然和当初死磕Bloom一样悲惨!

DxR的本质

DxR并没有发明什么新的算法,它之所以高效是因为它分离了路由项中的路由前缀和下一跳这两个基本元素。在这个的基础上,它就可以采用三张表来实现自己的既高效又占用空间小的目的。我来总结一下:

DxR算法的前提:分离了路由前缀和下一跳。

这 个前提及其重要!分离前缀和下一跳可以消除数据冗余,构建查找表的目标就从构建单纯的查找匹配表转换成了构建IPv4地址的某一段区间和下一跳表的映射关 系,这就直接导致了区间查找。我们来看一下很类似的Trie树查找算法,这个算法中路由前缀和下一跳是作为一个“路由项”绑定在一起的,因此查找的过程就 是一个精确匹配+回溯的过程。而DxR算法则消除了回溯的过程。

DxR算法的设施:直接索引表,区间表,下一跳表。

这个我后面还会说,但记住,这不是核心,这只是一种实现方式。

DxR算法表设计:直接索引表的意义

直接索引表合并了巨大的IPv4地址区间,以便区间表在合并后的少的多的区间中更快速地进行搜索,两个表的目的都是指向下一跳表的索引。这就建立了区间到下一跳的映射。

用路由前缀将IPv4地址空间分割成区间

如 果到此为止你还不知道DxR算法是什么,那也无所谓,其实它的思想很简单。路由表的最终结果就是将某个连续地址段对应到某个下一跳(不允许不连续掩码 了...),因此路由表实际上是将整个IPv4地址空间分割成了若干个区间,每个区间只和一个下一跳关联。我把那篇关于记录失败经历的文章中一个正确的图 贴如下:


从模拟MMU设计一个路由表的失败到DxR的回归_第1张图片


拿 着目标IP地址当索引,向右走,碰到的第一个路由项就是结果。最长掩码的逻辑完全体现在插入/删除过程中,即从左到右前缀依次变短,长前缀的路由项会盖在 短前缀的路由项的前面,这就是核心思想。虽然我现在已经否定了拿IPv4地址直接去做索引,但是核心思想并没有变,即“拿XX映射到具体的下一跳”,在那 篇失败记录中,XX是IPv4地址索引,而在正确的做法中,XX是区间。其实在HiPac防火墙中,也正是使用了这个思想,即区间查找。在HiPac算法 中,区间就是match域,而下一跳对应Rule。
       那么,DxR算法就是针对上述图示的一步步优化。为了更好的说明DxR,我再次给出上图的变换形式:


从模拟MMU设计一个路由表的失败到DxR的回归_第2张图片


区间查找

如 果按照上面的图示,整个IPv4地址空间被分割成了N个区间,路由查找的最终目标是将某个IPv4地址对应到某个区间中!到此为止,其实工作已经完成了。 但是有个前提,那就是你要找出或者自己实现一个高性能的“区间匹配算法”!,即建立一个区间表,内部保存N个区间项,每个区间项对应一个下一跳索引,比如 区间m对应下一跳C,我们的目标是给定一个IPv4地址,判断它属于哪个区间。这样的算法比比皆是,自己实现一个似乎也不难,比如二分法,哈希算法等,所 以本文不关注这些。然而DxR似乎并不满足这个发现,当然我也不满足。DxR似乎希望找到一种更加优化的方式实现这个区间匹配。
       在给出DxR的框架之前,到此为止,我们发现,DxR实质上就是使用了区间匹配来将一个目标IPv4地址对应到一个区间,然后取出该区间对应的下一跳!

划分子区间

如 果针对每一个到来数据包的目标IPv4地址都要在N个区间中做匹配,似乎不太优雅。如果能将这N个区间划分为若干个子区间,那么每次匹配时匹配的区间数量 将会大大减少,比如N为100,如果能将整个IPv4地址空间划分为20个相等的子区间,那么每次匹配的区间数量将会是5个,而不是100个!!但是这里 又有一个前提,那就是划分子区间的开销一定要能被由于减少区间数量而带来的收益抵消掉,并且收益要更大!
       这个时候,如果你深入理解二级页表就好办了,一个页目录项包含1024个页表项,一个页表项指向一个4096字节大小的页面。其中页目录就把整个32位虚 拟地址空间分割成了1024个相同大小的区间段,每一个区间段的大小为4096*1024,32位虚拟地址对应32位IPv4地址,事情不就是这样吗?不 过,二级页表或多级页表解决的是稀疏地址的问题,如果是一级的页表,那么中间会有很多的“洞”,这是因为进程如何安排虚拟地址在内核和MMU看来是管不了 的。而对于目前我们遇到的问题,采用类似的分级方式是为了划分子区间从而提高每次区间匹配的效率,注意,这并不是以索引为目的的,我错误的将索引作为了目 的而不是手段,于是跌到了万劫不复的深渊!
       但是,对于IPv4地址,并不采用10bit(这是考虑到虚拟地址寻址的特点以及页面的大小而设定的)这样的划分法,而是采用k bit划分法,注意,路由表并不存在页面的概念!如果k等于16,那么就把IPv4地址的高16位就成了一个索引,由于低16位的存在且自由取值,那么每 一个索引表项包括16位涵盖的IPv4地址数量,即65535个IPv4地址。目前的区间查找表变成了下面的样子:


从模拟MMU设计一个路由表的失败到DxR的回归_第3张图片


要知道,IPv4地址高16位地址可以一下子索引出子区间,这是一个瞬间的操作!然后下面的问题就是“如何合理布局这些子区间”。

子区间布局

如何将子区间布局成紧凑的结构事关重大,因为紧凑的数据结构意味着可以载入CPU Cache!
       以上面最后一幅图为例子,我们当然希望所有的区间依然连续存放,这样似乎是紧凑的唯一方式。我们把这个紧凑的合并后的子区间表叫做区间表,如下图所示:


从模拟MMU设计一个路由表的失败到DxR的回归_第4张图片


这个时候,IPv4地址的高16位索引表怎么可以区分出自己索引的那囊括65535个地址的区间到底要分割为哪些子区间呢?答案当然是指示一个起始位置和区间数量了。如果我们把所有的图示展示成一种最终的方式,那么请看下图:


从模拟MMU设计一个路由表的失败到DxR的回归_第5张图片


以 上的图仅仅包含三个表,一个索引表,一个区间表,另外还有一个下一跳表。关于下一跳表图中没有画出,这是因为它的内容不固定,可以仅仅是一个IP地址,也 可以有设备信息以及状态信息等,也可以是一个链表,用于负载均衡,当然,也可以指向别的东西。其中最关键的就是前两个表,即索引表和区间表。这两张表都可 以放在很紧凑的空间中,占用很小的内存,这两张表将以最大的能力毛遂自荐以被载入CPU Cache。

DxR到底是个什么样子的

有点不好意思。因为上面说着说着就把该说的全部说了。
       其实,DxR就是上面的那幅图所表达的!只是在DxR中:

1.DxR中的x指的就是上文中的k,在我的例子中,我取的是16,实际上它可以取别的值。但是一般而言,大于等于16吧。

2.图中索引表的第三部分,即编码优化数据,这部分可以优化定义,使得这些表更加紧凑。

3.如果索引表中定位的区间表的区间数量为1,那么索引表可以直接指向下一跳索引。

总 的来讲,k值越大,索引表占据的空间越大,如果k值取32,那就不好意思了,索引表项为4G个,区间表不复存在,因为所有的IP地址到下一跳的映射都明细 化了,这就是我自己那次模拟MMU的设计最终的结果,总之,索引表越大,就有越多的IP地址到下一跳的映射明细化,区间表的大小在统计意义上就会越小,这 也是空间换时间的体现...固定索引表大小的时候,区间表的大小是不固定的,取决于你的路由表的路由项布局,因此要想好好使用DxR,没有一点路由规划能 力是不行的,比如你要尽量使用诸如汇总之类的技巧,为了使得路由可以汇总,你可能会还需要重新布线,让可以汇总的路由可以共用同一接口相连的下一跳,这又 涉及到了一些路由分发的能力,特别是你在混用动态路由和静态路由的时候。总之,IP路由是比较复杂的,涉及到了综合的能力,算法,IP地址的理解,地址规 划,路由分发,动态路由,配置命令,甚至综合布线...
       我并没有说这个表如何增删改,这个我觉得是可以自己分析的,它主要受到动态路由的影响,毕竟,如果线路状态不是经常变化,路由表一般也是稳定的。