首页 > 运维 > linux运维 > 正文

记一次Redis连接池问题引发的RST

看不見的法師
发布: 2025-06-27 12:44:11
原创
635人浏览过

某个项目,由于监控系统尚未完善,我常常需要手动检查状态。终于有一天,我发现了异常情况:

记一次Redis连接池问题引发的RSTwatch -d -n1 'netstat -s | grep reset'

如图所示,服务器发送了大量的重置信号(RST),在监控期间还在持续发送,明显存在问题。

通过 tcpdump,我们可以简单地捕获 RST 包:

shell> tcpdump -nn 'tcp[tcpflags] & (tcp-rst) != 0'
登录后复制
登录后复制

然而,更好的方法是使用 tcpdump 捕获更多流量,然后通过 wireshark 进行分析:

记一次Redis连接池问题引发的RSTRST

如图所示,描述了 web 服务器与 redis 服务器之间的交互过程。存在两个问题:

在我的场景中,我使用了 lua-resty-redis 连接池,为什么还会发送 FIN 来关闭连接?即使关闭连接,为什么 web 服务器在收到 FIN 后还会发送 RST 作为补充?由于项目代码较多,我暂时无法确定 lua-resty-redis 连接池的问题所在,因此我决定先解决为什么 web 服务器在收到 FIN 后还会发送 RST 作为补充的问题。

我们可以通过 systemtap 来检查内核(3.10.0-693)是通过什么函数发送 RST 的:

shell> stap -l 'kernel.function("*")' | grep tcp | grep resetkernel.function("bictcp_hystart_reset@net/ipv4/tcp_cubic.c:129")kernel.function("bictcp_reset@net/ipv4/tcp_cubic.c:105")kernel.function("tcp_cgroup_reset@net/ipv4/tcp_memcontrol.c:200")kernel.function("tcp_fastopen_reset_cipher@net/ipv4/tcp_fastopen.c:39")kernel.function("tcp_highest_sack_reset@include/net/tcp.h:1538")kernel.function("tcp_need_reset@net/ipv4/tcp.c:2183")kernel.function("tcp_reset@net/ipv4/tcp_input.c:3916")kernel.function("tcp_reset_reno_sack@net/ipv4/tcp_input.c:1918")kernel.function("tcp_sack_reset@include/net/tcp.h:1091")kernel.function("tcp_send_active_reset@net/ipv4/tcp_output.c:2792")kernel.function("tcp_v4_send_reset@net/ipv4/tcp_ipv4.c:579")kernel.function("tcp_v6_send_reset@net/ipv6/tcp_ipv6.c:888")
登录后复制

虽然我不熟悉内核,但这并不妨碍我解决问题。通过查看源代码,可以大致判断出 RST 是由 tcp_send_active_reset 或 tcp_v4_send_reset 发送的(虽然 tcp_reset 看起来像是我们要找的,但实际上它是处理收到 RST 时的操作)。

为了确认到底是谁发送的,我启动了两个命令行窗口:

一个运行 tcpdump:

shell> tcpdump -nn 'tcp[tcpflags] & (tcp-rst) != 0'
登录后复制
登录后复制

另一个运行 systemtap:

#! /usr/bin/env stapprobe kernel.function("tcp_send_active_reset") {    printf("%s tcp_send_active_reset\n", ctime())}probe kernel.function("tcp_v4_send_reset") {    printf("%s tcp_v4_send_reset\n", ctime())}
登录后复制

通过对比两个窗口显示的内容的时间点,最终确认 RST 是由 tcp_v4_send_reset 发送的。

接下来确认一下 tcp_v4_send_reset 是由谁调用的:

#! /usr/bin/env stapprobe kernel.function("tcp_v4_send_reset") {    print_backtrace()    printf("\n")}// output0xffffffff815eebf0 : tcp_v4_send_reset+0x0/0x460 [kernel]0xffffffff815f06b3 : tcp_v4_rcv+0x5a3/0x9a0 [kernel]0xffffffff815ca054 : ip_local_deliver_finish+0xb4/0x1f0 [kernel]0xffffffff815ca339 : ip_local_deliver+0x59/0xd0 [kernel]0xffffffff815c9cda : ip_rcv_finish+0x8a/0x350 [kernel]0xffffffff815ca666 : ip_rcv+0x2b6/0x410 [kernel]0xffffffff81586f22 : __netif_receive_skb_core+0x572/0x7c0 [kernel]0xffffffff81587188 : __netif_receive_skb+0x18/0x60 [kernel]0xffffffff81587210 : netif_receive_skb_internal+0x40/0xc0 [kernel]0xffffffff81588318 : napi_gro_receive+0xd8/0x130 [kernel]0xffffffffc0119505 [virtio_net]
登录后复制

