0

0

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

看不見的法師

看不見的法師

发布时间:2025-06-27 12:44:11

|

705人浏览过

|

来源于php中文网

原创

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

记一次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 观察一下:

灵光
灵光

蚂蚁集团推出的全模态AI助手

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

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

相关专题

更多
常用的数据库软件
常用的数据库软件

常用的数据库软件有MySQL、Oracle、SQL Server、PostgreSQL、MongoDB、Redis、Cassandra、Hadoop、Spark和Amazon DynamoDB。更多关于数据库软件的内容详情请看本专题下面的文章。php中文网欢迎大家前来学习。

976

2023.11.02

内存数据库有哪些
内存数据库有哪些

内存数据库有Redis、Memcached、Apache Ignite、VoltDB、TimesTen、H2 Database、Aerospike、Oracle TimesTen In-Memory Database、SAP HANA和ache Cassandra。更多关于内存数据库相关问题,详情请看本专题下面的文章。php中文网欢迎大家前来学习。

633

2023.11.14

mongodb和redis哪个读取速度快
mongodb和redis哪个读取速度快

redis 的读取速度比 mongodb 更快。原因包括:1. redis 使用简单的键值存储,而 mongodb 存储 json 格式的数据,需要解析和反序列化。2. redis 使用哈希表快速查找数据,而 mongodb 使用 b-tree 索引。因此,redis 在需要高性能读取操作的应用程序中是一个更好的选择。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

480

2024.04.02

redis怎么做缓存服务器
redis怎么做缓存服务器

redis 作为缓存服务器的答案:redis 是一款开源、高性能、分布式的键值存储,可作为缓存服务器使用。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

399

2024.04.07

redis怎么解决数据一致性
redis怎么解决数据一致性

redis 提供了两种一致性模型,以维护副本数据一致性:强一致性 (sync) 确保写操作仅在复制到所有从节点后才完成;最终一致性 (async) 则在主节点上写操作后认为已完成,牺牲一致性换取性能。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

392

2024.04.07

mysql和redis怎么保证双写一致性
mysql和redis怎么保证双写一致性

确保 mysql 和 redis 双写一致性的技术包括:1、事务性更新:同时更新 mysql 和 redis,保证一致性;2、主从复制:mysql 主服务器更改同步到 redis 从服务器;3、基于事件的更新:mysql 记录更改并发送到 redis等等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

447

2024.04.07

redis缓存一般存些什么数据
redis缓存一般存些什么数据

redis缓存中存储的数据类型包括:字符串、哈希、列表、集合、有序集合、位图、地理空间数据和hyperloglog。这些数据类型适用于存储各种数据,从简单信息到复杂对象和地理位置。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

403

2024.04.07

redis的8种数据类型有哪些
redis的8种数据类型有哪些

redis 提供 8 种数据类型:字符串(文本、数字、二进制)、哈希(键值对)、列表(有序集合)、集合(无序唯一元素)、有序集合(按分数排序)、地理空间(地理位置)、hyperloglog(估计大数据基数)和位图(位序列存储)。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

430

2024.04.07

Golang 性能分析与pprof调优实战
Golang 性能分析与pprof调优实战

本专题系统讲解 Golang 应用的性能分析与调优方法,重点覆盖 pprof 的使用方式,包括 CPU、内存、阻塞与 goroutine 分析,火焰图解读,常见性能瓶颈定位思路,以及在真实项目中进行针对性优化的实践技巧。通过案例讲解,帮助开发者掌握 用数据驱动的方式持续提升 Go 程序性能与稳定性。

8

2026.01.22

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
进程与SOCKET
进程与SOCKET

共6课时 | 0.3万人学习

Redis+MySQL数据库面试教程
Redis+MySQL数据库面试教程

共72课时 | 6.4万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

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