有个新网站专门做外贸的叫什么,房产信息网官方,网站设计做微信发现界面,长春建设在上一篇博客#xff08;EMQX 性能调优#xff1a;最大连接与文件描述符#xff09;#xff0c;我们深入研究了 MQTT 连接与文件描述符之间的关系#xff0c;介绍了如何修改文件描述符相关的内核参数来突破默认的最大连接数量限制。
但你可能会发现#xff0c;在某些情况…在上一篇博客EMQX 性能调优最大连接与文件描述符我们深入研究了 MQTT 连接与文件描述符之间的关系介绍了如何修改文件描述符相关的内核参数来突破默认的最大连接数量限制。
但你可能会发现在某些情况下即便当前服务端的 MQTT 连接总数并未达到文件描述符限制客户端的连接请求仍然失败。当你运行以下命令你将看到以下 Overflowed 和 SYN Dropped 计数在不断增加
$ watch -d netstat -s | grep -i listen2091 times the listen queue of a socket overflowed3418 SYNs to LISTEN sockets dropped本文将介绍导致这一现象的原因以及如何通过内核参数调优来解决此问题。
SYN 队列与 Accept 队列
前文提到的这两个计数通常意味着 SYN 队列和 Accept 队列发生了溢出一旦溢出客户端的正常连接就会受到影响。
所以首先我们需要了解 SYN 队列和 Accept 队列分别是什么
MQTT 作为构建在 TCP 之上的应用层协议我们总是必须先建立 TCP 连接然后才能发送 MQTT 协议的 CONNECT 报文。建立 TCP 连接需要经过三次握手 客户端向服务器发送 SYN同步报文表示要建立连接。服务器用 SYN-ACK同步确认报文进行响应表示愿意建立连接。客户端回复 ACKAcknowledge报文表示已收到 SYN-ACK 报文连接已建立。
由于网络延迟的存在从服务端发出 SYN-ACK 报文到收到客户端回复的 ACK 报文总是需要一段时间。在这段时间内服务端需要暂存本次连接的关键信息例如 TCP 四元组、MSSMaximum Segment Size最大分段大小和窗口缩放因子Window Scale。所以Linux 内核维护了一个 SYN 队列来存放这些连接信息。
当服务端收到客户端回复的 ACK 报文连接建立完成就会将连接从 SYN 队列取出然后放入同样由内核维护的 Accept 队列直到上层应用调用 accept() 将连接从 Accept 队列中取出。Accept 队列在这里的主要作用是解耦了网络层和应用层使 TCP 三次握手和实际的数据传输可以并行进行有效提高连接请求的处理效率。 但服务端的资源是有限的不管是 SYN 队列还是 Accept 队列它们都有着最大长度限制。如果客户端一直只发送 SYN 报文而不回复最后的 ACK 报文或者服务端的应用程序没有及时地调用 accept()那么这两个队列就会有溢出的风险。
SYN 队列溢出时将发生什么
服务端在 SYN 队列溢出时的行为主要由 net.ipv4.tcp_syncookies 选项决定。
由于 SYN 队列的长度总是有限的所以一些攻击者会尝试采用发送大量 SYN 报文的方式来对服务端发起攻击企图耗尽服务端的 SYN 队列来阻止合法连接的建立这也就是我们常说的 SYN 泛洪攻击 (SYN Flood Attack)。
SYN Cookie 机制被设计用于解决这一问题。简单来说当启用这一机制后Linux 在收到 SYN 报文时将基于时间戳、四元组等信息计算出一个 Cookie然后作为 SYN-ACK 报文的序列号后返回给客户端。客户端在 ACK 中将序列号加一返回服务端只需要减一就可以逆推出原始的 Cookie。因此服务端不需要再将连接请求放入 SYN 队列。 但 SYN Cookie 机制也存在一些弊端
Cookie 的计算并未包含 SACKSelective Acknowledgment选择性确认 和 Window Scale 窗口缩放因子这些 TCP 选项启用 SYN Cookie 后服务端将不会保存这些选项所以这些功能将无法使用。虽然从 Linux 内核 v2.6.26 开始我们可以启用 TCP Timestamps 选项net.ipv4.tcp_timestamps借助 32 Bit 时间戳的低 6 bit 来存放这些 TCP 选项但 TCP Timestamps 需要客户端和服务端的共同支持才会真正启用。用于生成 Cookie 的 Hash 计算增加了服务端的负载。
所以目前 net.ipv4.tcp_syncookies 选项一共有三个可取值
net.ipv4.tcp_syncookies 0表示关闭 SYN Cookie 机制如果 SYN 队列已满那么新到的 SYN 报文将被丢弃。net.ipv4.tcp_syncookies 1表示 SYN Cookie 机制仅在 SYN 队列满时才正式启用。net.ipv4.tcp_syncookies 2表示无条件启用 SYN Cookie 机制。
不同类型和版本的操作系统中net.ipv4.tcp_syncookies 的默认值可能不同你可以运行以下命令查看当前值
sysctl -n net.ipv4.tcp_syncookies考虑到 SYN Cookie 可能带来的副作用通常我们建议仅在 SYN 队列满时才启用 SYN Cookie将 net.ipv4.tcp_syncookies 设置为 1优先尽可能地增加 SYN 队列最大长度。运行以下命令以修改此选项
sysctl -w net.ipv4.tcp_syncookies1RTT 对 SYN 队列的影响
连接请求在 SYN 队列中停留的时间基本等同于服务端发出 SYN-ACK 报文到收到客户端返回的 ACK 报文的时间。换句话说它完全取决于报文在客户端与服务端之间的往返时间Round Trip TimeRTT。
RTT 越长那么连接请求将越容易占满 SYN 队列。假设 RTT 为 200 msSYN 队列最大长度为 512那么只要每秒向服务端发起连接请求的客户端数量超过 2560 个就会造成 SYN 队列溢出。
查看当前 SYN 队列大小
Linux 没有提供相应的内核参数供我们直接查看当前 SYN 队列大小但通过前文我们可以知道 SYN 队列中的连接都处于 SYN-RECEIVED 状态因此我们可以借助 netstat 命令统计 SYN-RECEIVED 状态的连接数量来间接获得当前 SYN 队列的大小
sudo netstat -antp | grep SYN_RECV | wc -l如何确认 SYN 队列发生溢出
当 SYN 报文因为 SYN 队列满而被丢弃时服务端中的以下计数会相应增加
$ netstat -s | grep LISTENNumber SYNs to LISTEN sockets dropped不过 SYN 报文也可能因为 Accept 队列满而被丢弃所以还需要结合 Accept 队列的情况综合判断。
需要注意的是在 CentOS 中即便启用了 SYN Cookie 机制服务端已经不会再丢弃 SYN 报文此 SYN 丢弃计数仍然可能增加
SYN Cookie 设置为 1 时如果 SYN 队列已满那么新到的 SYN 报文仍然会使此计数增加。这可以帮助我们评估是否需要增加 SYN 队列的最大长度。SYN Cookie 设置为 2 时此计数不存在任何意义。
Accept 队列溢出时将发生什么
服务端在 Accept 队列溢出时的行为主要由 net.ipv4.tcp_abort_on_overflow 选项决定。
通常情况下此选项的默认值为 0即当 Accept 队列溢出时服务端将直接丢弃第三次握手的 ACK 报文并视作从未收到该 ACK 报文因此服务端将重传 SYN-ACK 报文最大重传次数由 net.ipv4.tcp_synack_retries 选项决定。 虽然服务端丢弃了 ACK 报文但是对客户端来说三次握手已经完成所以它可以发送后续的应用数据。不过这些携带了应用数据的 PSH 也会和 ACK 报文一样被服务端直接丢弃。由于收不到响应客户端将不断地重传 PSH 报文PSH 报文的最大重传次数由 net.ipv4.tcp_retries2 选项决定。 将 net.ipv4.tcp_abort_on_overflow 设置为 0 的好处是如果在 SYN-ACK 或 PSH 报文达到最大重传次数前上层应用及时地取出连接使 Accept 队列出现空位那么连接可以直接恢复这更有利于应对突发流量。但如果 Accept 队列过短导致客户端和服务端过早地重传报文也会浪费流量以及降低连接效率。
相反如果将 net.ipv4.tcp_abort_on_overflow 设置为 1那么服务端将在 Accept 队列溢出时直接向客户端返回 RST 报文来关闭连接。 所以通常我们建议将 net.ipv4.tcp_abort_on_overflow 设置为 0除非你确信服务端在短时间内无法从繁忙中恢复并且希望尽快通知客户端。 不同类型和版本的操作系统在这方面的行为可能不同。以上行为在 CentOS 中得到验证但 Ubuntu 似乎采用了另一种行为模式并且更改 net.ipv4.tcp_abort_on_overflow 似乎并不会改变 Ubuntu 的行为。 如果你对此感兴趣我们在 这里 提供了用于模拟 Accept 队列溢出的示例代码和操作步骤你可以在自己的环境中自行试验。 另外当 Accept 队列溢出时即使启用了 SYN Cookie 机制服务端也不会再接受新的连接请求即到达服务端的 SYN 报文将被直接丢弃这会使得 SYN 丢弃计数增加。
查看当前 Accept 队列大小
我们可以使用 ss 命令来查看当前 Accept 队列的情况。对于监听状态的套接字ss 命令获得的第二列 Recv-Q 表示当前 Accept 队列的大小第二列 Send-Q 则表示 Accept 队列的最大长度
$ ss -lnt
LISTEN 0 1024 *:1883 *:*如何确认 Accept 队列发生溢出
每当服务端因为 Accept 队列溢出而丢弃报文时不管是第一次握手的 SYN 报文还是第三次握手的 ACK 报文又或者是 PSH 报文服务端中的以下计数都会相应加 1
$ netstat -s | grep overflowedNumber times the listen queue of a socket overflowed因此我们可以通过观察这个计数是否增长来判断 Accept 队列是否发生溢出。
如何增加 SYN 队列和 Accept 队列的大小
Accept 队列比较简单它的最大长度由监听函数例如 listen(fd, backlog)中的 backlog 参数和 net.core.somaxconn 这个 Linux 内核参数决定。Linux 总是取 backlog 和 net.core.somaxconn 中对的较小值作为 Accept 队列的最大长度。
在 EMQX 中我们可以为每个监听器都单独设置 backlog以默认的 TCP 监听器为例我们只需要在 emqx.conf 中添加以下配置即可
listeners.tcp.default {tcp_options {backlog 1024}
}如果想要修改其他监听器的 backlog只需要使用对应的协议名和监听器名称即可
listeners.[tcp | ssl | ws | wss | quic].Listener Name {tcp_options {backlog 1024}
}SYN 队列略为复杂它的最大长度并不由某个内核参数直接决定而是受到 net.ipv4.tcp_max_syn_backlog、net.core.somaxconn 等参数的综合影响。在不同的操作系统中这些参数的效果还会有所差异。在 CentOS 中SYN 队列的最大长度有着以下计算公式
Max SYN Queue Size roundup_pow_of_two(max(min(somaxconn, backlog, sysctl_max_syn_backlog), 8) 1)roundup_pow_of_two(Num) 表示将 Num 向上取整到 2 的幂。例如当 Num 为 67 或 8 时roundup_pow_of_two(Num) 总是返回 8。
因此如果我们将 somaxconn 设置为 64tcp_max_syn_backlog 设置为 128而 listen() 函数的 backlog 设置为 256 时那么在 CentOS 中最终 SYN 队列的最大长度将是 256。
而在 Ubuntu 中SYN 队列的长度必须小于 Accept 队列的最大长度并且小于等于 0.75 倍的 net.ipv4.tcp_max_syn_backlog。我们可以转换为以下公式
Max SYN Queue Size min(min(somaxconn, backlog), 0.75 * tcp_max_syn_backlog 1)如果我们将 somaxconn 设置为 64tcp_max_syn_backlog 设置为 512而 backlog 设置为 256 时Accept 队列的最大长度为 64小于 0.75 倍的 tcp_max_syn_backlog所以此时 SYN 队列的最大长度为 64。
如果我们将 somaxconn 设置为 1024tcp_max_syn_backlog 设置为 256backlog 设置为 512 时Accept 队列的最大长度为 512大于 tcp_max_syn_backlog所以此时 SYN 队列的最大长度为 193。
验证 SYN 队列最大长度
我们可以通过以下方式来验证 SYN 队列的最大长度
首先我们需要一个简单的 TCP 服务端它监听 12345 端口但从不调用 accept() 从 Accept 队列中获取连接。注意将 backlog 修改为你期望的值
import socket
import timedef start_server(host, port, backlog):server_socket socket.socket(socket.AF_INET, socket.SOCK_STREAM)server_socket.bind((host, port))server_socket.listen(backlog)print(fServer is listening on {host}:{port})while True:time.sleep(3)if __name__ __main__:start_server(0.0.0.0, 12345, 256)然后我们需要在服务端进行以下操作
# 如果你使用的是 Ubuntu那么需要关闭 SYN Cookie以便接下来可以看到计数变化
echo 0 /proc/sys/net/ipv4/tcp_syncookies# 将 somaxconn 和 tcp_max_syn_backlog 设置为你期望的值
echo 64 /proc/sys/net/core/somaxconn
echo 512 /proc/sys/net/ipv4/tcp_max_syn_backlog# 为服务端的 eth0 接口设置 200ms 的网络延迟使 SYN 队列更容易溢出。
#
# 当你不再需要此延迟时你可以运行以下命令删除它
# sudo tc qdisc delete dev eth0 root
sudo tc qdisc add dev eth0 root netem delay 200ms完成以上设置后运行以下命令启动 TCP 服务器
python3 ./server.py另起一个终端窗口运行以下命令用于观察 SYN 队列是否溢出
watch -n 1 -d netstat -s | grep -i listen在客户端中安装 hping3 工具
apt-get install hping3运行以下命令以指定速率发送指定数量的 SYN 报文
# -S发送 SYN 报文
# -p Port指定端口
# -c Count发送报文的数量
# -i u100, 以 100us 的间隔发送报文
hping3 -S -p 12345 -c 65 -i u100 Your Hostname如果你使用的是 Ubuntu并且 somaxconn 等参数的值与以上示例保持一致那么在运行 hping3 命令后你将看到 SYN 丢弃计数加 1因为此时 SYN 队列最大长度为 64。
令改动永久生效
echo 64 /proc/sys/net/core/somaxconn 和 sysctl -w net.core.somaxconn64 都只是临时性的改动一旦用户注销或者系统重启我们改动就会失效。如果我们确认当前值满足最终期望那么可以将它们写入 /etc/sysctl.conf
net.core.somaxconn 4096
net.ipv4.tcp_max_syn_backlog 4096然后运行 sysctl -p 使改动立即永久生效。
总结
在现代操作系统中 net.ipv4.tcp_syncookies 通常默认为 1net.ipv4.tcp_abort_on_overflow 则通常默认为 0所以除了 Accept 队列溢出导致服务端拒绝后续连接请求以外我们很难直接观察到客户端连接失败的情况。
当 SYN Cookie 机制生效时虽然 SYN 报文不会再被丢弃但 TCP 的部分功能可能会受到限制并且服务端的负载会相应增加。特别在物联网、车联网这类 RTT 较高的场景中SYN 队列会更加容易溢出。所以及时关注 SYNs Dropped 计数并调整 SYN 队列大小是非常有必要的。
Accept 队列通常在服务端繁忙时更容易溢出tcp_abort_on_overflow 等于 0 的情况下较短的 Accept 队列可能会使客户端和服务端过早地进入报文重传反而增加网络负载。如果 Accept 队列不断溢出但服务端的 CPU 并未饱和那么可以适当增大 Accept 队列。
另外将 SYN 队列和 Accept 队列设置为一个非常非常大的值并不是一件好事。在遭受泛洪攻击或者突发流量导致服务端繁忙时对这两个队列施加合理的大小限制反而是对服务端的保护。
在后续的博客中我们将继续带来更多 Linux 系统中影响 EMQX 性能表现的内核参数的优化指南。