Linux路由缓存的前世此生

route cache

3.6版本必定算得上是Linux网络子系统中一个特别的版本, 这个版本(补丁patch)移除了查找FIB以前的缓存查找。本文就来谈谈路由缓存的前世此生。linux

几个基本概念

为了让本文的阅读曲线更加平缓我决定仍是将本文涉及的一些术语做个说明。git

路由:将skb按照规则送到该去的地方,这个地方多是本机,也多是局域网中的其余主机,或者更远的主机。从这个角度来讲,它一个动词。那么路由发生在哪一个时候呢? 咱们知道路由是网络层(L3)的概念,接收方向,它须要决定收到的skb是应该上送本机仍是转发,发送方向,它须要决定skb从哪一个网络接口发出。下图本来是描述Netfilter在内核中的钩子位置的,但我以为用来讲明路由的位置也是比较合适的。数据库

forward

与此同时,路由也能够特指上面所说的规则,这是名词的用法。路由从哪来? 通常来讲有三个来源:1. 用户主动配置;2.内核生成; 3. 其余一些路由协议进程(OSPFBGP)生成。普通主机上可能没有最后一种,因此,为了理解方便,你能够将路由就理解为你用route命令看到的内容。缓存

[root@tristan]# route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
192.168.99.0    0.0.0.0         255.255.255.0   U     0      0        0 eth0
192.168.98.42   192.168.99.1    255.255.255.255 UGH   0      0        0 eth0
127.0.0.0       0.0.0.0         255.0.0.0       U     0      0        0 lo
0.0.0.0         192.168.99.254  0.0.0.0         UG    0      0        0 eth0

FIB:全称是(Forwarding Information Base),翻译过来就是转发信息表FIB是内核skb路由过程的数据库,或者说内核会将路由翻译成FIB中的表项。咱们习惯说的查询路由,对于内核来讲,应该叫查询FIB网络

3.6版本之前的路由缓存

缓存无处不在。现代计算机系统中,CacheCPU与内存间存在一种容量较小但速度很高的存储器,用来存放CPU刚使用过或最近使用的数据。路由缓存就是基于这种思想的软件实现。内核查询FIB前,固定先查询cache中的记录,若是cache命中(hit),那就直接用就行了,没必要查询FIB。若是没有命中(miss), 就回过头来查询FIB,最终将结果保存到cache,以便下次再也不须要须要查询FIB性能

缓存是精确匹配的, 每一条缓存表项记录了匹配的源地址和目的地址、接收发送的dev,以及与内核邻居系统(L2层)的联系(negghbour)
FIB中存储的也就是路由信息,它经常是范围匹配的,好比像ip route 1.2.3.0/24 dev eth0这样的网段路由。spa

下图是3.6版本之前的本机发送skb的路由过程.....net

fiblookup

看上去的确可能能提升性能! 只要cache命中率足够高。要得到高的cache命中率有如下两个途径:1. 存储更多的表项; 2.存储更容易命中的表项翻译

缓存中存放的表项越多,那么目标报文与表项匹配的可能性越大。可是cache又不能无限制地增大,cache自己占用内存是一回事,更重要的是越多的表项会致使查询cache自己变慢。使用cache的目的是为了加速,若是不能加速,那要这劳什子有有什么用呢?code

前面说了,cache的特色决定了它只能作精确匹配。也就是说,只有目标数据报文与cache中的表项彻底一致,才算匹配成功。最简单的cache查找过程应该是下面这样:遍历cache中的全部表项,直到遇到匹配的表项跳出循环。

foreach entry in cache:
then
    if entry match skb
    then
        /* 条件匹配,将缓存表项中记录的结果设置到skb上 */
        skb->dst <= entry->dst
        return
    endif
end

显然,cache表项的数目越多,那么查找的过程就越长! 固然,内核不会这么蠢地将全部cache拉成一个线,而是使用hash桶,看上去应该是这么一个结构。

