在OpenResty中实现Redis Sentinel支持

在OpenResty中实现Redis Sentinel支持

1、引言

OpenResty 通过 lua-resty-redis 模块为 Nginx 提供了与 Redis 交互的能力,但该模块原生仅支持直连模式,不具备高可用场景下的故障转移能力。在复杂的生产环境中,单点 Redis 容易因节点故障导致服务中断,因此需要借助 Redis Sentinel 来实现监控、故障切换、通知和配置提供者等高可用特性Redis。Redis Sentinel 通过分布式的哨兵进程对主从实例进行心跳检测,一旦主节点失效,即可自动将某个从节点提升为新主,并通知客户端重新连接Redis。为了让 OpenResty 应用在 Redis 主节点故障时具备自动切换的能力,我们需要在 lua-resty-redis 之上实现对 Sentinel 模式的支持。

2、Redis Sentinel原理

Redis Sentinel 是一个负责监控、通知和自动故障切换的分布式系统组件,它通过多个 Sentinel 进程与主从实例协同工作,为 Redis 提供高可用性和自动故障恢复能力。在发生主节点故障时,多个 Sentinel 进程会共同判断并选举出新的主节点,同时向客户端动态发布最新的主节点地址,从而保证 Redis 服务的持续可用。

2.1、redis基础架构

Sentinel 进程

  • 订阅并发布 Sentinel 专用的 Pub/Sub 频道,用于互通对各节点健康状况的判断
  • 维护对所有监控对象(主节点和从节点)的状态信息,并与其他 Sentinel 交换,形成故障检测的共识
  • 在故障切换时,参与 leader 选举,协调后续提升和重配置操作

Redis 主节点与从节点

  • 主节点(Master):承担所有写操作,并将写命令通过复制协议同步到从节点
  • 从节点(Replica):负责同步主节点的数据,可用于读请求;在主节点故障时,有资格被提升为新主节点

客户端(Clients)

  • 客户端配置中指定所有 Sentinel 地址
  • 启动或连接主节点失败后,向 Sentinel 查询当前主节点地址
  • 通过获取的地址发送命令,故障发生时无需人工干预即可自动切换到新主节点

2.2、主观下线和客观下线

一个 Sentinel 需要获得系统中多数(majority) Sentinel 的支持, 才能发起一次自动故障迁移, 并预留一个给定的配置纪元 (configuration Epoch ,一个配置纪元就是一个新主服务器配置的版本号)。也就是说在只有少数(minority) Sentinel 进程正常运作的情况下, Sentinel 是不能执行自动故障迁移的。

Redis 的 Sentinel 中关于下线(down)有两个不同的概念:

  • 主观下线 指的是单个 Sentinel 实例对服务器做出的下线判断。
  • 客观下线 指的是多个 Sentinel 实例在对同一个服务器做出 SDOWN 判断, 并且通过 SENTINEL is-master-down-by-addr 命令互相交流之后, 得出的服务器下线判断。 (一个 Sentinel 可以通过向另一个 Sentinel 发送 SENTINEL is-master-down-by-addr 命令来询问对方是否认为给定的服务器已下线。)

如果一个服务器没有在 master-down-after-milliseconds 选项所指定的时间内, 对向它发送 PING 命令的 Sentinel 返回一个有效回复(valid reply), 那么 Sentinel 就会将这个服务器标记为主观下线。

服务器对 PING 命令的有效回复可以是以下三种回复的其中一种:

  • 返回 +PONG
  • 返回 -LOADING 错误。
  • 返回 -MASTERDOWN 错误。

如果服务器返回除以上三种回复之外的其他回复, 又或者在指定时间内没有回复 PING 命令, 那么 Sentinel 认为服务器返回的回复无效(non-valid)。

注意, 一个服务器必须在 master-down-after-milliseconds 毫秒内, 一直返回无效回复才会被 Sentinel 标记为主观下线。

举个例子, 如果 master-down-after-milliseconds 选项的值为 30000 毫秒(30 秒), 那么只要服务器能在每 29 秒之内返回至少一次有效回复, 这个服务器就仍然会被认为是处于正常状态的。

从主观下线状态切换到客观下线状态并没有使用严格的法定人数算法(strong quorum algorithm), 而是使用了流言协议: 如果 Sentinel 在给定的时间范围内, 从其他 Sentinel 那里接收到了足够数量的主服务器下线报告, 那么 Sentinel 就会将主服务器的状态从主观下线改变为客观下线。 如果之后其他 Sentinel 不再报告主服务器已下线, 那么客观下线状态就会被移除。

