在Linux3.5版本号(包括)之前。存在一个路由cache。这个路由cache的初衷是美好的,可是现实往往是令人遗憾的。下面是陈列得出的两个问题:
1.面临针对hash算法的ddos问题(描写叙述该问题的文章已经汗牛充栋,不再赘述);
2.缓存出口设备是p2p设备的路由项会降低性能。

这些问题本质上是由于路由cache的查找方式和路由表的查找方式互不相容引起的。路由cache必须是精确的元组匹配,因此它必须设计成一维的hash表,而路由表查找算法是最前前缀匹配。因此它能够是多维的。

路由查找终于会找到路由项。在不考虑策略路由的前提下,我们来看一下把出口设备为p2p设备的路由项塞进路由cache是多么的没有意义。

p2p设备的邻居集合里仅仅有一个下一跳,那就是它的对端,因此对于p2p设备。甚至都不须要进行邻居绑定的过程。然而假设将这类路由塞进路由cache的话。将会占领巨量的内存,试想假设有10w个IP地址须要通信。源IP集合中相同有10w个IP地址。将有可能会建立100w条路由cache项。极端一点,假设此时系统中仅仅有不多的几条路由表项的话,查找路由表的开销可能会反而低于查找路由cache的开销。特别地。假设路由结果是p2p设备,其实仅仅要想办法cache这唯一的一个条目就可以。这就是一和多的差别,这次,我们发现不光零到一有意义。一到多也相同不可小觑。

假设系统中有一块以太网卡eth0。由于同一网段会有多个邻居,不同的目标IP地址。其下一跳可能会有所不同,我们不得不cache每个与eth0相关的路由项,然后针对每个数据包进行精确匹配,然而假设系统中有一块p2p网卡,它的邻居仅仅有一个,对于点对点设备而言,其对端逻辑上仅仅有一个设备,它是唯一的且确定的。它是该点对点设备的邻居集合中的唯一一个邻居,因此其实无需进行邻居绑定过程,仅仅要从点对点设备将数据包发出,该数据包就一定会到达唯一的对端,在这样的情况下,假设我们还cache每个与该p2p网卡相关的路由项,意义就不大了,然而,对于Linux的路由cache机制而言,这是无法做的的,由于在查找路由cache以及查找路由表之前。我们无从知道这个数据包就是终于要从一个p2p网卡发送出去的。

一个解决方式是,假设查找路由表的结果表明其出口设备是p2p设备。则设置一个NOCACHE标志,表示不cache它,待到数据包发送完成即释放,我想这个实现是简单而明了的。本来去年9月份想实现掉它,也是为了我们的一个网关产品能够提高性能。可是后面我离职了,此事也就不了了之,直到近期,我再次面临了此问题。

然而我有了更好的建议,那就是升级内核到3.6+。只是这是后话,其实,假设你必须维护基于低版本号内核的老产品的话,改动代码就是避不开的,幸运的是,无论是老公司。还是新公司,我与2.6.32版本号的代码打交道已经6年了。

扩大点说。路由查找这东西确实非常尴尬,能够肯定,一台设备上可能会有数十万条的路由。然而与其相连的邻居集合内的节点数却能够用一个字节来表示,并且大多数节点的邻居可能仅仅有不超过10个!我们消耗了大量的精力,什么cache查询。什么最长前缀匹配。终于就是为了在数十万数量级的大海中捞出几根针,所以说,这一直都是一个比較有挑战性的领域,与TCP加速相比。这个领域更加闭环,它不受其他影响。仅仅有算法本身影响它!其实,不光p2p设备,就连ethX设备。结局也是悲哀的,配置几十条路由。终于的下一跳可能仅仅有五六个,p2p设备仅仅是更加极端一些罢了。对于p2p设备,我们一般这么写路由就可以:
route add -host/net a.b.c.d/e dev tunlX
然而对于ethX设备而言,一般来说我们必须写路由:
route add -host/net a.b.c.d/e gw A.B.C.D
也就是说,p2p设备直接告知了数据包从设备发出去就可以,然而对于ethX设备(或者全部的广播网络设备以及NBMA设备),必须进行地址解析或者下一跳解析才会知道从哪里发出去。不光如此。路由cache还会对邻居子系统造成影响,简单的说,就是路由项引用邻居。路由项释放之前,邻居不能被释放。即便p2p设备不须要邻居解析。在代码层面也必须特殊处理,不幸的是,Linux内核中并没有看到这样的特殊处理,p2p设备的路由项依旧会塞进路由cache。

以上就是路由查找的困境。困境在于多对一或者多对少的映射过程,这样的情况下。营造一个精确匹配的cache可能使结局更加悲哀,因此,用一种统一的方式进行调优可能更加符合人之常情。

