listen函数中的backlog字段

今天在公司无意间又拿redis-benchmark测了一下silly的IO并发性并与redis本身比较了一下。

发现在2000个client同时并发的情况下,性能只是Redis的30%的左右。

直觉上这很不正常,虽然silly所有的数据经过lua层时都需要malloc和memcpy,但最多性能上差个10%~20%就已经很可观了,绝对不可能差70%这么多。

通过不断的调整client的个数,我发现性能并没有明显随着client的数量降低而降低,而是在client个数到达某个值时突然降低的。

比如我自己测的数据是在491个client时,silly的性能与redis相差无几,但在492个client同时并发时,silly的性能锐降到redis 30%左右。

这也再次说明了应该不是malloc和memcpy造成的开销。

在此期间,我分别尝试加大epoll的缓冲区和增大socket预读缓冲区,都没有明显的效果。这也说明问题并不在IO的读取速度上和系统调用开销上。

万般无奈之下Download下来redis3.0的源码开始对着比,最终发现惟一不一样的地方就是redis的listen的backlog竟然是511,而我的只有5.

一下子豁然开朗了,由于backlog队列过小,导致所有的connect必须要串行执行,大部的时间都在等待建立连接上,在将backlog的值改为511后,性能已然直逼redis。

google了一下发现,除了redis连nginx竟然也是用511。但是记忆中backlog参数会影响未完成连请求队列的大小,似乎增加backlog会增加syn洪水攻击的风险。

查了好一会资料,最后发现man上早都指出在Linux 2.2之后listen中的backlog参数仅用于指定等待被accept的已完的socket队列的长度。未完成连接的队列长度则通过/proc/sys/net/ipv4/tcp_max_syn_backlog来指定。

至于为什么backlog是511而不是512, 是因为kernel中会对backlog做roundup_power_of_tow(backlog+1)处理,这里使用511实际上就是为了不浪费太多不必要的空间。

之前一直看资料上说backlog是个经验值,需要根据经验调节。然而并没有想到,当大批量连接涌入时,backlog参数会起到这个大的影响。那么这个经验看来就是要估算每秒的连接建立个数了。

比如web服务器由于http的特性,需要频繁建立断开链接,因此同一时刻必然会涌入大量连接,因此可能需要更高一些的backlog值,但是对于游戏服务器来讲,大多都是长连接,除了刚开服时会有大量连接涌入,大部分情况下连接的建立并不如web服务器那么频繁。当然即使如此依然需要对每秒有多少链接同时进入进行估算,来衡量backlog的大小。

虽然可以不用估算直接使用backlog的最大值,但却可能会造成‘已完成未被Accept的socket的队列’过长,当accept出队列后面的连接时,其已经被远端关闭了。

经过测试,即使backlog为63,在局域网内同时并发2000客户端并无性能影响。


12月23日纠下补充:
1. listen的backlog值其实是会精确指定accept的队列的,只不过它除了控制accept队列的大小,实际上还会影响未完成的connect的队列的大小,因此
roundup_power_of_tow(backlog+1)增大的实际是未完成connect队列的大小。
2. /proc/sys/net/ipv4/tcp_max_syn_backlog 虽然字段名中有一个sync但其实限制的是accept队列的大小,而并非是未完成connect队列的大小

虽不欲写成kernel net源码解析的文件(实际上是怕误人子弟:D), 但还是走一下流程证明一下吧(只针对tcp和ipv4基于3.19)。

先看listen的整个流程:

listen系统调用 其实是通过sock->ops->listen(sock, backlog)来完成的。

那么sock->ops->listen函数是咋来的呢,再来看socket系统调用, 其实是通过socket_create间接调用__sock_create来完成的。

sock->ops->listen函数则是通过__socket_create函数中调用pf->create来完成的。而pf其实是通过inet_init函数调用socket_register注册进去的,至于什么时间调用了inet_init这里就不赘述了,毕竟这不是一篇kernel分析的文章:D.

由此我们找到pf->create实际上调用的就是inet_create函数.

啊哈!接着我们终于通过inetsw_array找到sock->ops->listen函数最终其实就是inet_listen函数。可以看到我们通过listen传入的backlog在经过限大最大值之后,直接被赋给了sk_max_ack_backlog字段。

OK,再来看一下kernel收到一个sync包之后是怎么做的

好吧,先去看icsk->icsk_af_ops->conn_request这个函数是怎么来的。

回过头来看inetsw_array发现其中SOCK_STREAM中类型的prot字段其实是指向tcp_prot结构体的。