客观下线条件只适用于主服务器: 对于任何其他类型的 Redis 实例, Sentinel 在将它们判断为下线前不需要进行协商, 所以从服务器或者其他 Sentinel 永远不会达到客观下线条件。

只要一个 Sentinel 发现某个主服务器进入了客观下线状态, 这个 Sentinel 就可能会被其他 Sentinel 推选出, 并对失效的主服务器执行自动故障迁移操作。

2.3、故障迁移

  • 判定主节点故障
    • 当多个 Sentinel 达成共识,主节点进入“客观下线”(ODOWN)状态后,触发故障转移。
  • 纪元(Epoch)自增与 Leader 选举
    • 纪元自增:一旦进入 ODOWN 状态,Sentinel 会将全局配置中的 current-epoch 加一,用作本次故障迁移的版本号。
    • Leader 选举:Sentinel 向同伴发送带有新纪元的选票,谁获得多数票谁就成为 leader。若选举失败,则等待 2 × 故障迁移超时时间 后,重新发起选举(纪元可再次自增或保持,取决于实现)。
  • 提升新主节点
    • UPGRADE:当 leader 当选后,会在标记为合格的从节点中挑选一台,向其发送 SLAVEOF NO ONE(或 REPLICAOF NO ONE)命令,将其升级为新主节点。
    • 配置广播:leader 通过 Sentinel 专用的 Pub/Sub 频道,将包含新主节点地址和纪元的配置信息广播给其它 Sentinel,确保所有进程都采用同一配置。
  • 重连剩余从节点
    • 重新复制:Leader 向原主节点的其余从节点依次发送 SLAVEOF <新主节点地址>,让它们连接并开始同步新主节点的数据Redis
    • 状态监控:Leader 持续监控这些从节点的复制状态,直至所有从节点都开始接收并应用来自新主的复制流。
  • 终止故障迁移
    • 结束迁移:当所有从节点完成重连与同步后,leader 宣布本次故障迁移完成,并退出选举状态。
    • 配置持久化:Sentinel 会向所有被重配置的实例发送 CONFIG REWRITE 命令,将最新角色和配置信息持久化到磁盘,以应对 Sentinel 重启或网络分区等场景。

新主节点选择规则详解

  1. 排除不合格从节点
    • 主观下线或断线:凡是被标记为 SDOWN、网络连接断开,或最后一次 PING 响应超过 5 秒的从节点,均不予考虑。
    • 长时间未连接:与故障主节点断链时长超过 down-after-milliseconds × 10 的从节点,也会被淘汰,避免选到与主节点过度脱节的副本。
  2. 优先复制进度
    • 复制偏移量:在剩余候选节点中,优先选择复制偏移量(replication offset)最大的从节点,以最大化最新数据可用性。

2.4、发布与订阅信息

Redis Sentinel 通过内置的 Pub/Sub 通道向客户端推送包括实例状态变化、故障迁移各阶段、配置更新等在内的丰富事件;客户端则只能以 订阅者(Subscriber)的身份使用 subscribepsubscribe 命令来接收这些事件,无法向 Sentinel 发送 publish 消息

1、客户端如何订阅 Sentinel 事件

连接 Sentinel

  • 客户端可以像连接普通 Redis 那样,通过 TCP 连接到任意一个 Sentinel 进程的默认端口 6379
  • 可以在配置中指定多个 Sentinel 地址,确保在某个 Sentinel 宕机时仍能继续接收事件。

订阅命令

  • subscribe <channel> [channel ...]:直接订阅一个或多个指定频道,用于精准接收特定事件。
  • psubscribe <pattern> [pattern ...]:以模式匹配方式订阅,可使用通配符(如 *)一次接收多个频道事件,推荐 psubscribe * 用于接收 Sentinel 支持的所有事件。
1
2
3
4
# 接收所有 Sentinel 事件
redis-cli -p 6379 PSUBSCRIBE "*"
# 只接收主观下线和故障切换完成事件
redis-cli -p 6379 SUBSCRIBE "+sdown" "+switch-master"

