记一次线上高并发环境 TCP 握手丢包的故障处理

背景

业务场景需要有客户端通过 tcp 连接线上环境 emqx 集群环境,集群规模有 5 台node节点承载emqx业务,每台节点在业务端口上都有 15w 左右的tcp连接保持。
近期发现与 emqx 相关的业务功能会出现间歇性的连接等待状态,索性运维同学在内网环境进行网络层的连接测试,确实复现了连接间歇性延迟的现象。

问题现象

发现在内网节点 telnet 端口会出现间歇性的 Connecting... 状态
telnet_test

按理说内网环境不应该存在连接链路拥堵丢包等的情况,所以在其中一台 server 节点上进行抓包排查,看看 tcp 握手包是否正常到达目标节点。

排查

我们先排查到底这个丢包是在传输过程中的某一环丢包了,还是在 server 端丢包了。

tcpdump 抓包

我们先直接在目标节点进行 tcpdump 抓包
tcpdump
由此发现,数据包不存在传输途中丢包的情况,而是 server 端在已经收到 syn 握手包时,内核层在 TCP/IP 协议栈上直接就丢弃,没有响应 ack 。

头脑风暴过往类似现象可能的原因

记得,还在 Kernel 2.6.x(CentOS 5/6)的时候,处理过与这种现象类似的故障,当初也是在服务器入口层出现不同程度的收到 syn 但是协议栈不应答的现象。

我们知道,在内核参数中,有一个 net.ipv4.tcp_tw_recycle = 1 的配置开关,这个开关模式是出于关闭状态(即0),主要是为了解决服务器上 TIME_WAIT 连接状态过多的问题。

在 TCP 断联的四次挥手阶段,TIME_WAITTCP状态机的最后一步,主要是该状态是为了防止四次挥手阶段最后一个 FIN 包由于网络丢包导致对端没有收到的问题。

这个状态在Linux内核代码上直接硬编码了 60s 的状态保持时长,不可配置。

话说回来,这个参数生效的条件是 net.ipv4.tcp_timestamps = 1net.ipv4.tcp_tw_recycle = 1 两个参数均为打开状态,而且建连的tcp会话两端均需要有 TS Val 标志位,即都需要开启 tcp_timestamps 内核参数,其中 tcp_timestamps 默认为开启状态。

加之如今互联网设备大多都基于 Linux 内核或者其衍生版进行开发,TCP/IP 协议栈的逻辑早都被证明是非常成熟高效可靠的逻辑,tcp_timestamps 内核参数默认又是打开状态,所以无论手机、平板、电脑、物联网设备、智能网联车机系统、路由器、游戏机 甚至洗衣机家电都默认支持 tcp timestamp 机制。

所以,一般情况下,对于现代互联网应用来说,要进行服务器上 TIME_WAIT 连接状态的快速收敛,通常在只需要服务器端开启 net.ipv4.tcp_tw_recycle = 1 便可。
毕竟,在网络上搜索 linux 回收 TIME_WAIT 等关键字时,跳出来10篇文章有9篇都建议开启 tcp_tw_recycle 参数。

这么描述下来,开启 tcp_tw_recycle 视乎是一个很不错的参数咯?

错! 它在一些特定的网络环境下会出现严重的后果。

虽然 IPv6 普及了很多年,但如今现代互联网环境中仍然充斥着大量的 IPv4 网络通信,IPv4 地址早已是稀缺资源,自然就逃不开 NAT 这个技术,虽然 NAT 网络地址翻译技术被吐槽了很多年,但它在大多数场景下仍然是必不可少的互联网冲浪手段。
当 NAT 在遇到 tcp_tw_recycletcp_timestamps 时,就会开启内核上的一个保护逻辑,具体来说如下:

当用户终端(手机、电脑等)通过互联网访问到 网络负载均衡器再到具体的应用服务器,其数据包已经经过了多层的数据 NAT 地址翻译。

在服务器上通过 ss 来看,TCP 会话全都是 LB 的负载均衡器地址池与本机建立的 tcp 会话。

TCP 头部的 Timestamp 时间戳并非我们理解的真正意义上的传统日期时间戳,这个时间戳代表操作系统启动开启至今的运行秒数,所以每个终端的网络数据包中 TS val 值都是不一样的,而且差别很大。

当内核协议栈在收到同一个IP 同一个目标地址 同一个目标端口 的相同 TCP 协议数据包时,会对比其前后脚数据包中 TS Val 时间戳是否符合自然增长逻辑,如果时间戳跳差过大,超过一定的容错阈值,那么内核就会认为是无效数据包,内核协议栈收到 syn 数据包后直接丢弃处理,不会回复 ACK 。

