这篇文章对 TCP Backlog 的说明写的特别好,How TCP backlog works in Linux 强烈建议看一看,希望这篇文章的地址不要变化,方便以后回顾。

Linux 上 TCP 握手的时候为每个独立进程维护了两个队列,一个是 Server 在收到 Client 的 SYN 并回复 SYN/ACK 之后,会将连接放入一个 incomplete sockets queue,这个队列的大小受到 net.ipv4.tcp_max_syn_backlog 的限制,超过这个队列长度之后 Server 再收到 SYN 就不会做响应,等 Client 超时之后会重传 SYN 再次尝试建立连接。需要注意的是在 net.ipv4.tcp_syncookies 置位时 net.ipv4.tcp_max_syn_backlog 参数会失效。net.ipv4.tcp_syncookies 的相关信息在下面专门说。

Client 收到 Server 的 SYN/ACK 后会回复 ACK 以确认连接建立,之后 Server 收到 Client 的 ACK 时 TCP 连接完成握手工作。Server 将连接从 incomplete sockets queue 中移出来放入另一个进程相关的等待队列称为 completely established sockets queue,在应用调用 accept 系统调用之后移出。completely established sockets queue 队列大小由应用调用 listen 系统调用时传入的 backlog 值决定。如果应用层调用 accept 不及时,新建立的连接会在 completely established sockets queue 堆积,当 completely established queue 满了之后,如果 net.ipv4.tcp_abort_on_overflow 置位,Server 无法将新创建的连接放入 completely established queue,在收到 Client 在 TCP 握手阶段最后一个 ACK 时会直接给 Client 回复 RST 关闭整条连接;如果 net.ipv4.tcp_abort_on_overflow 为 0 (默认为 0),则队列 completely established queue 满了之后 Server 不做任何事情,直接丢弃 Client 发来的 ACK,连接依然会放在 incomplete socket queue 中。

此时在 Client 看来它并不知道 ACK 被 Server 忽略了所以认为连接已经处在 Established 状态,但是连接实际是 Half-Open 的。需要注意的是 ACK 并没有重传机制,Client 发完 ACK 到 Server 后不会等着这个 ACK 被 Server ACK,而是会直接尝试发送实际数据给服务端。服务端收到数据后会将数据丢弃并重发 SYN/ACK。Client 收到 SYN/ACK 会再次回复 ACK 和重发应用层数据,如果 complete established sockets queue 一直是满的,Server 会一直这么丢弃 Client 的 ACK 并在每次收到 Client 数据时候回复 SYN/ACK 直到回复 SYN/ACK 的次数达到 net.ipv4.tcp_synack_retries 上限,接下来 Server 收到 Client 数据就不回复 SYN/ACK 而是直接回复 RST 重置整个连接。

除了 Client 在发实际数据到 Server 时 Server 会重发 SYN/ACK 之外,如果此时 Server 没有开启 net.ipv4.tcp_syncookies,Server 会有个超时时间超时后会重发 SYN/ACK,开启 SYN Cookies 之后 Server不会有这个超时重发,只有在收到 Client 数据的时候才会重发 SYN/ACK。这个下面会再次提及。

在 completely established sockets queue 满了之后,系统还会减慢接收新 Client 发来 SYN 的速度,也就是说即使 incomplete sockets queue 有空间,当 completely established sockets queue 满了之后,系统在收到 SYN 之后也会开始按一定比率丢弃 SYN 而不是回复 SYN/ACK 并将连接放入 incomplete sockets queue。也就是说 completely established queue 的长度是会影响到 incomplete sockets queue 的。下文会说 completely established sockets queue 的长度主要由应用调用系统调用 listen 时设置的 backlog 和系统的 net.core.somaxconn 参数决定,有人认为 incompletely socket queue 的长度是 backlog、net.core.somaxconn、net.ipv4.tcp_max_syn_backlog 三个参数中的最小值,实际这个倒不一定,completely established sockets queue 长度只是会影响 incomplete sockets queue 长度,但不是说 incomplete sockets queue 最长就是和 completely established sockets queue 一样。