收到消息的格式

  • 对于 普通订阅SUBSCRIBE),客户端收到格式为:

    1
    2
    3
    1) "message"
    2) "<channel>"
    3) "<payload>"
  • 对于 模式订阅PSUBSCRIBE),客户端收到格式为:

    1
    2
    3
    4
    1) "pmessage"
    2) "<pattern>"
    3) "<channel>"
    4) "<payload>"

    其中 <payload> 通常包含事件名之后的参数,如实例类型、实例名、IP、端口及其对应主节点信息等


2、Sentinel 在哪些频道上发布事件

在 Redis Sentinel 管理主从 Redis 实例的高可用过程中,Sentinel 会通过发布事件来通知订阅者(例如 OpenResty 或其他服务)关于 Redis 实例状态变化的情况。这些事件会发布在 __sentinel__:hello 或通配模式下的频道,订阅者通常会通过 psubscribe * 来捕捉所有相关事件。

常见订阅事件类型

1、+failover-state-reconf-slave

1
pmessage&*&+failover-state-reconf-slave
  • 含义:Sentinel 进入故障转移流程,正在将一个 slave 配置为新的 slave。
  • 用途:过程性事件,表示 failover 正在进行中。

2、+slave-reconf-sent

1
pmessage&*&+slave-reconf-sent&slave 10.10.10.10 6379
  • 含义:通知某个从节点正在被重新配置。
  • 用途:也属于过程性事件,表示该从节点被通知去跟随新的主节点。

3、 +config-update-from

1
pmessage&*&+config-update-from&sentinel 10.10.10.10 6379
  • 含义:从某个 Sentinel 节点接收到新的配置。
  • 用途:Sentinel 之间同步状态信息,通常用于调协主选举结果。

4、 +switch-master(最关键事件)

1
pmessage&*&+switch-master&mymaster 10.10.10.10 6379 20.20.20.20 6379
  • 含义:主从切换完成,mymaster 由旧主(IP、端口)切换到了新主(IP、端口)。
  • 用途:这是客户端需要重点处理的事件,OpenResty 正是基于该事件提取出新的主节点地址,并更新配置。

3、总结

本质上

客户端只需要订阅并处理 +switch-master 事件,这是 Sentinel 对外暴露的“主节点切换成功”的最终确认信号。

  • 客户端只在 +switch-master 时提取新的主 IP/端口;
  • 更新内存配置;
  • 自动切换 Redis 客户端连接;
  • 获取 slave 列表用于下一次故障转移备用

3、openresty实现Redis Sentinel 支持

3.1、单进程订阅Sentinel

1、初始化连接和订阅(只在一个 worker 进程中执行)

核心逻辑:

  • 仅由 第一个 NGINX worker 进程 执行订阅(监听哨兵事件),防止多进程重复订阅。
  • 启动定时器回调订阅函数,针对每一个哨兵实例启动一个 Lua thread:
  • 在 订阅函数中:
    • 与哨兵建立连接并发出 PSUBSCRIBE * 命令。