前面看过的的inet_create函数中的最后部分会调用sk->sk_prot->init函数。而sk_prot字段其实是通过调用sk_alloc时将inetsw_array中的prof字段赋值过去的。

因此在inet_create函数的最后sk->sk_prot->init调用的实际上是tcp_v4_init_sock函数。而在tcp_v4_init_sock函数中会将icsk->icsk_af_ops的值赋值ipv4_specific的地址。由此终于找到了icsk->icsk_af_ops->conn_request其实就是tcp_v4_conn_request函数,此函数随即调用tcp_conn_request函数来完成之后的内容。

在tcp_conn_request中是通过sk_acceptq_is_full来判断的。
从sk_acceptq_is_full函数中看到他是通过sk_max_ack_backlog字段判断的,而这个字段在我们分析listen系统调用时已然看到其实就是listen传入的那个值。

另外需要额外说明的时,在reqsk_queue_alloc中为的listen_sock::syn_table分配的空间仅仅是一个hash表,并不是际的request_sock空间。

如何做到宽容的收

在《unix编程艺术》提到过,Postel规定过:“宽容的收,谨慎的发”。

当时看到这一段话时,并没有多加思考,以为仅仅是对于网络协议多加检查就好了,并没有深想。

然而经过这两年的实践,我发现其实这条准则可以适用于更多的领域。

对于一个进程来说,他的输入可能是标准输入、管道、网络、配置文件等这些外部输入源。

那么进程所表现出来的宽容就是,不管从外部输入源接收到任何数据,都不应该影响程序的正常执行。即不会由于错误的数据格式或内容,造成崩溃,污然数据等副作用。

实现没有外部输入源程序时,我习惯上会信任设计正确的数据。

即,设计上在使用这个数据时,数据是合法的,那么一般不会再多加检查是否合法,最多加个assert去保护一下。

因为我觉得如果设计上正确的数据,如果出了问题,一定是设计出了问题,加上数据有效性检测并不能解决根本问题。此时需要做的是去修正设计,而非在局部打补丁。

而第一次接触到有外部输入源程序的那一段时间,由于没有经验,并没有区分哪些是内部数据,哪些是外部数据。所有的数据只要是设计正确的,全部都被信任,以至于在输入非法数据时,逻辑错误,数据紊乱。后来简直是草木皆兵,所有使用数据的地方都对其进行合法性判断。

经过几次实践并思考之后,发现其实可以将整个进程提象成一个整体,所有的外部输入源可以抽象成几个可以流入数据的口子。

然后抽象出一个筛子层,将筛子置于各外部输入源的口子处。筛子层负责对口子输入的数据进行仔细校验合法性,以确保通过筛子的的数据都是有效的合法数据。

由于所有的外部输入数据已经经过筛子过虑, 在此在实现真正的逻辑层时,完全可以和以前一样,直接信任设计正确的数据。这样在写逻辑代码时就不再需要提着思想的包袱,时时刻刻想着是否需要验证数据的合法性了。可以让我们更专注于逻辑设计本身。

然而,由于外部数据的合法性校验必然与具体逻辑密切关联,因此筛子层必然与逻辑层耦合在一起。

不过提象出筛子层的意义并不是用于与逻辑层分离。而是为了抽象出一个屏障,以清晰指明,哪些数据是由外部输入得来,需要仔细判断是否正确,哪些数据是程序内部自己产生,可以完全信任。

此准则不仅仅适用于进程,还适用于框架和库。

一般来说一个框架或库,一定是用于解决某个问题而生的。 即然抽象成框架或库,那么一定是希望别人能够快速使用,并且尽可能的屏蔽细节。因此,我们可能仅仅使用框架提供有很的几组API就可以实现一个很强大的功能。

但是由于这样或那样的原因,可能别人在开发过程中并没有完全领会这组API的输入数据的要求。如果框架不能宽容的收,框架的使用者就可能面临着很艰难的局面。

框架宽容的收,并不是说像进程收到非法数据一样,保证进程的稳定运行。由于框架是向业务逻辑提供功能的一个组件。因此,如果框架被误用,必须要尽快的将问题暴露出来。

因此,我觉得框架的宽容,应该仅仅保证不会因为非法数据造成误动作及污染数据等副作用。在一些特殊的情况下是可以直接崩溃,以尽快提醒业务逻辑的实现者此处的错误。

总而言之,不管是对于进程还是框架,只要是一个完整的个体,一定是自成闭环的。即,只靠自己个体内的数据,就有足够的能力确认当前外部输入数据是否合法。