Linux3.6以后。去除了路由cache的支持,全部的数据包要想发送出去,必须查找路由表。现在的过程可能会变成下面的逻辑:

dst=lookup_fib_table(skb);
dst_nexthop=alloc_entry(dst);
neigh=bind_neigh(dst_nexthop);
neigh.output(skb);
release_entry(dst_nexthop);

这是一个完美的过程。然而在协议栈的实现层面,出现了新的问题。即alloc/release会带来巨大的内存抖动,我们知道,内存分配与释放是一个必须要在CPU外部完成的事务。它的开销是巨大的。尽管在Linux中有slab cache,可是我们相同也知道。cache是分层的。

其实,Linux在3.6以后。实现了新的路由cache。不再缓存一个路由项。由于那须要skb的元组精确匹配,而是缓存下一跳,找到这个cache必须经过lookup_fib_table这个例程。

这是个创举。由于缓存的东西是唯一的,除非发生一些例外!这就破解了解决多对一以及多对少的问题。在找到缓存之前,你必须先查找路由表。而查找完成之后,理论上你已经知道了下一跳,除非一些例外(再次重申!

)这个新的下一跳缓存仅仅是为了避免内存的分配/释放!伪代码例如以下:

dst=lookup_fib_table(skb);
dst_nexthop=lookup_nh_cache(dst);
if dst_nexthop == NULL;
then
dst_nexthop=alloc_entry(dst);
if dst_nexthop.cache == true;
then
insert_into_nh_cache(dst_nexthop);
endif
endif
neigh=bind_neigh(dst_nexthop);
neigh.output(skb);
if dst_nexthop.cache == false
then
release_entry(dst_nexthop);
endif

就这样,路由cache不再缓存整个路由项,而是缓存路由表查找结果的下一跳。

鉴于一般而言,一个路由项仅仅有一个下一跳。因此这个缓存是极其有意义的。这意味着。在大多数时候,当路由查找的结果是一个确定的dst时。其下一跳缓存会命中。此时便不再须要又一次分配新的dst_nexthop结构体,而是直接使用缓存中的就可以。假设非常不幸,没有命中,那么又一次分配一个dst_nexthop,将其尽可能地插入到下一跳缓存,假设再次非常不幸,没有成功插入,那么设置NOCACHE标志,这意味着该dst_nexthop使用完成后将会被直接释放。

上述段落说明的是下一跳缓存命中的情况。那么在什么情况下会不命中呢,这非常easy,无非就是在上述的lookup_nh_cache例程中返回NULL的时候,有不多的几种情况会导致其发生。比方某种原因将既有的路由项删除或者更新等。

这个我随后会通过一个p2p虚拟网卡mtu问题给予说明,在此之前,我还要阐述第二种常见的情形,那就是重定向路由。

所谓的重定向路由,它会更新本节点路由表的一个路由项条目,要注意的是。这个更新并非永久的,而是暂时的。所以Linux的做法并非直接改动路由表,而是改动下一跳缓存!这个过程是异步的,伪代码例如以下:

# IP_OUT例程运行IP发送逻辑,它首先会查找标准路由表,然后在下一跳缓存中查找下一跳dst_nexthop,以决定是否又一次分配一个新的dst_nexthop。除非你一開始指定NOCACHE标志。否则差点儿都会在查找下一跳缓存失败进而创建新的dst_nexthop之后将其插入到下一跳缓存,以留给兴许的数据包发送时使用,这样就避免了每次又一次分配/释放新的内存空间。
func IP_OUT:
dst=lookup_fib_table(skb);
dst_nexthop = loopup_redirect_nh(skb.daddr, dst);
if dst_nexthop == NULL;
then
dst_nexthop=lookup_nh_cache(dst);
endif
if dst_nexthop == NULL;
then
dst_nexthop=alloc_entry(dst);
if dst_nexthop.cache == true;
then
insert_into_nh_cache(dst_nexthop);
endif
endif
neigh=bind_neigh(dst_nexthop);
neigh.output(skb);
if dst_nexthop.cache == false
then
release_entry(dst_nexthop);
endif
endfunc # IP_ROUTE_REDIRECT例程将创建或者更新一个dst_nexthop,并将其插入到一个链表中,该链表由数据包的目标地址作为查找键。
func IP_ROUTE_REDIRECT:
dst=lookup_fib_table(icmp.redirect.daddr);
dst_nexthop = new_dst_nexthop(dst, icmp.redirect.newnexthop);
insert_into_redirect_nh(dst_nexthop);
endfunc