主要分为2步

  • 获取当前主节点
  1. 使用命令

    1
    SENTINEL get-master-addr-by-name <master-name>

    该命令会直接返回指定主节点名称对应的 IP 和端口,无需遍历所有主实例列表。

  2. 返回格式

    • 成功时,回复数组:

      1
      2
      1) "<master-ip>"
      2) "<master-port>"
    • 名称不存在时,返回空(NIL) 。

  3. 应用场景

    • 启动时:初始化连接 Redis,即刻知道当前应连接的主节点。
    • 重连时:网络超时或 Sentinel 重启后,重新查询以获取最新主节点。

  • 订阅所有 Sentinel 事件
  1. 使用模式订阅命令

    1
    PSUBSCRIBE "*"

    以通配符 * 订阅 Sentinel 发布的所有频道,包括 +switch-master+sdown+odown

  2. 消息格式
    当有事件发布时,客户端会收到类似以下格式的回复:

    1
    2
    3
    4
    5
    1) "pmessage"
    2) "<pattern>" # 即 "*"
    3) "<channel>" # 例如 "+switch-master"
    4) "<payload>" # 事件内容,如 master 名称及新旧地址
    ``` :contentReference[oaicite:4]{index=4}
  3. 处理逻辑

    • 客户端在回调中只需关注最关键的 +switch-master 事件,即主从切换完成的通知;
    • 其他事件(如 +sdown+odown)可用于日志监控,但不影响连接逻辑。

2、处理订阅的事件(监听 +switch-master)

核心逻辑:

  1. 处理事件格式:

    1
    "pmessage", "*", "+switch-master", "mymaster 10.10.10.10 6379 20.20.20.20 6379"
  2. 识别事件类型:

    1
    if string.find(redis_status, "pmessage&*&+switch-master&", 1, true) == nil then return end
  3. 提取新主节点地址:

    1
    2
    10.10.10.10  -- 新主IP
    6379 -- 新主Port
  4. 写入共享内存:

3、主动提取 Slave 信息(从新的主节点中获得)

核心逻辑:

  • 连接新的主节点,调用 INFO replication

    1
    redis:info("replication")
  • Redis 返回内容示例:

    1
    2
    3
    role:master
    connected_slaves:1
    slave0:ip=10.10.10.10,port=6379,state=online,offset=...,lag=0
  • 解析 slave IP 和端口:

  • 将 slave 信息也写入共享配置:

3.2、work更新节点信息

1、其他 Worker 周期性同步新配置

  • 所有非第一个 worker 会定时读取共享内存中配置

  • 解析 serverportslave_server 等字段并更新本地配置

  • 这确保了所有工作进程都能基于最新主节点进行连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
┌────────────────────
Sentinel 集群
└────────┬───────────
│ 发布事件 (+switch-master)

┌──────────────────────────
│ 第一个 Worker 订阅事件
└────────┬─────────────────

┌─────────────────────────────────────────────
deal_change_for_redis:
│ - 识别事件并提取主节点
│ - 调用 get_slaves_config 获取从节点
│ - 更新 redis_cfg 并写入 config_dict
└─────────────────────────────────────────────

┌─────────────────────────────────────
│ 其他 Worker3 秒拉取 redis_config
│ 并更新本地 redis_cfg
└─────────────────────────────────────

3.3、动态域名解析

在 OpenResty 中,Lua 模块本身不会在运行时调用系统的 /etc/hosts 或者 gethostbyname 接口来做域名解析。因为这是阻塞的,openresty避免在运行过程中阻塞 nginx 的 epoll 循环调用。

这意味着如果上游地址Redis使用域名,默认情况下 NGINX/Lua 不会动态更新解析结果,而只能在配置加载阶段解析一次。为了在运行时做到非阻塞、可控、动态的域名解析,需要借助专门的 Lua DNS 客户端库,比如lua-resty-dns

主要有2个好处

  • 高可用 & 灾备切换
    redis集群发生故障切换时,如果域名指向了新的 IP,需要立即生效,而非重启或 reload NGINX。
  • 性能 & 非阻塞
    Lua DNS 客户端通过 cosocket 实现非阻塞查询,性能更优。

实际需要在connect之前需要先解析域名,当然如果传入的是ip会直接返回,也不会存在问题。

3.4、ssl双向验证

在某些特殊场景下,openresty连接redis时需要ssl双向验证,目前openresty和lua-resty-redis 已经实现了相关指令,支持openresty在lua层面与后端进行ssl双向验证的交互

  • ssl

    If set to true, then uses SSL to connect to redis (defaults to false).

  • ssl_verify

    If set to true, then verifies the validity of the server SSL certificate (defaults to false). Note that you need to configure the lua_ssl_trusted_certificate to specify the CA (or server) certificate used by your redis server. You may also need to configure lua_ssl_verify_depth accordingly.

1
2
3
4
5
6
#客户端证书
lua_ssl_certificate ../../cert/test.crt;
#客户端私钥
lua_ssl_certificate_key ../../cert/test.key;
#验证redis的证书
lua_ssl_trusted_certificate ../../cert/test.crt;

在连接时,需要先connect,然后进行sslhandshake

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if unix then
if port_or_opts ~= nil then
ok, err = sock:connect(host, port_or_opts)
opts = port_or_opts
else
ok, err = sock:connect(host)
end
else
ok, err = sock:connect(host, port_or_opts, opts)
end

if opts and opts.ssl then
ok, err = sock:sslhandshake(false, opts.server_name, opts.ssl_verify)
if not ok then
return ok, "failed to do ssl handshake: " .. err
end
end

3.5、总结

1、启动阶段

  • OpenResty 启动时进行初始化。
  • 它会判断当前是哪个 worker 进程:
    • 第一个 worker:启动订阅线程,监听 Sentinel 的事件;
    • 其他 worker:定时从共享内存中读取配置,保持配置同步。

2、Sentinel 订阅处理(只在第一个 worker 中)

  • 每个 Sentinel 都会启动一个独立的线程 tread_for_sentinel
  • 线程中调用 init_for_sentinel(),与 Sentinel 建立连接,并通过 PSUBSCRIBE * 订阅所有事件。
  • 成功后立即获取当前主节点及从节点信息,写入共享字典 config_dict

3、监听到主从切换事件(+switch-master)

  • 当 Sentinel 触发故障转移并完成主从切换后,会发布 +switch-master 事件。
  • 模块通过 deal_change_for_redis 处理这个事件:
    • 提取新的主节点 IP 和端口;
    • 更新 redis_cfg
    • 调用 get_slaves_config 从新主节点中获取从节点信息;
    • 最后将所有更新后的信息写入共享字典。

4、其他 Worker 拉取配置

  • 非订阅 worker 每 3 秒执行一次定时任务 update_cfg_for_redis
  • 从共享字典中读取最新配置,更新本地 Redis 连接参数。

5、实现效果

  • 响应 +switch-master 事件,快速感知主节点切换;
  • 自动完成主从信息提取与缓存;
  • 多 worker 保持配置一致,无需重启;
  • 支持多个 Redis Sentinel、多个 Redis 主从集群。

4、异常场景

首先来看

1
redis:read_reply()

它使用 OpenResty 的 cosocket API(非标准 Lua 网络模块);

底层调用 ngx.socket.tcp():receive(),这个是 事件驱动的非阻塞接收

当没有消息时,它会让出当前 Lua 协程的控制权

NGINX 的 epoll/kqueue 会监听这个 socket 的“可读事件”;

一旦 Redis 服务器推送消息到来,NGINX 会唤醒协程继续执行,此时 read_reply() 立即返回数据

4.1、网络中断

read_reply可能触发 timeout 的真实场景

网络异常场景 描述
Sentinel 宕机 Sentinel 节点突然挂掉,客户端连接仍保持但收不到数据
网络不通 Redis 与 OpenResty 之间网络中断、丢包,消息丢失
防火墙限制 Redis 连接通了,但防火墙屏蔽了订阅消息
Sentinel 正常但没消息推送 短时间内没有任何主从事件(也会超时,但不是错误)

如果不处理 timeout,系统就会“瞎等”而错过关键事件,最终导致连接错误或服务异常。需要让系统能自动感知异常、自动恢复连接、自动更新主节点配置,从而实现高可用。

1、导致的问题

  1. 协程会持续调用 read_reply(),但什么都读不到,丢失哨兵推送消息
  • 当 Redis Sentinel 因为某些原因长时间没有发送事件(比如网络中断或 Sentinel 重启),read_reply() 每次都返回 "timeout"
  • 如果你不处理这个错误,协程会一直空转在超时循环中,每次 sleep 一下,然后再次调用 read_reply()
  • 此时你其实已经“失联”了,但却毫无察觉。

  1. 不能发现主节点已经切换
  • 如果 Sentinel 在这期间发生了故障转移,而原订阅连接没有断(但也收不到新消息),你将错过 +switch-master 事件;
  • 所有 Worker 仍旧使用旧主节点地址,导致业务连接失败;
  • 简单说:主已经切了,但还连着旧主,业务报错。

  1. 无法自我恢复,必须手动介入
  • 如果不主动重连 Sentinel,那除非 OpenResty 被 reload 或重启,否则这个订阅协程会一直挂在“无响应”状态;
  • 实际部署中常会遇到:主从切换发生了,但系统完全没有感知,直到业务报警或用户报错才发现问题。

2、解决方式

当触发 timeout时

1.重新创建 Sentinel 连接

  • 创建一个新的 Sentinel 客户端实例,获取连接对象;

  1. 请求 Sentinel 获取主节点地址
  • 使用 redissent:sentinel("get-master-addr-by-name", redis_cfg.master_name) 获取当前主节点的 IP 和端口;

  1. 解析和更新配置
  • 获取主从节点,更新共享内存,让work可以感知切换

5、参考

sentinel: http://doc.redisfans.com/topic/sentinel.html

客户端实现规范:https://redis.io/docs/latest/develop/reference/sentinel-clients/

lua-resty-redis:https://github.com/openresty/lua-resty-redis?tab=readme-ov-file#connect

lua-nginx-module:https://github.com/openresty/lua-nginx-module#lua_ssl_trusted_certificate


在OpenResty中实现Redis Sentinel支持
https://zjfans.github.io/2025/05/17/网关支持redis哨兵/
作者
张三疯
发布于
2025年5月17日
许可协议