Linux 上 listen 系统调用上有关于 backlog 的说明:

1
2
3
The behavior of the backlog argument on TCP sockets changed with Linux 2.2. Now it specifies the queue length for completely established sockets waiting to be accepted, instead of the number of incomplete connection requests. The maximum length of the queue for incomplete sockets can be set using /proc/sys/net/ipv4/tcp_max_syn_backlog. When syncookies are enabled there is no logical maximum length and this setting is ignored. See tcp(7) for more information.
If the backlog argument is greater than the value in /proc/sys/net/core/somaxconn, then it is silently truncated to that value; the default value in this file is 128. In kernels before 2.4.25, this limit was a hard coded value, SOMAXCONN, with the value 128.

需要着重关注第二段,listen 传入的 backlog 值会受到系统参数:net.core.somaxconn 的影响,如果 net.core.somaxconn 设置的小,调用 listen 时传入的 backlog 再大都没有用,都会默认使用系统的 net.core.somaxconn 值。大名鼎鼎的 Netty 默认的 backlog 值就是读取的系统 somaxconn 参数,参看这里

从上面描述能看出来这个 backlog 值对会在短时间内建立大量连接的服务很重要,如果 backlog 非常小,服务很可能还没来的急执行 accept 呢 backlog 就满了,于是开始丢弃 Client 发来的 ACK,并且在收到 Client 数据包或者 Server 超时时重发 SYN/ACK。因为 Server 收到 Client 数据包后会直接丢弃数据,所以会给网络带来不必要的开销。另外 Server 超时重发 SYN/ACK 的话等待时间会很长,并且重传是 exponential backoff 的,等待时间会越来越长。

不过这两个开销还不是最主要的,网络开销也就是影响一些带宽,Server 的超时一般不存在因为一般会开启 SYN Cookies,最重要的影响是上面说过 completely established sockets queue 满了之后 Server 会按一定比率丢弃 Client 发来的 SYN,Client 不知道发出去的 SYN 被丢弃了,必须等足一个超时时间之后才会再次发送 SYN,一般这个超时时间是 3s,并且是 exponential backoff 的,第二次发 SYN 如果还被 Server 丢弃就要等足 9s 才会再次重发 SYN,这个时间长度就很讨厌了,在用户那里直接感受就是服务好慢,连半天连不上。这篇文章里说的 Backlog 小了之后在有大量并发连接的时候 Client 会出现 3s,9s 的延迟就是因为这个原因。 如果连接已经进入 incomplete sockets queue,Client 只要发数据上来服务端就会立即重传 SYN/ACK,所以不会直接产生延迟。

Backlog 修改方法

如果服务执行能力足够,稍微大一点的 backlog 值是有助于提高系统建立并发连接能力的。但很不幸的是,默认情况下 net.core.somaxconn 的值非常的小,只有 128,在高并发系统下很可能会让 Client 超时,很多人建议是调整到 1024 或者 2048,根据需要和测试情况也有调整到 4096 的。一般建议是将 net.core.somaxconn 设置的稍微大一些,将其作为上限来设置,这样应用可以根据自己需要在执行 listen 的时候将这个值调小。

另外需要说明的是,backlog 最大值是 65535,这个上限是内核规定的但没有文档明确说明。并且 backlog 值并不是越大越好,系统维护的建立连接的两个队列是有资源消耗的,一个是会吃一些内存,一般说是一个 entry 是 64 字节内存;另一个是会消耗 CPU,排在 completely established sockets queue 的连接都是合法的已经完全建立的连接,随时都可能有数据发上来,数据发上来后 Server 就要消耗 CPU 做处理即使这个连接还没被应用层 accept

除了消耗资源之外,backlog 如果特别大超过应用处理能力,应用要很久才能把 backlog 清空,那这个时间 Client 可能已经超时,甚至认为连接已经失效而执行了 TCP 断开连接的流程,这个时候 Server 处理完 backlog 再想往连接写数据就写不下去了,可能报 Broken pipe。相当于 Server 费半天劲在做无用功,Client 超时后如果还有重试机制会加重 Server 负担恶性循环。

