Linux 3.6内核去掉了路由cache的支持,这件事众口难调。也许你会记得路由cache给你带来的各种好处,也许你已经注意到去掉了路由cache支持后fn_trie_lookup在perf top中显示的比重是多么地高,然而不管怎样,社区的人总是能告诉你100个路由cache下课的原因。如果你不服,那大可不必理会这些缺陷,将路由cache重新移回你自己的版本即是,然而我并不觉得这是一个好的方案,我觉得还有更好的方案。
3.6内核前的路由cache问题在于,系统并不知道这些被cache住的路由是不是有效的路由,而有效的方式是要cache住有效的路由,什么是有效的路由?就是那些真正为本机所用的路由。
暂不考虑forward的情况下,我认为但凡已经成功创建的socket发送数据所使用的路由都是有效的路由!以TCP为例,TCP连接在建立的过程中就已经挡掉了大多数的无效项,比如SYN-Cookie之类,它可以让很多无效的连接无法建立,因此你可以认为,凡是已经建立的连接,都是有效的。
这在你的机器作为服务器的时候特别有意义。如果你的系统配置了大量的路由表项,其中只有少量的路由项是你的数据发出时要经由的(WEB服务器更多的面临这种情况),不幸的是,最长前缀匹配算法无法保证拥有最大流量的那个连接的发送方向路由查找以最快的速度返回,因此还是需要一个cache系统来保证这一点,而我的方式就是,将路由项保存在已经确认有效的socket结构体中。
代码十分简单。我花了20分钟,修正了代码并编译了一个版本,行之有效!以下就是我的代码,我并不准备将其提交以及并不奢望它能被真正使用,和以往一样,仅仅是玩玩而已,所以我不以patch的形式提供代码,顺便说一下,在现有的Linux协议栈实现中,已经有sk_dst_set/get这类API将一个socket与一个路由项关联,然而我看来,它只是指示了一些经由该路由发出的数据包的“链路特征”,比如MTU,MSS,RTT之类,并没有使用它来免除最长前缀匹配的路由查找,所以说,为了不造成二义性,我没有基于此而修改代码,我自行维护了一套新的数据结构。
代码列如下:
1.include/net/sock.h中增加sock的一个路由cache项
struct sock {
struct dst_entry *sk_dst_cache;
// 此为新增字段,标识一个被socket缓存的路由项
struct rtable *sk_rt_cache;
};
2.include/net/dst.h中增加路由项当前版本信息
struct dst_entry
{
...
// version用于判断该cache是否依然可用
int version;
};
3.include/net/net_namespace.h中增加全局的版本信息
struct net {
...
// 该version标识当前的ns中最新的路由cache版本信息
atomic_t rt_version;
};
4.include/linux/snmp.h中增加netstat统计项
enum
{
...
IPSTATS_MIB_OUTBCASTOCTETS, /* OutBcastOctets */
IPSTATS_MIB_SKCACHE,
__IPSTATS_MIB_MAX
};
5.net/ipv4/proc.c中增加netstat统计项的展示
static const struct snmp_mib snmp4_ipextstats_list[] = {
...
SNMP_MIB_ITEM("OutSocketRTCache", IPSTATS_MIB_SKCACHE),
SNMP_MIB_SENTINEL
};
6.net/ipv4/route.c增加全局版本号的更新
void rt_cache_flush(struct net *net, int delay)
{
// 只要系统路由有增删改操作,就会将版本好增加1,以禁用所有cache,省去了精确匹配替换
atomic_inc(&net->rt_version);
rt_cache_invalidate(net);
if (delay >= 0)
rt_do_flush(!in_softirq());
}
7.net/ipv4/ip_outout.c增加如何使用socket路由cache的逻辑
int ip_queue_xmit(struct sk_buff *skb, int ipfragok)
{
struct sock *sk = skb->sk;
struct inet_sock *inet = inet_sk(sk);
struct ip_options *opt = inet->opt;
struct rtable *rt;
struct iphdr *iph;
struct net *net = sock_net(sk);
// 进入IP层之初就查找socket路由cache
rt = sk->sk_rt_cache;
if (rt &&
net_eq(dev_net(rt->u.dst.dev), net) &&
atomic_read(&net->rt_version) == rt->u.dst.version) {
IP_INC_STATS(net, IPSTATS_MIB_SKCACHE);
goto got_cached_rt;
}
/* Skip all of this if the packet is already routed,
* f.e. by something like SCTP.
*/
rt = skb_rtable(skb);
if (rt == NULL) {
...
rt->u.dst.version = atomic_read(&net->rt_version);
sk->sk_rt_cache = rt;
}
got_cached_rt:
skb_dst_set(skb, dst_clone(&rt->u.dst));
...
}
以上代码完成后,编译,启动内核,然后开始正常的常规操作,此时先去看统计信息。/proc/net/stat/rt_cache中显示的out_hit将会大量减少,这意味着路由cache大量没有命中!
然而这却是好事!系统的路由cache没有命中,意味着它命中了我的socket路由cache!
请看/proc/net/netstat的IpExt单元最后一项OutSocketRTCache的值,在其它机器从本机下载大文件的时候,该计数器会急剧升高,这意味着命中!
这种socket路由cache的命中在系统路由cache之前执行,效率更高,相当于系统路由cache的上一级cache,在Linux 3.6内核将系统路由cache去掉之后,这个socket路由cache依然可以继续发挥作用。
值得注意的是,此优化对于forward数据包的路由无效,因为forward数据无法跟一个socket建立关联。此优化最大的用武之地是,你的机器提供对外的服务,别人经常从你的机器下载数据,这样这个优化的收益是非常可观的,因为每一个数据包的路由查找开销都被节省了下来!
3.6版本内核之前的路由cache,其思想并没有错,错在这个cache放错了地方以至于可能会成为众矢之的而被DDos,如果将其放在socket中,相当于将其从野外移到了屋里,威胁没有了,自然就一切都好了,此时socket会承担更多的保护路由cache的义务,其收益便是,路由cache可以瞬间为数据指一条明路。