从慢订阅者到发布者:LiteBus 如何用 Unix Socket 背压保护可靠 Topic

从慢订阅者到发布者:LiteBus 如何用 Unix Socket 背压保护可靠 Topic

"消息不能静默丢失",听起来像是给队列足够大的内存就行。实际不是。

在发布订阅系统里,如果某个订阅者处理不过来,而发布者仍然无限快速地产生消息,任何有限队列最终都会满。此时系统必须做选择:

复制代码
丢消息;
无限占内存;
让慢订阅者拖垮整个 Broker;
或把压力沿链路传回发布者。

LiteBus 阶段 4A 的 RELIABLE Topic 选择最后一种:背压。

本文解释它如何在单线程 Broker、Unix Domain Socket 和非阻塞 epoll 模型中工作,以及它保证什么、不保证什么。


问题:慢订阅者会把消息堆在哪里

假设有一条可靠 Topic:/robot/critical_event,三个角色:

复制代码
Publisher:持续发布关键事件。
Broker:   收到发布消息后,路由给订阅者。
Subscriber:因业务处理慢,读取速度低于发布速度。

如果 Broker 对 Subscriber 持续 send(),但 Subscriber 不及时读取,Broker 端 Socket 最终会暂时不可写,send() 返回 EAGAIN。

这时已经不能继续把数据交给内核了。若 Broker 仍在自己的内存里不断排队,outbound_frames 最终也会达到上限。

LiteBus 的每个连接都有独立输出队列:

复制代码
outbound_frames:待发送的完整 LiteBus 帧。
outbound_offset:队首帧已发送的字节数。
queued_bytes:   队列累计字节数。

当前上限为:256 帧 / 1 MiB。

这些上限不是吞吐指标,而是内存保护线。


三种 Topic 策略的分歧点

当 Subscriber 输出队列已满时,三种策略的选择不同:

策略 行为
BEST_EFFORT 丢弃刚到的新完整 Event
LATEST 删除同 Topic、尚未开始发送的旧状态,尝试保留最新状态
RELIABLE 不丢 Event;不继续接受这个 Publisher 的更多发布数据;等 Subscriber 恢复

这里的 RELIABLE 不是"无限缓存",而是**"拒绝无限积压"**。


背压不是停止连接,而是暂停 EPOLLIN

LiteBus Broker 使用 epoll 监听每个 Client fd。正常情况下:

复制代码
EPOLLIN:      Socket 有数据可读。
EPOLLOUT:     Socket 可继续发送待发帧。
EPOLLERR/HUP/RDHUP:连接错误或关闭。

当某个可靠订阅者队列满、Broker 发现不能再把当前 Event 放入该 Subscriber 队列时,会把这条消息记录为待路由发布:

复制代码
deferred_publications:
  key:   publisher_fd
  value: 按发布顺序保存的未完成路由消息
  每条记录:Event + 还没有成功入队的 RELIABLE subscriber_fd 集合

随后 Broker 更新 Publisher fd 的 epoll 事件:

复制代码
移除 EPOLLIN;
保留 EPOLLERR、EPOLLHUP、EPOLLRDHUP。

这不是关闭连接,也不是清空缓存。 它只是告诉内核:

这个 Publisher 后续即使有数据可读,也暂时不要因为可读事件唤醒 Broker。

因此 Broker 不会继续调用 recv(),也不会把 Publisher 的后续字节继续搬到自身用户态内存。


数据会不会丢

正常情况下不会。数据会沿着链路逐层停住:

复制代码
Publisher 应用层
    │ send()
    ▼
Publisher 内核发送缓冲区
    │
    ▼
Broker 内核接收缓冲区
    │ Broker 已暂停 EPOLLIN,不再 recv()
    ▼
Broker 用户态 receive_buffer_

当 Broker 内核接收缓冲区满后,Publisher 再继续发送:

  • 非阻塞 Socket :send() 返回 EAGAIN / EWOULDBLOCK
  • 阻塞 Socket :send() 阻塞,直到 Broker 恢复读取

LiteBus Node 使用非阻塞 Socket。因此 Publisher Node 的发送逻辑会等待 EPOLLOUT;若长期不可写,最终按自身发送超时返回失败。

这就是背压的含义:

复制代码
慢 Subscriber
    ↓
Subscriber 输出队列满
    ↓
Broker 暂停读 Publisher
    ↓
Broker 接收缓冲区满
    ↓
Publisher 最终无法继续高速发送

压力没有消失,而是从最慢的位置向上游传播。


Broker 如何恢复