如上所示,tcp_v4_rcv 调用 tcp_v4_send_reset 发送了 RST。让我们看看 tcp_v4_rcv 的源代码:

int tcp_v4_rcv(struct sk_buff *skb){    ...    sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest);    if (!sk)        goto no_tcp_socket;process:    if (sk->sk_state == TCP_TIME_WAIT)        goto do_time_wait;    ...no_tcp_socket:    if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb))        goto discard_it;    if (skb->len doff 
登录后复制

有两处可能会触发 tcp_v4_send_reset(no_tcp_socket)。先看后面的 tcp_v4_send_reset 代码,也就是 do_time_wait 相关的部分,只有进入 TIME_WAIT 状态才会执行相关逻辑,而在本例中发送了 RST,并没有正常进入 TIME_WAIT 状态,不符合要求,因此问题的症结应该是前面的 tcp_v4_send_reset 代码,也就是 __inet_lookup_skb 相关的部分:当 sk 不存在的时候,发送重置信号。

但是为什么 sk 会不存在呢?当 web 服务器发送 FIN 时,进入 FIN_WAIT_1 状态,当 redis 服务器回复 ACK 时,进入 FIN_WAIT_2 状态,如果 sk 不存在,那么就说明 FIN_WAIT_1 或 FIN_WAIT_2 中的某个状态丢失了。通过 ss 观察一下:

shell> watch -d -n1 'ss -ant | grep FIN'
登录后复制

通常,FIN_WAIT_1 或 FIN_WAIT_2 存在的时间都很短暂,不容易观察,但在本例中,由于流量较大,所以没有问题。如果你的环境没有大流量,也可以通过 ab/wrk 等压力工具人为施加压力。结果发现,可以观察到 FIN_WAIT_1,但很难观察到 FIN_WAIT_2,看起来 FIN_WAIT_2 似乎丢失了。

原本以为可能与 linger、tcp_fin_timeout 等设置有关,经确认排除嫌疑。彷徨了许久,记起 TIME_WAIT 有一个控制项:tcp_max_tw_buckets,可以用来控制 TIME_WAIT 的数量,会不会与此有关:

shell> sysctl -a | grep tcp_max_tw_bucketsnet.ipv4.tcp_max_tw_buckets = 131072shell> cat /proc/net/sockstatsockets: used 1501TCP: inuse 117 orphan 0 tw 127866 alloc 127 mem 56UDP: inuse 9 mem 8UDPLITE: inuse 0RAW: inuse 0FRAG: inuse 0 memory 0
登录后复制

对比系统现有的 tw,可以发现已经接近 tcp_max_tw_buckets 规定的上限。尝试提高阈值,发现又能观察到 FIN_WAIT_2 了,甚至 RST 的问题也随之消失。

如此一来,RST 问题算是有眉目了:TIME_WAIT 数量达到 tcp_max_tw_buckets 规定的上限,进而影响了 FIN_WAIT_2 的存在(问题细节尚未搞清楚),于是在 tcp_v4_rcv 调用 __inet_lookup_skb 查找 sk 时查不到,最终只能发送 RST。

结论:tcp_max_tw_buckets 不能设置得太小!

...

问题到这里还不算完,别忘了我们还有一个 lua-resty-redis 连接池的问题尚未解决。

如何验证连接池是否生效呢?

最简单的方法是核对连接到 redis 的 TIME_WAIT 状态是否过多,如果是的话,那么就说明连接池可能没有生效,为什么是可能?因为在高并发情况下,当连接过多时,会按照 LRU 机制关闭旧连接,此时出现大量 TIME_WAIT 是正常的。

最准确的方法是使用 redis 的 client list 命令,它会打印每个连接的 age 连接时长。通过此方法,我验证发现 web 服务器与 redis 服务器之间的连接,总是在 age 很小的时候就被断开,说明有问题。

在解决问题前了解一下 lua-resty-redis 的连接池是如何使用的:

local redis = require "resty.redis"local red = redis:new()red:connect(ip, port)...red:set_keepalive(0, 100)
登录后复制

只要用完后记得调用 set_keepalive 把连接放回连接池即可。一般出问题的地方有两个:

openresty 禁用了 lua_code_cache,此时连接池无效redis 的 timeout 太小,此时长连接可能会频繁被关闭在我的场景里,如上问题均不存在。每当我一筹莫展的时候,我就重看一遍文档,当看到 connect 的部分时,下面一句话提醒了我:

也就是说,即便是短连接,在 connect 的时候也会尝试从连接池里获取连接,这样的话,如果是长短连接混用的情况,那么连接池里长连接建立的连接就可能会被短连接关闭掉。顺着这个思路,我搜索了一下源代码,果然发现某个角落有一个短连接调用。

结论:不要混用长短连接!

以上就是记一次Redis连接池问题引发的RST的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习
PHP中文网抖音号
发现有趣的

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号