标签:地址空间 alt 回归 之间 方案 思想 接下来 segment 顺序
收到邮件时,被人要求写这样的命题作文,其实我是拒绝的,由于你不能叫我写我就立即去写,首先我自己得懂这个。我又不能说到了写完了的时候贴了非常多baidu出来的图片,说了非常多套话,人家一看就知道我这是转载或者翻译的,又没人给我打分也得不到什么奖励,还不如不写...我得先理一下思路。发现这些内容正是我心里的东西,那思考了一个上午之后呢,我去嘉定新天地吃了顿小火锅,喝了点酒,然后回来整理图片,起码这些东西我是非常熟悉的,如今每天仅仅要有时间我还折腾这些,不光如此。我还一度引诱我的同事,朋友折腾这些,就像引诱他们研究历史一样...
其实,当这些文字和图整理出来以后,我是欣慰的。
我记得。上一次我整理这类图的时候是在2011年我的小小出生的那几天,我晚上在医院。白天换班回家歇息。然后就写写随笔什么的。非常多都没有发表在博客上,由于非常多也是跟IT没有关系的。
只是无论怎么说,这几年我也确实有所进步,尽管在某些地方也退步了不少。
近期大家都在忙着跳槽。找工作的时候,请记住第一个问题非常重要。真的。
格局一定要大。措词一定要专业...只是这些都是虚的。你自己能把这个问题回答得跟玩一样,还是须要一定的积累的。我不敢说自己是网络领域的什么专家,差点儿就是个菜鸟,有点积累的老鸟。所以我写了这篇作文。看看能不能对那些希望在底层IP网络觅食的小伙伴们带来一点帮助。据非常多大拿大咖的预測以及我自己的感觉,IP将在近期几年有大动作。这个大动作并非指IPv6,而是别的,和物联网,云。机会网络,自组织相关的。所以,蹲好马步才干去手持弹弓准打MM屁股啊。
路由项的key是一个bit前缀。比方192.168.0.0/16,172.16.20.0/24,1.2.3.0/28,3.4.5.6/32等。而这些key作为IP的表达方式。所不同的是它们仅仅考虑32位中的某些连续的位而不一定非要是全部,我们会发现,对于32位前缀的路由项key。它们相应最底层叶子节点的某些节点,而对于小于32位前缀的路由项key。它们正好相应一些中间结点。
因此我们基于IPv4地址树而得到了一棵新的树。即带有路由节点的IP地址树:
路由节点作为中间结点或者叶子节点。我们有两种方式得达到它。
然而假设并不代表实际,这是一种执果索因法,我们并不知道输入IP的实际位置,我们也不想从漠河游历到日喀则,因此怎样缩短路径就是要解决的问题。我们的问题是:
1).有32位前缀的精确路由吗?假设有。里面有没有和输入IP地址一样的呢?假设有,那就是它了。假设没有的话...
2).有31位前缀的路由吗?假设有。有哪个或者哪些(负载均衡?度量?...)的前31位值和输入IP的前31位相等呢?假设有,那就是它了,假设没有的话...
3).有30位前缀的路由吗?假设有。有哪个或者哪些(负载均衡?度量?...)的前30位值和输入IP的前30位相等呢?假设有,那就是它了,假设没有的话...
4).有29位前缀的路由吗?假设有,有哪个或者哪些(负载均衡?度量?...)的前29位值和输入IP的前30位相等呢?假设有。那就是它了,假设没有的话...
...
逻辑是简单的,问题是我们怎么知道究竟有没有。算法上怎样去实施。答案当然也非常显然。那就是把全部的路由项依照前缀自大到小进行排序。每个前缀拥有一个链表。每个链表节点都是一个路由项,例如以下:
32 prefix list:node32-1,node32-2,node32-3。node32-4
31 prefix list:node31-1,node31-2,node31-3,node31-4...
30 prefix list:null
29 prefix list:node29-1。node29-2
...
最简单的方式就是拿输入IP地址依次从上到下,每个链表从左到右去遍历上述结构。然而事情到了如此明了的地步,是个人应该都能够想到使用HASH来加快计算效率的吧。因此上述结构变成了:
32 prefix hashlist:hash1(node32-1。node32-4),hash2(node32-3),hash3(node32-2)
31 prefix hashlist:hash1(node31-8。node31-5,node31-3)。hash2(node31-1)。hash3(...)
30 prefix hashlist:hash1(null)。hahs2(null)。hash3(null)
29 prefix hashlist:hash1(node29-2),hash1(null)。hash2(node29-2)
...
这下就免除了每个前缀链表遍历的困境。仅仅须要计算bucket=hashcalc(IP_input),然后在每个前缀hash表的hash[bucket]冲突链表中遍历一小部分就好了。
自下而上查找总是从最精确的前缀開始的,旨在找到第一个匹配的路由项,而以下要详细描写叙述的自上而下的查找则是从最不精确的前缀開始。旨在找到最后一个匹配的路由项,咋一看,自下而上占了优势,然而其实,自上而下的方案也不赖。
自下而上的算法是如此显然,以至于再多添加一些笔墨就显得多余,所以接下来的大部分篇幅,我想留给第二种截然相反的算法。
注解:自下而上的算法难道不就是Linux路由表hash查找的算法吗?
那棵庞大的IPv4地址树仅仅是为了给出一个理解路由查找原理的方式,实际上我们关注的应该是:怎样使得一个特定的IPv4地址,即目标地址能够高速的到达某个路由节点或者高速发现没有这样的路由节点。
要达到这样的目的,就不得不掠过非常多不带有路由项的“空心节点”,为此我们须要将这棵IP地址树进行压缩,从而终于在这棵树上仅仅保留最少数量的“空心节点”。它们存在的目的,仅仅是高速引导我们到达某一个实心的路由节点。
使用这样的方式。全部的路由节点都被安排到了叶子。非常方便于Level压缩。
方式2:路由节点新增元信息,指示路径状况
Right more:同上,左换成右
Pre node idx:路径中上一个路由节点的索引(为此须要将全部路由项进行索引),假设当前节点不匹配,则直接取出上一个节点来使用,假设当前节点匹配且Left/Right more为0,则使用当前节点。
由于IP地址树存在压缩(立即就会讲到),因此这样的方式不如方式1应用广泛,由于存在压缩的情况下,即便当前路由节点不匹配,上一个也不一定匹配,再者,由于存在动态的路径压缩和Level压缩,节点间的关系也会动态变化。叶子和非叶子节点也会变化,而方式2须要不断追踪节点之间的关系,开销比較高。
在我描写叙述地址树压缩的时候,有个前提,那就是一切都是基于前面一小节的方式1来变换的。关于地址树的压缩,其实它就是标准二叉树的压缩,方案有两类。路径压缩和Level压缩。
只是我不准备用那个及其庞大的IP地址图来开刀,它实在太大了。我仅仅是用它的上面一部分来说明原理:
能够看出,路径压缩做的事比較简单,操作也比較简单,它的目的就是忽略和路由项无关的节点。仅仅保留路由项以及路由项引导项节点。全部的输入IP地址在查找路由的时候,不必再逐位比較。而仅仅和路由项的检查位比較就可以,终于顺着一条路径到达一个叶子节点,该节点即终于找到的路由项。
由于採用了方式1进行了路由项合并,因此须要用输入IP逐个检查叶子路由项的掩码链表,终于选出掩码最长的那个路由项。然而由于存在路径压缩,可能这一步并不会成功。我来解释一下这是为什么。请看上图的路由节点路径上的X位。对于路由项的key而言(比方192.168.1.0/24,其key就是192.168.1.0),它将全部的被压缩的bit都假设为0或者模式同样的若干位。如都是1。都是11010等,即不检查,也就是它们被忽略了。造成的结果就是,输入IP地址的这些bit可能有不是0的,这就会造成终于的叶子节点的精确匹配不会通过。而此时,就须要回溯了。这是下一节的主题。
对于怎样游历而言,非常简单,每个节点都是相似以下的结构体:
struct node { u8 pos; struct node *child[2]; };
Level压缩也面临路径压缩时的bit忽略,例如以下图所看到的:
struct node { u8 pos; u8 bits; struct node *child[0]; };
为了更好支持动态Level压缩,将纯二叉树路径压缩也涵盖进来,仅仅只是其bits为1而已。
关于Level压缩,还要说一点。那就是。我们总是希望终于的树能够规则一些,好看一点,那么在压缩之前须要对这个树做一定的变换,比方将畸形的树变成全然的树等。幸运的是,无类路由查找是最长前缀匹配的。也就是说。在IP地址树上,每个IP地址所使用的路由项是从它到树根的路径上近期的那个路由项。反过来说,每个路由项都覆盖地址树第32层叶子节点的一个范围,依据最长前缀匹配规则,假设发生覆盖范围重叠。层次深的路由项范围自己主动覆盖层次浅的路由项范围,假设我们想更好的进行Level压缩。方法也非常简单,过程例如以下:
1).将中间路由节点下移到叶子节点。
2).动态压缩或者直接压缩。
关于这个概念,还是先看一个图:
其实。它们真的是有联系的。
从这里開始,后面提到压缩路由树的时候。指的就是压缩变换(路径压缩+Level压缩+路由项合并前缀列表变换)了的带有路由项的IPv4地址树。
注解:Linux和BSD
在这里说详细的实现有点早,只是能够说一点。
不要拘泥于名称。Radix树,Trie树等(所谓:道 可道,非 常道![一般人断不好这句子,会念做“道可道,非常 道...”]),其实Linux的Trie树採用的就是路径压缩+Level压缩+合并前缀链表的混合LC-Trie算法,而BSD曾一度是标准纯二叉树路径压缩+合并前缀链表算法,尽管它也被称作Radix树算法。
3).压缩带来的问题
前面说过,压缩会带来问题。压缩是一次冒险,冒险值得与否须要权衡。当採用压缩时,就会有冒险失利的情况,因此在带有压缩的路由地址树上查找路由项的过程分为两步骤:
a.路由项的精确匹配过程
目标IP地址ip1作为路由地址表的输入,依照标准的查找过程终于到达一个孩子节点,即:
1).取出root的pos和bits,计算ip1的[pos...pos+bits]值作为孩子index,进入root->child[index];
2).假设不是叶子,则将root->child[index]赋值给root。继续1)。
3).假设是叶子。则依据叶子的掩码链表的从长到短排序的每个前缀prefix,依次做ip1 & prefix。与叶子节点的key做比較,如若相等,则成果返回,若不等,进入b。
如今问题是,假设上述第3)步匹配成功,能保证此路由结果的前缀是全部匹配结果的最长的吗?答案当然是肯定的,其依据在于。假设没有压缩,我们到达的最后的一个路由节点肯定是最精确的,如今尽管採用了压缩。可是注意我们是在冒险,我们假设路由项中全部压缩掉的bit和我们的输入ip1的相应bit是相等的。仅仅有真的相等,才会终于匹配到叶子节点。而当终于真的匹配成功时。我们就会知道,我们冒险成功了。它们确实是相等的。而终于的叶子节点可能是诸多上层的路由节点合并的结果,然而我们已经将前缀进行了排序。保证首先匹配最长前缀的那个。
假设冒险失败呢?那就回溯,这是以下要详谈的内容。
b.前缀递减的回溯匹配过程
这个又叫最长前缀匹配,可是为了避免和路由查找的最长前缀匹配混淆,我使用前缀递减过程来替代。这个过程仅仅有在步骤a终于失败的情况下才会使用。冒险失败的惩处就是回溯,它用额外的时间开销抵偿了压缩带来的空间收益,可是细致想想,也不。假设不压缩,时间开销不也非常大么?难道不须要逐位比較么?毕竟恶性回溯是能够通过平衡操作避免掉的。比方依据上述的理论,把树压得比較胖的时候。就能避免恶性回溯。
我一向喜欢通过一个样例来说明问题,对于这个回溯,也是一样。首先还是先看个样例,这个样例包括了精确匹配和前缀递减匹配两个过程:
我尤其喜欢在阐述完一个样例后给出一个一般性的解释,可是我发现总结出一个一般的算法简直太难了,因此不得不将Linux的代码再次拿出来,抛弃掉一些细节。展示一个全面的算法:
路由树查找算法 { pn = 树根; chopped_off = 0; IP_prefix = 32; while (pn) { pos = pn.pos; bits = pn.bits; if (!chopped_off) cindex = (IP_input & IP_prefix)[pos...pos+bits]; n = 获取pn的cindex孩子; if (n不存在) { 回溯; } if (n是一个叶子路由节点) { // Lambda表达式 遍历n的前缀链表prefix(prefix->{IP_input & prefix == n.key ? 匹配}); if (没有匹配) 回溯; 找到并退出; } cn = n; // 回溯时遇到非叶子节点时显然应该一路向左 if (IP_prefix < pos+bits) { if (cn.key & IP_prefix[IP_prefix...pos] || !(cn.child[0])) 回溯; } pn = n; chopped_off = 0; continue; 回溯: chopped_off++; // 移除0的影响,bit0不会影响结果。此后依次化1为0 // 比方当前的cindex为0101100 // cindex更新的步骤依次为: // 0101000 -> chopped_off = 3;IP_prefix = pos+7-3 // 0100000 -> chopped_off = 4;IP_prefix = pos+7-4 // 0000000 -> chopped_off = 5;IP_prefix = pos+7-5 while ((chopped_off <= pn.bits) && !(cindex & (1<<(chopped_off-1)))) chopped_off++; // 更新IP_prefix if (IP_prefix > pn.pos + pn.bits - chopped_off) IP_prefix = pn.pos + pn.bits - chopped_off; if (chopped_off <= pn.bits) { // 一次消除1个最右边的1 cindex &= ~(1 << (chopped_off-1)); } else { parent = 获取pn的parent; cindex = pn.key[parent.pos...parent.pos + parent.bits]; pn = parent; chopped_off = 0; 回溯; } } }
非常easy证明,每个IP地址都唯一相应了一个这样的区间,一个区间由且仅由一个路由项负责。
那么,为一个IP地址查找路由的问题就转换成了为一个IP地址查找它所相应的区间。示意图例如以下所看到的,我依旧用子树说明问题,再次给出Level压缩那一小节给出的图:
struct element { u32 start; u32 end; struct rt_node *node; };
果真就是这样,假设路由项为基准。那么总的数组大小就是路由项的数目。一般不会超过256(一跳可达的IP地址),假设以区间为基准,那么数组的大小就是路由项们将整个IP地址空间切割成的区间数量,这个数量受路由条目的数量以及汇总情况,路由分布的影响,总之假设配置得当的话,也不会是个非常大的数字。
剩下的就是区间查找算法了。
其实还有更好,更优化的方案。即DxR算法。关于这个主题,我已经写了好几篇文章了《从模拟MMU设计一个路由表的失败到DxR的回归》,《模拟MMU设计一个将IPv4地址索引化的路由表,不同于DxR》等,因此就不再赘述了,在这些文章中。我从无到有详细描写叙述了DxR的思想。总之就是切割子区间的方案造就了高效的时间和空间利用率。
在这个小节的最后。关于区间查找。我不想描写叙述它是怎么用到路由查找中的,仅仅知道一个输入IP地址相应一个区间。而一个区间相应一个下一跳就够了。我想描写叙述一下给定一个IP地址。怎样为其定位区间,或者更加广泛的。将一个连续的域划分为若干区间。给定该域的一个数,怎样定位它属于哪个区间。我们先看一下区间:
问题如今变成了怎样构造一个单一的入口用以开启那一发不可收拾的查找过程,显然。我是带有偏见的,由于假设使用HASH算法。是不须要构造单一入口的,HASH函数已经将界限给界定了,仅仅须要解决冲突就可以,在比key少得多的冲突元素的情况下。遍历冲突链表也不是什么坏主意。
可是本文通篇都是二叉树。所以我的偏见在于,我想构建一棵二叉树,将这个区间映射到这棵树中,然后从根部开启这个自己主动的查找过程。或者说,二分查找过程。首先我将这些区间的边界点构造成叶子,然后从叶子開始,到根部为止,逆向构建这棵树:
这两者是截然对立的。我们能够从两个參量来分析:
区间匹配算法由于分离了路由项的key前缀和下一跳,能够多对一共享下一跳。它不须要压缩,由于模糊性给了区间,即根本不须要输入IP地址相应到第32层叶子节点的精确位置,相应到一个区间就可以,而这个区间相应一个已经和路由项分离了的下一跳。这就算是找到了路由-其实是找到了下一跳,而其实,此时已经没有完整的路由项了。路由项早就化身为了一个区间和一个下一跳。
无论怎样,模糊性是肯定要存在的,由于你不可能保存一棵完整的IP地址树,所以就须要牺牲它的精确性,带来一点模糊性。这个模糊性存在于哪里,带来的是算法的本质不同。
对于自下而上的查找而言,模糊性在于,你事先根本不知道输入IP地址会落在哪个位置。甚至哪个区间都不知道,因此你必须将全部的路由项依照前缀自长而短一个一个试。而HASH算法。仅仅是帮助这个尝试的过程更快结束而已。它并非查找算法必须的。你全然能够将HASH算法更换成不论什么一种你认为比HASH更快的查找算法。自下而上查找的本质在于路由项依照前缀长度递减的顺序被尝试,而不是HASH算法。
BSD和Linux实现的方案都是通用的方案。它并没有假设你有不论什么的Cache能够利用,它们都是纯算法级别的,它们能够让你分析其时间复杂度和空间复杂度,可是DxR却不能,DxR很多其他的是一种实现方式而非算法本身,由于它利用了太多能够让你利用的东西。假设我将DxR算法作为转发表的实现方案,或许会更好些吧。
此外,关于名称。我是拒绝讨论的。假设有面试官问起你来,你就说你仅仅懂算法不知道叫什么名字。
非常早以前。我听说BSD的算法叫做Radix树算法,后来又有人叫它PC-Trie,而Linux的算法叫做LC-Trie,后来我被这些名字搞晕了。索性也就不在乎它们究竟叫什么了。爆炸!
因此,为了在查找的时候高效。牺牲一些构建,更新成本还是必要的。
详细的时间复杂度请自行搜索。总之对于二叉树,基本就是N*logN,logN级别的,当然不要考虑最坏情况,那种情况早就被OS发现了,发现了之后就会进行平衡操作了,对于Level压缩的树,就要考虑每次索引的bit长度了。对于HASH算法,基本要看你选择的HASH函数有多好以及怎样解决冲突。
本文考虑的情况差点儿全部是关于查找的,没有说怎样插入,构建的,这不是本文的主题。由于主流软件路由器的关注的是查找性能,它们差点儿也不会执行复杂的动态路由协议,使能动态路由协议的差点儿都是核心路由器,起码也是个接入层的了。它们关注的是硬件的设计而不是软件的算法。在软件路由器上。查找性能必须优化到起码每秒接近千万次,对于上述的算法而言,足够了。再一个。IP路由是有规律可循的。它们并非随机的一组数让你排序什么的。另外就是,假设你真的爱上了DxR,那么你暂且把Cache寻址作为内部排序,而把内存里的排序操作权当外部排序吧,其实,转发表真的就不把内存当“希腊公民”看待,对于转发表来讲,内存就是磁盘...
当然。也能够混合使用各种算法。比方DxR算法。它其实就是先构建了一棵两层的65535叉树,然后再在这棵树的节点上挂接一棵区间树。这样的组合的方案往往能够达到惊人的效果。
1).步骤分解
在本文中。我直接简单的给出了路由表的查询结构,可是并没有说明这些结构是怎样构建起来的,其实。说真的,这还真的非常麻烦,可是无关紧要。为什么呢?由于这些事情能够“慢慢来做”,这就意味着你有足够的时间去想办法假于物也。
2).模糊性/效率权衡
时间和空间并非二元对立的。有的时候,比方在数据统计的时候。假设并不须要特别精确的统计,你大可同一时候获得一些时间和空间收益,代价就是冒错误统计之险。此举能够先用模糊匹配过滤掉大多数的不匹配的情况,然后再用少量的精确匹配纠正错误。其实。即便不谈Bloom,路经压缩树本身就採用了这个思想。
多线程版本号的OpenVPN中多个线程同一时候操作这张表是常常的,鉴于它不会常常被写入,我使用了读写锁对其进行保护,这是一种非常好的思想。起码我当时认为。然而,假设系统中保留一份全局的multi_instance表,然后将这个表优化成更加利于查找的结构,复制给每个线程。岂不是更好吗?这样就没有锁操作了。这个无锁的思想也正是ZeroMQ的思想:别指望一群喝醉的人能够安全共享一瓶酒。
这个复制后的本地表称为转发表。而那个全局的表称为路由表。
转发表由路由表生而成之。
在我预计启动的OpenVPN重构计划中,我将设计一个全局的multi_insance表,然后在各个线程中基于它构造一个转发表。全然使用ZeroMQ...说的有点跑题了...爆炸
这将把代码带到一个无锁的世界,既然有一群醉汉,干嘛不给他们一人一瓶酒呢?其实。在核心的路由器上,正是採用了这样的设计。
路由缓存的意义大吗?这是和应用类型有关的,在以往固定节点网络的TCP长连接的年代。比方Telnet,FTP等。数据流的时间局部性非常明显。甚至在像HTTP这类多报文短连接(长连接更好)协议,时间局部性也是能够利用的,可是如今以及未来就有所不同了。P2P网络,机会网络,随机网络。自组织网络,移动网络,这类网络中节点与节点的通信路径是让人捉摸不定的,即便是长连接协议,也会由于节点的高速移动而使在上一次的位置节点路由器上缓存下来的路由项再也不会有机会命中,甚至,即便我们不考虑移动性,考虑一些信令协议,或者纷繁复杂的应用...
Linux如今已经取消了路由缓存的支持,而我本人在它取消路由缓存的支持之前也由于另外的原因採用另外的办法取消了路由缓存的支持。路由缓存的取消主要有双方面的原因,其一是路由项已经没有必要被缓存-它们八辈子不会用到一次,其二-须要维护缓存一致性?哦,这第二个原因是不合理的,由于,由于IP协议本身就不须要维护状态,因此也就不须要什么缓存一致性。而我。正是由于这第二个原因取消了路由缓存。可是我也不是明知故犯。而是,而是我使用了Linux Netfilter的conntrack机制。而conntrack机制和IP配合的总是不太好。因此,我承认,我为此付出了代价。
...
命题作文:在一棵IPv4地址树中彻底理解IP路由表的各种查找过程
标签:地址空间 alt 回归 之间 方案 思想 接下来 segment 顺序
原文地址:http://www.cnblogs.com/tlnshuju/p/7103471.html