在网络层上就表现为连接发起端认为是数据丢包,会进行重传逻辑。

又是 tcp_tw_recycle 的锅 ?

我们通过命令直接查看有没有开启 tcp_tw_recycle 的内核参数。

sysctl -a | grep tcp_tw_recycle

查询返回为空!

看看内核版本呢:

uname -a 
5.15.0-205.149.5.1.el8uek.x86_64

查阅相关资料后发现。
噢,原来在 Kernel 4.10 版本开始变更了时间戳的生成机制,从 Kernel 4.12 已经废弃掉了 tcp_tw_recycle 内核配置参数,当前服务器已经是 5.15.x 版本了。

如果不是 tcp_tw_recycle 内核参数的锅,那又是什么问题呢?

头脑风暴其他的可能性

查看相应指标发现有两个指标的计数器和丢包现象 在时间点上具有高度吻合性。

使用命令查看溢出指标:

watch -d -n 1 "netstat -s | grep -E 'overflow|drop'"

使用相同命令查看其他节点的现象

每个节点确实持续有全连接队列溢出的现象,而且现象会随着业务的高并发情况越发频繁。

内核协议栈源码片段

简单看下 Kernel 的源代码:
Kernel-4.9:

## file: net/ipv4/tcp_input.c
int tcp_conn_request(struct request_sock_ops *rsk_ops,
		     const struct tcp_request_sock_ops *af_ops,
		     struct sock *sk, struct sk_buff *skb)
{
    ....
	/* Accept backlog is full. If we have already queued enough
	 * of warm entries in syn queue, drop request. It is better than
	 * clogging syn queue with openreqs with exponentially increasing
	 * timeout.
	 */
	if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1) {
		NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
		goto drop;
	}
    ....
}

Kernel-4.10:

# tag: kernel v4.10
# file: net/ipv4/tcp_input.c
int tcp_conn_request(struct request_sock_ops *rsk_ops,
		     const struct tcp_request_sock_ops *af_ops,
		     struct sock *sk, struct sk_buff *skb)
{
    ....
	if (sk_acceptq_is_full(sk)) {
		NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
		goto drop;
	}
    ....
}

可以看到在内核版本 4.9 及其之前,会判断 sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1,大概意思是 全连接队列满了,且有 young_ack(表示刚刚有SYN到达),那么就会被丢弃。
在内核版本 4.9 之后,只会判断 sk_acceptq_is_full(sk) 全连接队列是否满了。

服务端相应第三次握手的时候,还会再次判断全连接队列是否满。
如果满了,同样丢弃握手请求。

解决全连接队列溢出

服务端在执行 listen() 的时候就确定好了版链接队列和全连接队列的长度。

对于全连接队列来说,其最大长度是 listen() 时传入的 backlognet.core.somaxconn 两个配置值之间较小的那个值。

如果需要加大全连接队列长度,那么需要调整 backlogsomaxconn 两个内核参数,然后重启应用。

>_ sysctl -a | grep -E 'backlog|somaxconn'
net.core.somaxconn = 16384
net.ipv4.tcp_max_syn_backlog = 16384

将这两个值的 16384 改大,重启应用即可。

监控

内核代码已经包含诸如 LINUX_MIB_LISTENOVERFLOWS 相关 SNMP 指标。

手动查看指标

如前所述,可以手动通过:

netstat -s | grep -E 'overflowed|dropped'
    493989 times the listen queue of a socket overflowed
    493997 SYNs to LISTEN sockets dropped

两个指标。

node-exporter 监控

线上环境基本都是 prometheus + node-exporter 方案,其中 node-exporter 已经默认已经开启采集 --collector.netstat 指标。
所以,可以通过指标 node_netstat_TcpExt_ListenDrops > 0node_netstat_TcpExt_ListenOverflows > 0 来查找。

其中,

  • node_netstat_TcpExt_ListenOverflows 代表的是 * times the listen queue of a socket overflowed,三次握手最后一步完成之后,Accept queue队列(完全连接队列,其大小为min(/proc/sys/net/core/somaxconn, backlog))超过上限时指标加1.
  • node_netstat_TcpExt_ListenDrops 代表的是 * SYNs to LISTEN sockets dropped,任何原因,包括Accept queue超限,创建新连接,继承端口失败等,加1.
    该指标包含 ListenOverflows 的情况,也就是说当出现 ListenOverflows 时,ListenDrops 也会增加1;除此之外,当内存不够无法为新的连接分配socket相关的数据结构时,也会增加1,当然还有别的异常情况下会增加1。
posted @ 2024-12-08 17:40  Professor哥  阅读(25)  评论(0编辑  收藏  举报