临时修改的话执行:
sysctl -w net.core.somaxconn=2048
sysctl -w net.ipv4.tcp_syncookies=1
sysctl -w net.ipv4.tcp_max_syn_backlog=65535

永久性修改的话需要修改 /etc/sysctl.conf 文件,将上面修改值写在文件中。

怎么看出来服务的 backlog 设置太小了?

netstat -s 是个神器,搞连接参数相关优化的时候这个能提供不少帮助。它实际读的是 /proc/net/netstat 这个文件,里面记录着系统内各种和网络相关的统计信息。跟 backlog 相关的是两条:

1
2
167480 times the listen queue of a socket overflowed
258209 SYNs to LISTEN sockets dropped

这两个数据分别对应着内核的 LINUX_MIB_LISTENOVERFLOWS 和 LINUX_MIB_LISTENDROPS 两个统计信息。一个还挺好用的看内核代码的地方是:linux/net/ipv4/tcp_input.c - Elixir - Free Electrons,可以搜索一下看看都是什么地方在更新这两个统计信息。

目前来看 LINUX_MIB_LISTENOVERFLOWS 都是因为 completely established sockets queue 满了丢弃 ACK 数据时会记录。LINUX_MIB_LISTENDROPS 被记录的地方太多了,有太多地方会出现 SYN 被丢弃,比如 SYN 本身格式不对,所以 LINUX_MIB_LISTENDROPS 大多数时候是大于 LINUX_MIB_LISTENOVERFLOWS 的。在网上看到很多人分析应用层 Backlog 问题的时候看的是 LINUX_MIB_LISTENDROPS 这个记录,这个是不对的,得看 LINUX_MIB_LISTENOVERFLOWS 这个。

能确认的跟 backlog 相关,会计入 LINUX_MIB_LISTENDROPS 的是下面这些地方:

  1. completely established queue 满了导致 SYN 被丢弃时;
  2. incomplete sockets queue 满了导致 SYN 被丢弃时;

所以当 netstat -s 中 XXX the listen queue of a socket overflowed 值比较大的时候就很有可能是 backlog 不合适,或者至少说明应用层没有来得及处理大量的并发连接而导致这些连接 TCP 握手时的 ACK 被丢弃了。

net.ipv4.tcp_syncookies 是干什么的?

前面说过 incomplete sockets queue 满了之后默认行为是丢弃 Client 发来的 SYN,这就给不法分子提供了一条进行恶意攻击的途径,参看: SYN flood - Wikipedia

SYN Cookie 就是用来应对 SYN flood 的,net.ipv4.tcp_syncookies 是开启 SYN Cookie 的配置,Linux 系统会默认开启这个配置。SYN Cookie 相关内容参看这里:SYN cookies - WikipediaMSS这个 wiki 里介绍的针对 SYN Cookie 的攻击挺神奇的,可以看看

基本原理就是 Server 收到 SYN 在构造 SYN/ACK 的时候,会将 Client SYN 内的一些本来应该存在 Server 的 incomplete sockets queue 内的信息编码到 SYN/ACK 的 Sequence Number 里,这样 Server 就能完全废弃 incomplete socket queue 而在 Client 回复 ACK 的时候从 ACK 的 Sequence Number 中恢复 Client 在 SYN 中的信息,从而正常建立连接。SYN flood 时候攻击者一般只是发 SYN 而不会在收到 Server 的 SYN/ACK 的时候回复 ACK,所以 SYN Cookie 机制相当于减少了 Server 为每个收到的 SYN 保留信息的开销,并能区分出攻击者和普通用户,从而能解决 SYN flood 问题。

