Redis Pub/Sub 常见配置问题排查指南

通过掌握 Redis Pub/Sub 配置挑战,确保实时消息传递的可靠性。本指南提供可操作的步骤,使用关键的 `client-output-buffer-limit` 指令排查慢消费者(导致不稳定的首要原因)。学习如何使用 `CLIENT LIST` 命令诊断内存峰值、管理专用订阅者连接,并实施高流量 Pub/Sub 隔离的最佳实践,以维护系统完整性。

Redis Pub/Sub 常见配置问题排查指南

Redis Pub/Sub 足够简单,以至于团队常常将其视为无需运维的小型消息代理。发布者调用 PUBLISH,订阅者接收消息,一切感觉都是即时的。

当涉及真实网络、慢客户端、重连和共享 Redis 实例时,问题就开始了。Pub/Sub 没有消息历史、没有确认机制、也没有重试队列。它是一种实时广播机制。如果订阅者离线,该订阅者就会丢失消息。如果订阅者已连接但读取速度不够快,Redis 必须为该客户端保留待发送的输出,直到达到配置的限制将其断开。

这使得 Redis Pub/Sub 非常适合缓存失效、在线状态更新、实时仪表盘和“最新值”通知。但对于每条消息必须恰好处理一次或在故障后重放的工作流,它并不适用。对于这种情况,请考虑 Redis Streams、RabbitMQ、Kafka 或其他具有投递跟踪功能的代理。

症状:Pub/Sub 流量高时内存增长

最常见的 Pub/Sub 故障是慢订阅者。发布者没有问题。Redis 起初也没有问题。一个消费者因为进程暂停、网络缓慢或在消息处理程序中执行耗时操作而落后。Redis 不断将消息写入该客户端的输出缓冲区。如果允许缓冲区无限制增长,内存压力会从一个不良订阅者扩散到整个实例。

检查客户端:

redis-cli CLIENT LIST

查找具有 Pub/Sub 命令和大输出内存的客户端:

id=88 addr=10.0.4.12:51244 flags=P db=0 sub=3 psub=0 omem=13421772 obl=128 oll=64 cmd=subscribe

有用的字段:

  • flags=P 表示 Pub/Sub 客户端。
  • subpsub 显示频道和模式订阅计数。
  • omem 是输出缓冲区内存(字节)。
  • obloll 显示输出缓冲区积压详情。
  • cmd=subscribecmd=psubscribe 确认客户端正在执行的操作。

不要对订阅者的 idle 感到恐慌。从命令角度来看,Pub/Sub 连接可能处于空闲状态,但同时仍在接收推送的消息。

使用输出缓冲区限制修复慢消费者

Redis 为 Pub/Sub 客户端提供了自己的输出缓冲区限制类别:

client-output-buffer-limit pubsub 32mb 8mb 60

解读如下:

  • 如果 Pub/Sub 客户端的输出缓冲区达到 32 MB,立即断开连接。
  • 如果超过 8 MB 持续 60 秒,则断开连接。

具体数值应根据您的流量和客户端行为进行调整。常见的错误是将限制设置为 0 0 0,因为在测试中断开连接很烦人。这移除了安全防护。在生产环境中,断开连接通常比让一个卡住的订阅者消耗内存直到 Redis 不稳定要好。

更改 redis.conf 后,根据您的部署流程重新加载或重启。您也可以检查实时值:

redis-cli CONFIG GET client-output-buffer-limit

如果您使用托管 Redis 服务,此设置可能通过提供商的参数组或配置界面暴露,而不是直接编辑文件。

症状:订阅者已连接但未收到任何消息

从最简单的测试开始。打开一个终端:

redis-cli SUBSCRIBE test.channel

打开另一个:

redis-cli PUBLISH test.channel hello

如果在 Redis 主机上有效但应用程序无效,则可能是客户端或网络问题。如果到处都失败,请检查身份验证、ACL、集群路由和实际频道名称。