以上就是3.6以后内核的下一跳缓存逻辑,值得注意。它并没有降低路由查找的开销,而是降低了内存分配/释放的开销。路由查找是绕只是去的。可是路由查找结果是路由项,它和下一跳结构体以及邻居结构体之间还有层次关系,其关系例如以下:
路由项-下一跳结构体-邻居项
一个数据包在发送过程中,必须在路由查找结束后绑定一个下一跳结构体,然后绑定一个邻居。路由表仅仅是一个静态表,数据通道没有权限改动它,它仅仅是用来查找。协议栈必须用查找到的路由项信息来构造一个下一跳结构体。这个时候就体现了缓存下一跳的重要性,由于它降低了构造的开销!

最后,我们能够看一下效果。假设你仅仅是看代码,那么当你看到input或者output路径中的rt_dst_alloc调用时,你可能会非常灰心丧气。可是假设你使用下面的命令看一下实际结果:
watch -d -n 1 “cat /proc/net/stat/rt_cache”
的时候。你就会发现,in_slow_tot和out_slow_tot两个字段的计数器添加十分缓慢。甚至停滞!

这意味着绝大多数的数据包在接收和发送过程中都命中了下一跳cache!假设你发现了异常,也就是说不是这样的情况,它们中的其一或者两者增长的非常快,那么可能是双方面的原因:
1.你的内核可能没有升级到足够高的版本号
这意味着你的内核有bug,在3.10的最初版本号中。RT_CACHE_STAT_INC(in_slow_tot);的调用是发生在下列代码之前的:

if (res.fi) {
if (!itag) {
rth = rcu_dereference(FIB_RES_NH(res).nh_rth_input);
if (rt_cache_valid(rth)) {
skb_dst_set_noref(skb, &rth->dst);
err = 0;
goto out;
}
do_cache = true;
}
} rth = rt_dst_alloc(net->loopback_dev,
IN_DEV_CONF_GET(in_dev, NOPOLICY), false, do_cache);
...

也就是说它遗留了路由cache存在的年代的代码,错误的将下一跳缓存当成了路由cache。仅仅须要将RT_CACHE_STAT_INC(in_slow_tot)移植到rt_dst_alloc之后就可以。

2.你可能使用了p2p设备。可是并没有正确的设置MTU
我们知道ipip隧道设备在Linux上是一个虚拟网卡设备,数据包要真正发送出去要经过又一次封装一个IP头部的过程,假设终于是经由ethX发送数据,其MTU默认是1500,假设ipip隧道设备的MTU也是1500或者小于1500减去必要头部开销的话,就到导致又一次更新MTU的操作,而一个下一跳缓存中包括MTU信息,假设MTU须要又一次更新,就意味着下一跳缓存须要更新。

在一般的物理设备中。这不是问题,由于往往在IP层发送数据前,MTU就是已经确知的。可是对于ipip隧道设备而言,在数据发送的时候,协议栈在实际往隧道发送数据前并不知道终于数据包须要再次封装。因此也就对MTU过大导致数据无法发送这件事不知情,特别是遇到gso,tso这样的情况。事情会更加复杂。此时我们有两个解决方式:
1).适当调低ipip隧道的MTU值,保证即使经过再次封装,也只是长度过载。这样就不会导致又一次更新MTU进而释放更新下一跳cache。
2).从代码入手!
依据代码的rt_cache_valid来看,不要让下一跳缓存的标志变成DST_OBSOLETE_KILL就可以,而这也是和MTU相关的,而在__ip_rt_update_pmtu中。仅仅要保证下一跳缓存的初始mtu不为0就可以。这能够添加一个推断,在rt_dst_alloc之后,初始化rth字段的时候:

if (dev_out->flags&(IFF_LOOPBACK|IFF_POINTOPOINT))
rth->mtu = dev_out->mtu;
else
rth->mtu = 0;

经过測试,效果良好!

BTW,和非常多的安全协议一样。路由表项以及下一跳缓存也使用了版本号号来管理其有效性。仅仅有表项的ID和全局ID一致的时候,才代表该表项有效,这简化了刷新操作。当刷新发生的时候,仅仅须要递增全局版本号号ID就可以。

现在,能够总结一下了。在Linux3.6以后,路由cache被去除了,取而代之的是下一跳缓存,这里面有非常多的蹊跷,比方有重定向路由的处理等...这主要是有效降低了内存管理的开销而不是查找本身的开销。在此要说一下内存的开销和查找的开销。

二者并非一个层次的,内存的开销主要跟内存管理数据结构以及体系结构有关,这是一个复杂的范畴,而查找的开销相对简单,仅仅是跟算法的时间空间复杂度以及体系结构相关,然而为什么用查找的开销换内存的开销,这永远是一个无解的哲学问题!

