摘要:Redis 通过长连接 block 方式订阅事件通知,如果连接异常断开导致半开连接,那么客户端将无法感知,永远不会收到事件通知。Redis-py 可以通过设置 keepalive 选项避免类似问题。

问题描述

使用 Redis pubsub 做事件订阅时,偶尔会遇到因网络异常,导致服务断开连接,但是客户端没有感知,以为连接还正常,以至一直收不到事件推送。

复现方法

在 Redis 订阅成功后,在客户端机器上添加iptables规则,把与服务端通信报文丢弃。然后重启 Redis 并去掉iptables规则。 这时客户端会一直保持一个旧的 Established 连接,永不断开,也收不到任何事件。

# iptables -nL INPUT --line-numbers   # 查看iptables规则
Chain INPUT (policy ACCEPT)
num  target     prot opt source               destination 

# # 等客户端订阅成功后
# iptables -I INPUT 1 -p tcp -s 127.0.0.1 --sport 6379 -j DROP  # 丢弃服务端发来的任何报文

# iptables -nL INPUT --line-numbers
Chain INPUT (policy ACCEPT)
num  target     prot opt source               destination         
1    DROP       tcp  --  127.0.0.1            0.0.0.0/0            tcp spt:6379

# supervisorctl restart redis_6379  # 重启redis-server

# iptables -D INPUT 1  # 删除刚才添加的iptables规则

问题原因

上面遇到的这种情况叫做TCP半开连接,主要有以下原因导致:

  1. 连接长时间不活动而被代理或防火墙断连(主要原因)

    HAProxy代理默认60s后断开非活动连接。

  2. 服务停止、网络波动、宕机、应用重启等导致连接异常断开

当出现半开连接后,除非使用该连接收发消息,否则无法检测到连接已经失效。

解决办法

升级 redis-py 版本>=2.10.0

redis-py版本>=2.10.0 版本开始引入socket-keepalive参数。

添加keepalive相关参数

可以参考以下示例代码:

import platform
import redis
import socket
import time
from redis.exceptions import ConnectionError, TimeoutError

if platform.system() == 'Linux':
    socket_keepalive_options = {
        socket.TCP_KEEPIDLE: 120,
        socket.TCP_KEEPCNT: 3,
        socket.TCP_KEEPINTVL: 5
    }
else:
    # 其他平台有些option不支持
    socket_keepalive_options =  None
    rd = redis.StrictRedis(host='127.0.0.1', port=6379,
    socket_connect_timeout=5,  # 避免connect时卡住
    socket_keepalive=True,     # 启用TCP keepalive
    socket_keepalive_options=socket_keepalive_option
)
ps = rd.pubsub(ignore_subscribe_messages=True)

while True:
    try:
        ps.subscribe('event.test')
        for msg in ps.listen():
            print(msg)
    except (ConnectionError, TimeoutError) as exc:
        # redis可能异常挂掉,keepalive检测到断线后会自动重连
        print(exc)
        time.sleep(1)  # sleep一会再试
        continue

添加完以上keepalive配置后,客户端会以 block 方式等待事件,也会在需要的时候发送keepalive探测包(probe packet)判断连接是否有效:

  1. 如果在120s(TCP_KEEPIDLE)内没有收到任何网络数据
  2. 则会每5s(TCP_KEEPINTVL)发送一个空包
  3. 如果连续3次(TCP_KEEPCNT)都没有收到 ACK 回复,则认为连接已经失效,抛出异常
  4. 业务端捕获异常,然后重试订阅

扩展资料

本文来自《Redis pubsub 使用 keepalive 保活问题》- 熊清亮的博客。

转载请注明原文链接:https://seealso.cn/debug/redis-pubsub-keepalive