backlog | 重归混沌的BLOG

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空间。



发表评论