当 Subscriber 再次读取数据,Broker 收到该连接的 EPOLLOUT:

  1. 从 outbound_frames 队首继续发送。
  2. 已部分发送帧从 outbound_offset 继续,不会重新发送头部,也不会跳到下一帧。
  3. 队列腾出空间后,Broker 重试 deferred_publications。
  4. 一条待路由 Event 对所有 RELIABLE Subscriber 都已成功入队后,删除该待路由记录。
  5. Publisher 没有更多待路由 Event 后,Broker 重新为该 Publisher 添加 EPOLLIN。

一旦重新监听 EPOLLIN,若 Broker 内核接收缓冲区中已有积压数据,epoll 会很快再次报告可读,Broker 继续按原顺序读取、解析和路由。


为什么已发送一半的帧绝不能丢

LiteBus 使用 SOCK_STREAM。Socket 传输的是连续字节,不保留应用消息边界。

每个 LiteBus 帧的格式是:

复制代码
[4 字节大端长度][Protobuf Envelope]

假设一帧只发送了前 300 字节,随后 send() 返回 EAGAIN。若 Broker 删除剩余部分并直接发送下一帧,接收端看到的是:

复制代码
[旧帧的前 300 字节][新帧的长度字段][新帧内容...]

接收端无法再判断下一个长度字段从哪里开始,后续全部字节都可能无法解析。

因此 LiteBus 的硬规则是:

复制代码
已开始发送的队首帧:继续发送到完整,或连接关闭。
只有尚未开始发送的完整帧:才可能因 Best Effort 或 Latest 策略被丢弃/替换。

这也是 RELIABLE、LATEST 和 BEST_EFFORT 共用输出队列时最重要的安全边界。


RELIABLE 保证什么

阶段 4A 的可靠 Topic 语义是:

复制代码
Broker 正常运行期间:
    不因 Subscriber 输出队列满而静默丢弃可靠消息。
队列压力:
    通过暂停 Publisher 读取向上游传播。
帧完整性:
    已开始发送的帧不会被截断或错位。

它不保证:

复制代码
Broker 崩溃后恢复未投递消息;
设备断电后恢复未投递消息;
Subscriber 断开后永久保存历史;
跨重连自动重放;
业务处理成功确认。

后面这些需要磁盘持久化、确认协议、重放、幂等和去重机制,属于持久化与运行管理阶段------不应该被"可靠 Topic"这个名字掩盖。


什么时候该用 RELIABLE

适合:

复制代码
关键告警;
不可静默遗漏的状态变化;
需要上游减速而不是丢失的控制事件。

不适合:

复制代码
高频图像;
激光点云;
高频原始传感器数据;
只关心最新值的位置、电量等状态流。

建议的选择:

数据类型 推荐策略
图像、雷达 BEST_EFFORT
姿态、位置、电量 LATEST + retain_last + transient
关键事件 RELIABLE

写在最后

可靠不是"总是更好"。

它的代价是:一个慢 Subscriber 可能降低 Publisher 的实际发布速度。

选择 QoS 时,应先明确业务真正不能丢失的是什么------不是所有数据都值得用背压保护,也不是所有数据都能承受静默丢失。

背压的本质,是用"上游变慢"换"不丢数据 + 不 OOM"。它把选择权交回给了系统设计者:你愿意为可靠性付出多少延迟代价?


本文基于 LiteBus 阶段 4A 的 RELIABLE Topic 实现。如果你在构建类似的发布订阅系统,希望这篇能帮你理解背压的完整链路和边界。

相关推荐
上海研博数据1 小时前
HY-K 太阳能控制器协议|水利物联太阳能供电系统接入指南
网络
AI数据标注猿1 小时前
开源视觉数据工具FiftyOne最新版本:标注工具开始“读懂机器人”
机器人
piaoyiren2 小时前
Python FTP 服务端项目详细功能文档
网络·数据库·python
半杯咖啡半行码2 小时前
QML进阶实战(二):异步网络、性能优化与工程化落地
网络·qt·性能优化·qml
悟天特斯2 小时前
安防一体化:从“各管各的“到“全局联动“
java·大数据·网络·人工智能·物联网
骑着蜗牛撵大象3272 小时前
手写一个 HTTP 服务器:从 socket 到 epoll,Linux 上到底怎么把请求变成响应
linux·服务器·网络·http·epoll·socket编程·并发模型
wusam2 小时前
基于HCL的python脚本编程(2)
网络·python·智能路由器
乌恩大侠2 小时前
从真实 5G 网络采集 AI 训练数据:基于 NVIDIA Aerial Data Lake 构建 AI-RAN 数据平台
网络·人工智能·5g·aerial·o-ru·ai-ran
合众恒跃3 小时前
合众恒跃具身机器人嵌入式AI解决方案
人工智能·机器人·rk3588·jetson·英伟达·瑞芯微·rk3576