为什么我最终放弃了分布式 Pub/Sub,选择了房间粘滞(Affinity)

背景补充:在之前的 架构大修记全栈 Monorepo 实践 中,我曾提到过为了可扩展性而追求“全量 Redis 化”和“无状态后端”。但随着重构的深入,我开始重新审视这一选型。

1. 序言

在构建一个支持多人实时协同的应用平台时,开发者通常会面临一个硬核的技术挑战:如何处理成千上万个长连接(WebSocket)在多台服务器间的同步?

在重构项目后端时,我陷入了两条路径的深度纠结:

  • 方案 A:分布式长连接(Stateless Servers + Pub/Sub) —— 理论上的“终极可扩展架构”。
  • 方案 B:房间亲和性(Room Affinity / Sticky Sessions) —— 务实派的“单机逻辑思维”。

经过对开发成本、运行效率以及心智负担的全面复盘,我最终选择了 方案 B


2. 两个流派的深度对垒

流派一:分布式转发(Stateless + Pub/Sub)

核心逻辑:用户可以连接到集群中的任何一台机器。如果同一个协作空间内的用户分散在不同机器,通过 Redis 的发布/订阅(Pub/Sub)机制来跨机交换消息。

  • 优势:极高的高可用性。任意机器宕机,用户重连到另一台机器,逻辑可以无缝延续。
  • 代价
    • 性能损耗:每一次操作都要经历“序列化 -> 传输 -> 反序列化”的循环。
    • 逻辑复杂化:为了防止多个进程同时修改同一个业务状态,必须引入分布式锁,开发量倍增。

流派二:房间亲和性(Room Affinity)

核心逻辑:通过网关调度,确保同一个协作空间内的所有用户都连接到同一台物理服务器上。

  • 优势:业务逻辑处理极度简单,数据在内存中原地完成闭环。
  • 代价:若单台服务器意外重启,该机器上正在进行的实时任务进度会丢失(除非引入额外的持久化快照)。

3. 维度对比:为什么“简单”才是最高级的生产力?

在选型过程中,我从以下三个核心维度进行了思考:

3.1 开发者的小组心智负担 (Cognitive Load)

维度 房间亲和性 (Affinity) 分布式转发 (Pub/Sub)
编程模型 像写单机程序。可以放心地使用局部变量、指针、Channel。 像写分布式数据库。每一步都要考虑数据在不在本地,是否需要远程同步。
状态一致性 天生一致。通过简单的局部互斥锁(Mutex)即可解决竞争,逻辑清晰。 极其沉重。在高频交互下,处理分布式锁冲突会消耗大量精力。
调试难度 。看一台机器的本地日志就能还原完整的业务流。 。必须汇总全集群日志才能拼凑出一个完整的操作现场。

3.2 运行效率与成本

  • Affinity(亲和性):数据在内存中直接下发给同一个进程内的 Socket,延迟是微秒级的。对于高频互动的操作(如实时绘图、协同白板),这种低延迟是决定性的。
  • Stateless(无状态):每一条消息都要走一次 Redis 转发。在高并发场景下,序列化开销和 Redis 的网络 I/O 往往先于业务逻辑达到性能瓶颈。

4. 最终选择:回归工程常识

我最终选择了 方案 B(房间亲和性),原因在于它完美平衡了“开发效率”与“系统鲁棒性”:

  1. 利用 Go 的并发威力:Go 语言的 Goroutine 处理成千上万个局部连接非常轻松。在达到单机瓶颈之前,过度拆分架构只会大幅降低上线速度。
  2. “伪分布式”的容灾退路:虽然选择了亲和性,但我并没有完全抛弃 Redis。每当业务状态发生关键变更,系统会异步发送快照至 Redis。若某台服务器崩溃,用户重连至新节点后,新节点从 Redis 读回快照恢复现场。这既保留了内存操作的高效,又获得了分布式的数据安全感。

5. 结语:架构是为了解决问题,而非创造问题

在重构过程中,我意识到开发者很容易陷入“为了架构而架构”的陷阱。总觉得不写一段分布式逻辑就显得技术含量不足,但现实是:

  • 开发效率(Time to Market)往往比架构完美度更重要。
  • 低心智负担的系统往往更不容易出 Bug。

通过 “统一入口调度 + 内存状态机”,我只用了原计划 1/3 的开发时间就实现了稳定的实时同步。这种简洁带来的掌控感,是任何复杂的分布式中间件都无法比拟的。

最好的架构,是能让你在项目快速迭代的同时,依然能睡个好觉。



为什么我最终放弃了分布式 Pub/Sub,选择了房间粘滞(Affinity)
https://erdianzhang.cn/2026/03/06/affinity-over-pubsub/
作者
兔特科技
发布于
2026年3月6日
许可协议