问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

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

创作时间:
作者:
@小白创作中心

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

引用
1
来源
1.
https://www.cnblogs.com/TopGear/p/18593487

在高并发场景下,TCP连接的稳定性和可靠性至关重要。本文详细记录了一次线上环境TCP握手丢包的故障处理过程,从问题现象、排查方法到最终的解决方案,为读者提供了一个完整的故障排查案例。

背景

业务场景需要客户端通过TCP连接线上环境的EMQX集群,集群规模为5台节点,每台节点在业务端口上保持约15万个TCP连接。近期发现与EMQX相关的业务功能出现间歇性的连接等待状态,运维同学在内网环境进行网络层的连接测试,确实复现了连接间歇性延迟的现象。

问题现象

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

排查

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参数。

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

虽然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。

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号