最后,我们可以看一下效果,如果你只是看代码,那么当你看到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被去除了,取而代之的是下一跳缓存,这里面有很多的蹊跷,比如有重定向路由的处理等... 这主要是有效减少了内存管理的开销而不是查找本身的开销。在此要说一下内存的开销和查找的开销。二者并不是一个层次的,内存的开销主要跟内存管理数据结构以及体系结构有关,这是一个复杂的范畴,而查找的开销相对简单,只是跟算法的时间空间复杂度以及体系结构相关,然而为什么用查找的开销换内存的开销,这永远是一个无解的哲学问题!