如何处理 SYN
Table of Contents
SYN 是 TCP 三次握手的一部分,开发网络应用时通常不会关注,但它与请求中偶发的长时延 (latency spike) 密切相关,是服务器维护环节中不可忽视的重要部分。如果 SYN 在发送过程中丢包了,通常客户端会在 1s, 3s, 7s, 15s, 31s 后重,这就是长延迟的来源之一。备受游戏公司困扰的 SYN Flood 攻击也是利用了 SYN 的特点。
1 Linux 是如何处理 SYN 的
如下面 TCP 状态图 的上部分 所示,当应用 listen() 之后,就可以接收 SYN 并发送 SYN+ACK,进入 SYN RECEIVED 状态;等待收到 ACK 之后,进入 ESTABLISHED 状态。之后就可以被应用 accept()。
这意味着一个 TCP 连接的建立过程里,有两个等待: 1 是等待 ACK,2 是等待应用 accept()。Linux 使用 2 个队列实现这两次等待, 队列中保存 struct inet_request_sock 结构。我们不考虑特殊情况,如 TCP_SAVED_SYN, TCP_DEFER_ACCEPT, TCP_FAST_OPEN 。
第一个队列是 SYN queue (也叫未完成队列),当收到 SYN 并返回 SYN+ACK 之后,连接会保存在这个队列中,并等待 ACK,连接进入 SYN RECEIVED 状态。等待 ACK
超时的话会重传 net.ipv4.tcp_synack_retries
次。由于 SYN Cookie 的存在,这个队列的长度并不十分重要。
第二个队列是 Accept queue (也叫完成队列),当收到 ACK 以后,连接从 SYN queue
移除,保存到 Accept queue 中,进入 ESTABLISHED 状态,等待应用调用 accept()
。如果队列满了会发送 RST。
SYN Queue 的长度由 /proc/sys/net/ipv4/tcp_max_syn_backlog
控制。 Accept queue
的长度由 listen()
的第二个参数 backlog
限制,并且不能大于
/proc/sys/net/core/somaxconn
。 somaxconn
在 Linux 5.4 之前默认值是 128,5.4
之后改成了 4096。
2 最佳 Accept Queue 长度是什么
这个问题经常被提出: backlog 应该设置为多少?
答案是看情况。大多数情况这个参数不重要,Golang 1.11 之前甚至不能改变这个参数。如果新建连接非常频繁,或者设置了 TCP_DEFER_ACCEPT,可以设置得大一些。但设置过大也是不对的,1个 inet_requst_sock
占用 256 字节呢。
有些系统管理员会建议设置一个非常大的 backlog 而且让 net.core.somaxconn
也非常大。这实际上是掩盖了应用程序的问题,如果应用程序一直来不及消费,队列大也没有多少意义,反而会引起客户端的长时延。不如设置一个短的队列,用 RST
尽早告诉客户端出了某些问题,可能需要重连,如果设置了合适的负载均衡器,重连时可能就是好的。
我一般设置 listen(fd, -1)
,然后根据情况修改 somaxconn
的值。
使用下面的命令可以查看某个端口的 SYN Queue 元素数量:
ss -n state syn-recv sport = :80 | wc -l
使用下面的命令可以查看某个端口的 Accept Queue 元素数量,其中 Recv-Q 就是 Accept Queue 元素数量,而 Send-Q 是 backlog 参数。
ss -plnt sport = :80
有几个变量可以查看队列满的次数:
- TcpExtListenOverflows 是 Accept Queue 满而无法服务的次数
TcpExtTCPReqQFullDoCookies 是 SYN Queue 满而转为 Syn Sookie 的次数。
使用下列命令查看这些变量:
nstat -az TcpExtListenOverflows
根据实际需要调整 somaxconn 即可。
3 SYN Flood 和 SYN Cookie
1996 年之前,不停地发送 SYN 可以另服务器 SYN Queue 满从而停止服务,直到 SYN Cookie 出现。这种攻击就是 SYN Flodd,只需要少量的带宽,发送不算太多的报文,就可以让 SYN Queue 满,简单有效:
因为 ACK 的 ack 值就是 SYN+ACK 的 seq 值,可以SYN的哈希值保存到 seq 里,收到 ACK 的时候再验证。这样就不再需要 SYN Queue 了:
+----------+--------+-------------------+ | 6 bits | 2 bits | 24 bits | | t mod 32 | MSS | hash(ip, port, t) | +----------+--------+-------------------+
开启 syn cookie ( net.ipv4.tcp_timestamps=1
,默认开启) 之后,SYN Queue
满时会启用这个功能,用以防范 SYN Flood。比较麻烦的问题就是,这会向网络中发送大量没用的 SYN+ACK。
Linux 4.4 之前,SYN 的处理是很慢的,然后他们把一个内核锁去掉了,现在基本不需要过度担心 SYN Flood 的问题。