频道名称是精确的字节字符串。events.userevents:Userevents:user 是不同的频道。在大型系统中,我更喜欢使用常量或一个小型频道命名模块,而不是在服务中分散手写字符串。

模式订阅增加了另一个混淆源:

PSUBSCRIBE events:*

这会匹配 events:user.created,但不会匹配 prod:events:user.created。如果您的应用程序包含环境名称前缀,请将前缀包含在模式中。

身份验证和 ACL 问题

现代 Redis 部署通常使用 ACL 用户,而不是一个共享的 requirepass 密码。Pub/Sub 客户端需要权限来连接、认证、订阅频道模式,有时还需要发布。

快速 CLI 检查:

redis-cli -u redis://app-user:[email protected]:6379 PING
redis-cli -u redis://app-user:[email protected]:6379 SUBSCRIBE events:test

常见的身份验证症状:

  • NOAUTH Authentication required:客户端在认证前发送了命令。
  • WRONGPASS invalid username-password pair:密码错误、用户错误或密钥轮换未在所有地方部署。
  • NOPERM this user has no permissions:用户已认证但缺少命令或频道权限。

使用 ACL 时,频道权限与键权限是分开的。用户可以允许执行普通键命令,但不能订阅您期望的频道。检查配置的用户:

redis-cli ACL GETUSER app-user

测试时不要将生产机密粘贴到 shell 历史记录中。尽可能使用环境变量或临时凭据。

不要将 Pub/Sub 连接与普通命令共享

一旦连接进入订阅模式,它就不再是普通的 Redis 命令连接。它会接收推送的消息,并且只能发出与订阅相关的命令,如 SUBSCRIBEUNSUBSCRIBEPSUBSCRIBEPUNSUBSCRIBEPINGRESET 或根据客户端和 Redis 版本的退出类命令。

如果您从通用连接池中借用连接并调用 SUBSCRIBE,该池化连接实际上不再可用于 GETSET 或其他普通命令。结果可能看起来像随机应用程序超时,因为连接池逐渐被卡在订阅者模式的连接填满。

使用单独的连接:

  • 一个普通客户端或连接池用于常规 Redis 命令。
  • 每个订阅者进程或线程一个长期存在的订阅者连接。
  • 如果您的客户端库推荐,一个单独的发布者连接。

许多客户端库为此提供了 duplicate()new connection 方法。使用它。不要猜测连接池会自行解决。

重连行为很重要

Pub/Sub 在重连后不会重放丢失的消息。如果订阅者断开 30 秒,在此期间发布的每条消息都会丢失。

这对于缓存失效是可以接受的,前提是您的应用程序有后备方案。例如,错过失效事件的服务仍应有 TTL、版本检查或从源数据库重建的方法。没有后备方案,一次网络波动可能导致本地缓存条目过时过久。

重连时,确保客户端重新订阅所有频道和模式。一些库会自动执行此操作。其他库需要您在重连后重新注册处理程序。通过杀死订阅者连接来测试:

redis-cli CLIENT LIST | grep subscribe
redis-cli CLIENT KILL ID <client-id>

然后发布一条测试消息,确认应用程序在重连后收到它。

高扇出可能成为 CPU 和网络压力

PUBLISH 的工作量随着匹配的订阅者和模式数量增加而增长。一个拥有数千订阅者的频道意味着 Redis 必须将消息推送数千次。这可能仍然没问题,但并非免费。

监控:

redis-cli INFO stats
redis-cli PUBSUB NUMSUB events:test
redis-cli PUBSUB NUMPAT

PUBSUB NUMSUB 有助于确认频道是否具有您认为的订阅者数量。PUBSUB NUMPAT 很有用,因为广泛的模式订阅可能会让您惊讶。少数服务使用 PSUBSCRIBE *PSUBSCRIBE events:* 可能会收到远超预期的流量。

