如何处理 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-state.png

这意味着一个 TCP 连接的建立过程里,有两个等待: 1 是等待 ACK,2 是等待应用 accept()。Linux 使用 2 个队列实现这两次等待, 队列中保存 struct inet_request_sock 结构。我们不考虑特殊情况,如 TCP_SAVED_SYN, TCP_DEFER_ACCEPT, TCP_FAST_OPEN 。

syn-queue.png

第一个队列是 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/somaxconnsomaxconn 在 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 的问题。


By .