Linux3.5内核以后的路由下一跳缓存的更多相关文章

  1. linux 接口地址全部清除才清理从此接口发出的下一跳路由

    接口地址全部清除才清理从此接口发出的下一跳路由 如: eth7配置两个地址 eth7: 192.168.1.1 10.1.1.1 添加一条路由: route add -net 2.2.2.0/24 g ...

  2. 等价路由在路由器和CE交换机上默认的行为是不同的,路由器总是走第一个下一跳,CE交换机是逐包。

    结论: 1.在eNSP中实验,路由器和CE交换机对于等价路由的默认转发行为是不同的, 路由器:默认是基于流的转发形态,更准确的来讲,ping两个不同的下一跳,都是走等价路由的第一个路由,不走第二条路由 ...

  3. 使用BGP的虚拟下一跳实现IGP的路由负载

    网络拓扑: XRV1 ============================================================== !hostname XRV1! interface ...

  4. ensp的基础路由命令,接口,下一跳的配置,入门必备

    关于ensp入门事情,第一件事当是安装必备三件套:而后,应该是接触路由和PC机了,最烦人满屏代码,眼花缭乱: 今天写一篇零基础接触ensp的首次操作,PC-路由-路由-PC的互通实验: 实验要拉出两台 ...

  5. Linux3.4内核的基本配置和编译

    转载自:http://www.embedu.org/Column/Column634.htm 作者:李昕,华清远见研发中心讲师. 了解Linux3.4内核的特性及新增功能,掌握Linux内核的编译过程 ...

  6. I2C(三) linux3.4(内核分析)

    目录 I2C(三) linux3.4(内核分析) (一)总线流程 bus.probe match i2c_device_probe (二)client注册 方式(一)静态加载 方式(二)指定设备 方式 ...

  7. 配置多个相同网段的ECMP下一跳,配合NQA健康检查实现高可靠性

    1.一般情况下,ECMP常用的常见是,针对很远的目的地址,下一跳分别是路由器的不同出端口,而路由器的不同端口是不同网段的,也就是说,下一跳是不同的网段地址. 但是,在连接到终端服务器时,常常会采用多个 ...

  8. 《LINUX3.0内核源代码分析》第二章:中断和异常 【转】

    转自:http://blog.chinaunix.net/uid-25845340-id-2982887.html 摘要:第二章主要讲述linux如何处理ARM cortex A9多核处理器的中断.异 ...

  9. 基于OMAPL:Linux3.3内核的编译

    基于OMAPL:Linux3.3内核的编译 OMAPL对应3个版本的linux源代码,分别是:Linux-3.3.Linux-2.6.37.Linux2.6.33,这里的差距在于Linux2,缺少SY ...

随机推荐

  1. Node.js:工具模块

    ylbtech-Node.js:工具模块 1.返回顶部 1. Node.js 工具模块 在 Node.js 模块库中有很多好用的模块.接下来我们为大家介绍几种常用模块的使用: 序号 模块名 & ...

  2. Hyper和Vmware冲突,Device/Credential Guard 不兼容

    切换到VM的时候,采用关闭策略 1.PS管理员关闭命令 bcdedit /set hypervisorlaunchtype off 2.系统设置,启用或关闭Windows功能那里,关闭Hyper-V ...

  3. 44.Qt通过子类化qstyle实现自定义外观

    main.cpp #include <QtGui> #include "brozedialog.h" #include "bronzestyle.h" ...

  4. Xposed那些事儿 — xposed框架的检测和反制

    之前看到有人发了关于使用xposed屏蔽抖音检测xposed的思路(https://www.52pojie.cn/thread-684757-1-1.html),贴出了部分伪代码,但觉抖音写的蛮有意思 ...

  5. python简易版学生管理系统

    #coding=utf- def showInfo(): print("**************") print(" 学生管理系统") print(&quo ...

  6. kindeditor文本编辑器乱码中乱码问题解决办法

    这个问题我已经解决掉了,不是更改内容的编码格式,只要将lang/zh_CN.js  这个文件的编码转换成unicode即可 操作方法是 用记事本打开这个文件,另存为,然后更改文件的编码格式为unico ...

  7. DataTable转Dictionary

    DataTable dt = new DataTable(); dt.Columns.Add("name"); dt.Columns.Add("no"); dt ...

  8. sublime3 install python3

    链接地址:https://blog.csdn.net/Ti__iT/article/details/78830040

  9. Windows Server2008上安装VS2008出错及解决办法

    作者:朱金灿 来源:http://blog.csdn.net/clever101 win server 2008安装vs2008后报错,如下图: 然后到网上找了一种解决办法: (1)打开服务器管理器 ...

  10. 23个Python爬虫开源项目代码:爬取微信、淘宝、豆瓣、知乎、微博等

    来源:全球人工智能 作者:SFLYQ 今天为大家整理了23个Python爬虫项目.整理的原因是,爬虫入门简单快速,也非常适合新入门的小伙伴培养信心.所有链接指向GitHub,祝大家玩的愉快 1.Wec ...