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

栏目: IT技术 · 发布时间: 4年前

内容简介:某个项目,因为监控尚不完善,所以我时常会人肉查查状态,终于有一天发现了异常:如图所示,服务器发送了大量的 reset,在我 watch 的时候还在发,多半有问题。通过 tcpdump 我们可以简单抓取一下 RST 包:

某个项目,因为监控尚不完善,所以我时常会人肉查查状态,终于有一天发现了异常:

记一次 <a href='https://www.codercto.com/topics/18994.html'>Redis</a> 连接池问题引发的RST

watch -d -n1 ‘netstat -s | grep reset’

如图所示,服务器发送了大量的 reset,在我 watch 的时候还在发,多半有问题。

通过 tcpdump 我们可以简单抓取一下 RST 包:

shell> tcpdump -nn 'tcp[tcpflags] & (tcp-rst) != 0'

不过更好的方法是通过 tcpdump 多抓一些流量然后用 wireshark 来分析:

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

RST

如图所示,描述了一个 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 reset
kernel.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 发送的。

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

一个运行 tcpdump:

shell> tcpdump -nn 'tcp[tcpflags] & (tcp-rst) != 0'

另一个运行 systemtap:

#! /usr/bin/env stap

probe 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 stap

probe kernel.function("tcp_v4_send_reset") {
    print_backtrace()
    printf("\n")
}

// output

0xffffffff815eebf0 : 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)
        <strong>goto no_tcp_socket;</strong>

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 < (th->doff << 2) || tcp_checksum_complete(skb)) {
csum_error:
        TCP_INC_STATS_BH(net, TCP_MIB_CSUMERRORS);
bad_packet:
        TCP_INC_STATS_BH(net, TCP_MIB_INERRS);
    } else {
        tcp_v4_send_reset(NULL, skb);
    }

    ...

do_time_wait:

    ...

    switch (tcp_timewait_state_process(inet_twsk(sk), skb, th)) {

        ...

        case TCP_TW_RST:
            <strong>goto no_tcp_socket;</strong>

        ...

    }

    ...
}

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

不过 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 存在的时间都很短暂,不容易观察,不过在本例中,流量比较大,所以没问题。结果发现,可以观察到 FIN_WAIT_1,但是却很难观察到 FIN_WAIT_2,看上去 FIN_WAIT_2 似乎丢失了。

想到这里我突然想到 TIME_WAIT 有一个相关的控制项:tcp_max_tw_buckets,用来控制 TIME_WAIT 的数量,可能与此有关:

shell> sysctl -a | grep tcp_max_tw_buckets
net.ipv4.tcp_max_tw_buckets = 131072

shell> cat /proc/net/sockstat
sockets: used 1501
TCP: inuse 117 orphan 0 tw 127866 alloc 127 mem 56
UDP: inuse 9 mem 8
UDPLITE: inuse 0
RAW: inuse 0
FRAG: inuse 0 memory 0

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

如此一来,web 服务器收到 FIN 后还会发送 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 是正常的。

When the connection pool would exceed its size limit, the least recently used (kept-alive) connection already in the pool will be closed to make room for the current connection.

最准确的方法是使用 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 的部分时,下面一句话提醒了我:

Before actually resolving the host name and connecting to the remote backend, this method will always look up the connection pool for matched idle connections created by previous calls of this method.

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

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


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

交互设计之路

交互设计之路

库帕 / Chris Ding / 电子工业出版社 / 2006-3 / 38.00元

本书是基于众多商务案例,讲述如何创建更好的、高客户忠诚度的软件产品和基于软件的高科技产品的书。本书列举了很多真实可信的实际例子,说明目前在软件产品和基于软件的高科技产品中,普遍存在着“难用”的问题。作者认为,“难用”问题是由这些产品中存在着的高度“认知摩擦”引起的,而产生这个问题的根源在于现今软件开发过程中欠缺了一个为用户利益着想的前期“交互设计”阶段。“难用”的产品不仅损害了用户的利益,最终也将......一起来看看 《交互设计之路》 这本书的介绍吧!

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具