SYN Cookie 的成本在上面 wiki 页面也有详细描述,主要是 SYN 中会有一些 Option 字段,不能完全编码到 Sequence Number 中,一个比较关键的就是 Maximum segment size - Wikipedia。Sequence Number 中只给 MSS 留了三个 bit 的位置做编码,所以开启 SYN Cookie 之后 Server 支持的 MSS 最多只有 8 个。内核中有个叫做 msstab 的表,记录了系统内在开启 SYN Cookie 之后支持的 MSS 值。有人专门做过研究确定下来几个固定值写在 msstab 中,据说基本能覆盖绝大多数情况,所以一般认为虽然 MSS 选择少了很多但能避免 SYN flood 还是很值得的,所以 SYN Cookie 默认为开启。只有对于 MSS 特别大的网络(因为一般内核默认的 MSS 为了覆盖大多数情况设置的值都比较小,基本都在 1500 以下)开启了 SYN Cookie 之后会导致本来网络能发特别大的数据包但因为 MSS 限制而不能发,因为每个数据包都会有 TCP Header、IP Header 等信息使得网络的 overhead 升高。

在前文中说过,当 completely established sockets queue 满了之后,如果进程来不及执行 accept,收到 Client 的 ACK 后连接不会从 incomplete socket queue 中移除,而是会丢弃 ACK。正常来说 Server 回复的 SYN/ACK 是有超时机制的,在丢弃了 Client 发来的 ACK 之后会等 SYN/ACK 超时再次发送 SYN/ACK 给 Client,但是开启 SYN Cookies 之后,incomplete sockets queue 被完全废弃,Server 在收到Client 的 SYN 并回复 SYN/ACK 之后就把这个 Client 完全忘记了,所以 SYN Cookies 开启后是不会有 Server 的 SYN/ACK 超时的,必须等待 Client 主动发数据到 Server 后 Server 才会重发 SYN/ACK。对于有些应用协议如果期待 TCP 握手之后 Server 先发个数据到 Client 的话,会需要额外的超时机制去让 Client 知道自己发的 ACK 丢失了。

对 MSS 再多记录一些东西吧,TCP 的 Option 格式 在这里有说明,每个 Option 都可能有三个 field,Option-Kind、Option-Length、Option-Data。Option-Kind 就是说这个 Option 是什么 Option,Option-Length 就是这个 Option 总共占了多少字节,Option-Data 就是 Option 的值是什么。拿 MSS 来说,比如抓包得到 TCP Option 如下,Option 全长 20 字节:

1
2
0000 02 04 05 84 04 02 08 0a 6b 73 a5 ca 3b 43 02 f7 ........ ks..;C..
0010 01 03 03 07 ....

02 是 Option-Kind 表示 MSS 这个 Option,04 表示 Option-Length 是四字节,也就是说 MSS 这个 Option 就是上面前四个字节 02 04 05 84,整个 Option 是 4 字节,Option-Kind 和 Option-Length 各占一个字节,05 84 就是 Option-Data 在十进制下值为 1412 。

SYN Cookies 相关统计

netstat -s 中有三个东西跟 SYN Cookies 相关:

1
2
3
909660620 SYN cookies sent
867502891 SYN cookies received
635627953 invalid SYN cookies received

从字面意思也能大概理解这三个东西是干什么的。SYN cookies - Wikipedia 这里介绍过一个通过生成随机的 Sequence Number 伪造 Client 的 ACK 从而连上 Server 未开启的端口,但这个攻击会产生大量的 invalid SYN cookies received 所以如果这个值短时间内大量增加有可能是正在遭受攻击。

监控 SYN cookies sent 和 SYN cookies received 值能提前判断是否遭受 SYN flood 攻击,正常来说 Sent 和 Received 是差别不大的,用户收到 SYN/ACK 之后大部分时间都会回应 ACK,而 SYN flood 攻击时攻击者为了不建立连接从而在本机产生消耗所以不会回复 ACK,从而出现服务端在遭受 SYN flood 时,SYN cookies sent 短时间内大量增加但是 SYN cookies received 变化幅度不大的现象。

一些参考

强力推荐 How TCP backlog works in Linux
强力推荐 https://vimeo.com/70369211
tcp_max_syn_backlog | VOIP Magazine
linux - Will increasing net.core.somaxconn make a difference? - Server Fault
Part 1: Lessons learned tuning TCP and Nginx in EC2 « Chartbeat Engineering Blog
Part 2: Lessons learned tuning TCP and Nginx in EC2 « Chartbeat Engineering Blog