cachebucket

内核首先根据目标报文的一些特征计算hash,找到对应的hash冲突链表。在链表上一个一个地进行比较遍历。

为了不cache表项过多,内核还会在必定时机下清除过时的表项。有两个这样的时机,其一是添加新的表项时,若是冲突链的表项过多,就删除一条已有的表项;其二是内核会启动一个专门的定时器周期性地老化一些表项.

得到更高的cache命中率的第二个途径是存储更容易命中的表项,什么是更容易命中的呢? 那就是真正有效的报文。遗憾的是,内核一点也不聪明:只要输入路由系统的报文不来离谱,它就会生成新的缓存表项。坏人正好能够利用这一点,不停地向主机发送垃圾报文,内核所以会不停地刷新cache。这样每一个skb都会先在cache表中进行搜索,再查询FIB表,最后再建立新的cache表项,插入到cache表。这个过程当中还会涉及为每个新建立的cache表项绑定邻居,这又要查询一次ARP表。

要知道,一台主机上的路由表项可能有不少,特别是对于网络交换设备,由OSPF*BGP等路由协议动态下发的表项有上万条是很正常的事。而邻居节点却不可能达到这个数量。对于转发或者本机发送的skb来讲,路由系统能帮它们找到下一跳邻居*就足够了。

总结起来就是,3.6版本之前的这种路由缓存在skb地址稳定时的确可能提升性能。但这种根据skb内容决定的性能倒是不可预测和不稳定的。

3.6版本之后的下一跳缓存

正如前面所说,3.6版本移除了FIB查找前的路由缓存。这意味着每个接收发送的skb如今都必需要进行FIB查找了。这样的好处是如今查找路由的代价变得稳定(consistent)了。

路由缓存彻底消失了吗? 并无!在3.6之后的版本, 你还能够在内核代码中看到dst_entry。这是由于,3.6版本其实是将FIB查找缓存到了下一跳(fib_nh)结构上,也就是下一跳缓存

为何须要缓存下一跳呢? 咱们能够先来看下没有下一跳缓存的状况。以转发过程为例,相关的伪代码以下:

FORWARD:

fib_result = fib_lookup(skb)
dst_entry  = alloc_dst_entry(fib_result)
skb->dst = dst_entry;

skb->dst.output(skb)   
nexthop = rt_nexthop(skb->dst, ip_hdr(skb)->daddr)
neigh = ipv4_neigh_lookup(dev, nexthop)
dst_neigh_output(neigh,skb)
release_dst_entry(skb->dst)

内核利用FIB查询的结果申请dst_entry, 并设置到skb上,而后在发送过程当中找到下一跳地址,继而查找到邻居结构(查询ARP),而后邻居系统将报文发送出去,最后释放dst_entry

下一跳缓存的做用就是尽可能减小最初和最后的申请释放dst_entry,它将dst_entry缓存在下一跳结构(fib_nh)上。这和以前的路由缓存有什么区别吗? 很大的区别!以前的路由缓存是以源IP和目的IP为KEY,有千万种可能性,而如今是和下一跳绑定在一块儿,一台设备没有那么多下一跳的可能。这就是下一跳缓存的意义!

early demux

early demux是在skb接收方向的加速方案。如前面所说,在取消了FIB查询前的路由缓存后,每一个skb应该都须要查询FIB。而early demux是基于一种思想:若是一个skb是本机某个应用程序的套接字须要的,那么咱们能够将路由的结果缓存在内核套接字结构上,这样下次一样的报文(四元组)到达后,咱们能够在FIB查询前就将报文提交给上层,也就是提早分流(early demux)

图 early demux

总结

3.6版本将FIB查询以前的路由缓存移除了,取而代之的是下一跳缓存。

REF

Route cache removed
IPV4 route cache removed from >= 3.6 linux kernel
remove routing cache
Linux3.5内核之后的路由下一跳缓存

相关文章
相关标签/搜索