如果 Pub/Sub 流量重要且量大,请隔离它。一个专用的 Redis 实例用于 Pub/Sub 可以保护您的缓存和会话存储免受订阅者缓冲区问题和广播峰值的影响。专用实例可以针对 Pub/Sub 调整配置,减少持久性关注,并监控客户端和网络输出。

保持消息处理程序简短

订阅者应读取消息,进行最小验证,并将工作交给其他部分。如果处理程序在读取下一条消息之前调用数据库、发出 HTTP 请求、渲染模板或执行长时间计算,它可能成为您试图避免的慢消费者。

更好的结构:

Redis 消息 -> 轻量级处理程序 -> 本地队列 -> 工作池

本地队列可以是进程内队列、线程池、异步任务队列,或者如果工作必须在进程重启后存活,则可以是持久化代理。关键思想是,从 Redis 读取不应等待最慢的下游依赖。

如果工作必须可靠,Pub/Sub 是错误的原语。带有消费者组的 Redis Streams 为您提供待处理条目和确认。如果您需要死信队列、延迟重试、优先级或长期保留,专用代理可能更好。

实用故障排查清单

当 Pub/Sub 看起来不健康时,我通常按以下顺序操作:

  1. 使用 SUBSCRIBEPUBLISH 手动确认频道。
  2. 检查 CLIENT LIST 中具有高 omem 的 Pub/Sub 客户端。
  3. 检查 client-output-buffer-limit pubsub
  4. 验证身份验证和 ACL 频道权限。
  5. 确认订阅者使用专用连接。
  6. 杀死一个订阅者连接并验证重连和重新订阅行为。
  7. 检查 PUBSUB NUMSUBPUBSUB NUMPAT 以发现意外的扇出。
  8. 决定工作负载是否实际上需要 Streams 或专用代理。

Redis Pub/Sub 仅在它承诺的意义上是可靠的:向当前连接的订阅者实时广播消息。大多数生产事故源于期望它像持久队列一样工作。配置缓冲区限制,隔离高流量,并设计订阅者时假设消息可能丢失,因为有时确实会丢失。

示例事件:一个仪表盘拖慢缓存

想象一个 Redis 实例用于普通缓存键和一个名为 metrics:live 的小型 Pub/Sub 频道。起初,只有一个内部仪表盘订阅。几个月后,几个浏览器网关进程也订阅了,其中一个开始通过慢速 WebSocket 连接发送更新。Redis 不知道下游浏览器很慢。它只看到一个读取不够快的订阅者连接。

第一个症状可能根本不涉及 Pub/Sub。使用普通 GETSET 调用的应用程序请求开始超时。内存攀升。Redis 主机看起来很忙,但 SLOWLOG 没有显示明显的昂贵命令。

有用的线索在 CLIENT LIST 中:

flags=P sub=1 omem=25165824 cmd=subscribe

这告诉您一个 Pub/Sub 客户端有大的输出缓冲区。如果配置的硬限制是 32 MB,该客户端即将被断开。如果限制被禁用,Redis 可能会继续缓冲,直到整个实例处于内存压力之下。

修复不仅仅是提高限制。提高限制可能隐藏问题,并允许下次更严重的内存峰值。更好的响应是:

  1. addrname 或客户端元数据识别订阅者进程。
  2. 如果内存有风险,杀死最差的订阅者。
  3. 添加或收紧 client-output-buffer-limit pubsub
  4. 将慢速下游投递移到 Redis 读取循环外部的内部队列。
  5. 考虑将高流量 Pub/Sub 移到专用 Redis 实例。

客户端名称使这更容易。许多 Redis 库允许您设置连接名称。使用诸如 web-1:pubsub-cache-invalidationsdashboard:metrics-live 的名称,以便 CLIENT LIST 指向所有者,而不仅仅是显示 IP 和端口。

CLIENT SETNAME dashboard:metrics-live

这一小点卫生习惯将模糊的内存事件转变为与拥有订阅者的团队的直接对话。