本文题目有点大,但其实我只想描述一些我我的一直比较关注的特性,而且不会太详细,跟往常同样,主要是帮忙理清思路的,不会分析源码。这主要是为了哪一天忽然忘了的时候,一目十行扫一眼就能记忆当时的理解,否则写的太细节了,本身都看不懂了。
缓存
先 从TCP的syncookie提及,若是都能使用syncookie机制该有多好,可是不能,由于它会丢失不少选项协商信息,这些信息对TCP的性能相当 重要。TCP的syncookie主要是为了防止半链接的syn flood***,超级多的节点发送大量的syn包,而后就无论了,而被***的协议栈收到一个syn就会创建一个request,绑定在syn针对的 Listener的request队列上。这会消耗很大的内存。
可是仔细想一想,抛开选项协商不说,仅仅针对TCP的syn,synack而言,事实上TCP在3次握手过程,只须要查找一下Listener便可,只要它 存在,就能够直接根据syn包构造synack包了,根本就不用Listener了,要记住2次握手包的信息,有两个办法,第一个办法就是 syncookie机制给encode并echo回去,等第3次握手ack来了以后,TCP会decode这个ack的序列号信息,构造子socket, 插入Listener的accept队列,还有一种办法就是在本地分配内存,记录这个链接客户端的信息,等第3次握手包ack到来以后,找到这个 request,构造子socket,插入Listener的accept队列。
在4.4以前,一个request是属于一个Listener的,也就是说一个Listener有一个request队列,每构造一个request,都 要操做这个Listner自己,可是4.4内核给出了突破性的方法,就是基于这个request构造一个新的socket!插入到全局的socket哈希 表中,这个socket仅仅记录一个它的Listener的轻引用便可。等到第3个握手包ack到来后,查询socket哈希表,找到的将再也不是 Listnener自己,而是syn包到来时构造的那个新socket了,这样传统的下面的逻辑就能够将Listener解放出了:
传统的TCP协议栈接收
服务器
sk = lookup(skb); lock_sk(sk); if (sk is Listener); then process_handshake(sk, skb); else process_data(skb); endif unlock_sk(sk);
能够看出,sk的lock期间,将是一个瓶颈,全部的握手逻辑将所有在lock期间处理。4.4内核改变了这一切,下面是新的逻辑:
cookie
sk = lookup_form_global(skb); if (sk is Listener); then rv = process_syn(skb); new_sk = build_synack_sk(skb, rv); new_sk.listener = sk; new_sk.state = SYNRECV; insert_sk_into_global(sk); send_synack(skb); goto done; else if (sk.state == SYNRECV); then listener = sk.lister; child_sk = build_child_sk(skb, sk); remove_sk_from_global(sk); add_sk_into_acceptq(listener, child_sk); fi lock_sk(sk); process_data(skb); unlock_sk(sk); done:
这个逻辑中,只须要细粒度lock具体的队列就能够了,不须要lock整个socket了。对于syncookie逻辑更简单,根本连SYNRECV socket都不用构造,只要保证有Listener便可!
这是周四早上蹲厕所的时候猛然看到的4.4新特性,当时就震惊了,这正是我在2014年偶然想到的,可是后来因为没有环境就没有跟进,现在已经并在 mainline了,不得不说这是一件好事。当时个人想法是依照一个syn包彻底能够无视Listner而构造synack,须要协商的信息能够保存在别 的地方而没必要非要和Listner绑定,这样能够解放Listener的职责。可是我没有想到再构造一个socket,与全部socket平行插入到同一 个socket哈希表中。
我以为,4.4以前的逻辑是简单明了的,不论是握手包和数据包,处理逻辑彻底一致,可是4.4将代码复杂化了,分离了那么多的if-else...可是这 是不可避免的。事实上,syn构造的request自己就应该与Listener进行绑定,只是若是想到优化,代码会变得复杂,可是若是在代码自己下一番 功夫,代码也会很好看,只是,我没有那个能力,我代码写的很差。
这个Lockless的思想跟nf_conntrack的思想相似,可是我以为conntrack对于related conn逻辑也能够这么玩。
less
紧随着Lockless TCP Listener而来的accept队列的优化!众所周知,一个Listener只有一个accept队列,在多核环境下这个单一的队列绝对是个瓶颈,一个高性能服务器怎么能够忍受这样!
其实这个问题早就被REUSEPORT解决了。REUSEPORT容许多个独立的socket同时侦听同一个IP/Port对,这对于当今的多队列网卡, 多CPU环境绝对是个福音。可是,虽然路宽了,车道多了,没有规则的话,性能反而降低,拥挤程度反而降级!
4.4内核为socket引入了一个SO_INCOMING_CPU选项,若是一个socket的该选项设置为n,意味着只有在n号cpu上处理协议栈逻 辑的执行流才能够将数据包插入这个socket。体如今代码上,就是在compute_score上给与加分,也就是说,除了目标IP,目标端口,源 IP,源端口以外,cpu也成了一个匹配项目。
正如patch说明说的,此特性与REUSEPORT,多队列网卡相结合,必定是一道美味佳肴!
socket
以 前的时候,有路由cache,一个路由cache项就是一个带有源信息的n元组信息,每个数据包在匹配到FIB条目后都会创建一条cache项,后续的 查找首先去查找cache,所以都是基于流的。然而在路由cache下课后,多路径选路变成了基于包的,这对于TCP这种协议而言确定会形成乱序问题。为 此4.4内核在多路径选路的时候,hash计算中引入了源信息,避免了这个问题。只要计算方法不变,永远一个流的数据hash到一个dst。
ide
这 个不是4.4内核携带的特性,是我本身的一些想法。early_demux已经被引入了内核,旨在消除本机入流量的路由查找,毕竟路由查找后还要再 socket查找,为什么不直接socket查找呢?查找到的结果缓存路由信息。对于本机提供服务的设备而言,开启这个选项吧。
可是对于出流量,仍是会有很大的开销浪费在路由查找上。虽然IP是无链接的,可是TCP socket或者一个connected UDP socket倒是能够明确标示一个5元组的,若是把路由信息存储在socket中,是否是更好的。好吧!不少人会问,怎么解决同步问题,路由表改了怎么 办,要notify socket吗?若是你被此引导而去设计一个“高效的同步协议”,你就输了!办法很简单,就是引入两个计数器-缓存计数器和全局计数器,socket的路 由缓存以下:
性能
sk_rt_cache { atomic_t version; dst_entry *dst; };
全局计数器以下:
优化
atomic_t gversion;
每当socket设置路由缓存的时候,读取全局gversion的值,设置进缓存version,每当路由发生任何改变的时候,全局gversion计数器递增。若是cache计数器的值与全局计数器值一致,就可用,不然不可用,固然,dst自己也要由引用计数保护。
ui