后端系统工程 08:群消息扇出与成员版本
后端系统工程 08:群消息扇出与成员版本 本批导航:00 技术方案怎样接受验证|01 API 契约与业务不变量|02 负载模型与容量预算|03 单库基线与故障实验工具|04 持久化边界|05 长连接接入|06 发送确认|07 排序与增量同步|08 群消息扇出|09 多设备已读|10 网关故障与路由收敛|11 撤回与集成验收 单聊的一条消息只送达两个人的几台设备,群消息要送达几十到几百名成员的设备。第 02 篇的模型已经表明,扇出带来的送达成本远大于消息写入本身,而且随群规模线性增长。本篇处理群消息的两个问题:送达成本怎样控制,以及在成员不断加入、退出的情况下,每条消息应该送给谁、谁可以读取它。 第二个问题比第一个更容易出错。成员变更与消息发送是并发的;推送是异步的,从消息提交到推送发出之间,成员可能已经变化。若推送目标按“推送时刻的成员”计算,刚退群的成员可能收到退群之后的消息,刚入群的成员可能收到入群之前的消息。本篇按事件序号计算成员资格,使这两种情况都不会发生。 1. 需求与不变量 编号 不变量 对应需求 F1 序号为 s 的事件,只推送给在 s 上属于成员...
后端系统工程 09:多设备已读与未读状态
后端系统工程 09:多设备已读与未读状态 本批导航:00 技术方案怎样接受验证|01 API 契约与业务不变量|02 负载模型与容量预算|03 单库基线与故障实验工具|04 持久化边界|05 长连接接入|06 发送确认|07 排序与增量同步|08 群消息扇出|09 多设备已读|10 网关故障与路由收敛|11 撤回与集成验收 一个用户同时登录手机和电脑时,“这条消息看过了吗”有两个不同的问题:这台设备上有没有这条消息,以及这个人有没有读过它。前者属于设备,后者属于用户。用户在手机上读完消息,电脑上的未读数应该消失;但电脑并没有因此获得这些消息的数据,如果它此后离线,打开会话时仍然要从服务端拉取。 本篇把两类状态分开存储:送达游标按设备,已读位置按用户;未读数不存储,而是在查询时由已读位置和事件推导出来。重点处理三种异常:两台设备的已读上报乱序到达,设备重装后旧安装发来的确认,以及设备从旧的本地备份恢复后上报一个更小的游标。 1. 需求与不变量 编号 不变量 观察位置 M1 送达游标按 (设备, 会话) 记录,单调,不超过 head device_cursors ...
后端系统工程 10:网关故障后的重连与路由收敛
后端系统工程 10:网关故障后的重连与路由收敛 本批导航:00 技术方案怎样接受验证|01 API 契约与业务不变量|02 负载模型与容量预算|03 单库基线与故障实验工具|04 持久化边界|05 长连接接入|06 发送确认|07 排序与增量同步|08 群消息扇出|09 多设备已读|10 网关故障与路由收敛|11 撤回与集成验收 第 05 篇处理的是单个连接的生命周期:握手、心跳、慢接收端,以及同一设备的新连接取代旧连接。那里的故障都是局部的,一次只影响一个连接。网关进程整体失效则不同。一个进程上挂着成千上万个连接,进程消失的瞬间它们同时断开,随后几乎同时尝试重连。这时有三个问题需要回答: 死掉的网关没有机会清理自己的路由记录,谁来清理,什么时候清理? 大量设备同时重连,怎样避免它们把剩下的网关和数据库一起压垮? 重连过程中网络会产生延迟和重复。某个旧握手在新连接建立之后才到达,怎样保证它不会把设备的归属改回已经失效的连接? 本篇在第 05 篇的路由声明之上增加三样东西:网关租约、握手准入和带抖动的重连退避。另外补上计划内下线(排空)的流程。所有结论都来自本地多进程实验:...
后端系统工程 11:撤回、历史保留与消息系统集成验收
后端系统工程 11:撤回、历史保留与消息系统集成验收 本批导航:00 技术方案怎样接受验证|01 API 契约与业务不变量|02 负载模型与容量预算|03 单库基线与故障实验工具|04 持久化边界|05 长连接接入|06 发送确认|07 排序与增量同步|08 群消息扇出|09 多设备已读|10 网关故障与路由收敛|11 撤回与集成验收|下一篇 12 有界连接池与等待取消 前面七篇都在处理一条消息“怎样写进去、怎样送出去”。本篇处理消息写入之后的变化:发送者撤回、保留期满后被删除、整个数据库从备份恢复。三者有一个共同点,都会和前面建立的不变量发生冲突。 第 07 篇假定已提交事件不可变,客户端可以只按序号增量同步。撤回却要求修改一条已经被别人收到的消息。 第 04 篇保证会话内序号从 min_seq 起连续,第 01 篇保证同一请求键只产生一条消息。保留清理要删除的,正好是序号和请求键的载体。 备份恢复会把数据库带回过去某个时刻,所有不变量都需要在副本上重新检查,而不能默认成立。 本篇的另一项任务是集成验收:把第 04–11 篇的全部 71 项检查与第 00 篇的需求卡 R...
后端系统工程 01:API 契约与业务不变量
后端系统工程 01:API 契约与业务不变量 本批导航:00 技术方案怎样接受验证|01 API 契约与业务不变量|02 负载模型与容量预算|03 单库基线与故障实验工具|04 持久化边界|05 长连接接入|06 发送确认|07 排序与增量同步|08 群消息扇出|09 多设备已读|10 网关故障与路由收敛|11 撤回与集成验收 第 00 篇把消息服务的承诺写成了需求卡 R1–R7。需求卡说明系统保证什么,接口契约说明调用方在每一种回复下可以相信什么。两者之间有一个容易被忽略的落差:服务端内部即使做到了“同一请求只写一行”,如果回复格式不能让客户端区分“首次成功”“重复请求的成功”“请求被拒绝且不应重试”“结果未知、应当重试”,客户端仍然会做出错误决定,例如对一条已经提交的消息弹出“发送失败”。 本篇定义消息服务的请求格式、回复格式和错误码,重点处理三类异常请求:重放、越权和过期。所有判定都落到可执行检查上,检查编号来自第 04、06、11 篇的实验。 1. 契约要回答的问题 调用方每发出一次请求,最终处在以下四种状态之一: 收到成功回复:操作已生效,回复中的标识(seq、m...
后端系统工程 02:负载模型与容量预算
后端系统工程 02:负载模型与容量预算 本批导航:00 技术方案怎样接受验证|01 API 契约与业务不变量|02 负载模型与容量预算|03 单库基线与故障实验工具|04 持久化边界|05 长连接接入|06 发送确认|07 排序与增量同步|08 群消息扇出|09 多设备已读|10 网关故障与路由收敛|11 撤回与集成验收 “峰值每秒一千多条消息”这样的数字本身不能指导设计。同样是每秒 1250 条消息,单聊为主的负载和大群为主的负载对数据库、网关和带宽的压力相差一个数量级以上;同样的消息量,客户端每收到一条就确认一次和攒够十几条再确认一次,数据库写入量也相差数倍。容量预算要回答的不是“总量多大”,而是“哪一种资源先用完,以及哪个参数一变,瓶颈就会换位置”。 本篇把第 01 篇定义的接口操作折算成五类资源消耗,用一个带单位的脚本 examples/design/capacity.py 计算,再通过敏感性扫描找出决定性参数。全部规模参数都是教学假设,不来自任何真实系统;结论只在这些假设下成立。 1. 模型要回答的问题 在基线参数下,哪类资源最先达到预算?超出多少? 改变单个参数...
后端系统工程 03:单库基线与故障实验工具
后端系统工程 03:单库基线与故障实验工具 本批导航:00 技术方案怎样接受验证|01 API 契约与业务不变量|02 负载模型与容量预算|03 单库基线与故障实验工具|04 持久化边界|05 长连接接入|06 发送确认|07 排序与增量同步|08 群消息扇出|09 多设备已读|10 网关故障与路由收敛|11 撤回与集成验收 前三篇给出了承诺(第 00 篇)、契约(第 01 篇)和负载模型(第 02 篇)。它们都需要一个运行环境来检验:承诺需要可执行的检查,契约需要在崩溃和并发下观察回复与存储状态,模型的结构性质需要测试。本篇搭建这个环境,并规定实验的写法。 基线选择单个 PostgreSQL 实例。不是因为单库最终够用,而是因为它让每一个结论都可以追溯:所有持久状态在一个地方,事务语义由数据库文档定义,崩溃恢复由 WAL 保证,故障可以在进程级别精确注入。在单库上无法成立的不变量,换成分布式存储也不会自动成立;单库上成立的不变量,在迁移到更复杂的存储时可以作为回归基准。 1. 需求:一个实验工具要保证什么 编号 要求 原因 E1 每次实验从已知状态开始 不同实...
后端系统工程 05:长连接接入与连接归属
后端系统工程 05:长连接接入与连接归属 本批导航:00 技术方案怎样接受验证|01 API 契约与业务不变量|02 负载模型与容量预算|03 单库基线与故障实验工具|04 持久化边界|05 长连接接入|06 发送确认|07 排序与增量同步|08 群消息扇出|09 多设备已读|10 网关故障与路由收敛|11 撤回与集成验收 第 04 篇的存储层回答了“消息写到哪里”。要把消息送到设备,服务端需要一条可以主动下发数据的通道,并且知道每台设备此刻连在哪一个网关进程的哪一条连接上。本篇实现这条通道:基于 WebSocket 的网关进程 examples/im/src/im/Gateway.java,以及记录连接归属的 routes 表。 长连接的难点不在建立,而在于连接的状态不可靠:TCP 连接可能已经失效,两端却都不知道;对端可能还连着,但已经不读数据;同一台设备可能同时存在新旧两条连接,旧连接的清理逻辑可能误删新连接的记录。本篇针对这三类情况分别给出检测与处理方法,并用本地多进程实验验证。 1. 需求与不变量 编号 不变量 对应需求 G1 一台设备在任一时刻至多有...
后端系统工程 06:发送确认与可恢复的消息投递
后端系统工程 06:发送确认与可恢复的消息投递 本批导航:00 技术方案怎样接受验证|01 API 契约与业务不变量|02 负载模型与容量预算|03 单库基线与故障实验工具|04 持久化边界|05 长连接接入|06 发送确认|07 排序与增量同步|08 群消息扇出|09 多设备已读|10 网关故障与路由收敛|11 撤回与集成验收 一条消息从发送方到接收方,要经过两段互相独立的确认:发送方需要知道“服务端已经接收并保存”,服务端需要知道“接收设备已经保存”。两段确认的承担者、观察位置和失败方式都不同。把它们混为一谈,常见的结果是用“已推送”代替“已送达”,或者把“已送达”的状态记在发送方的消息上,导致多设备场景下状态无法表达。 本篇把这两段确认分开处理。前一段由第 01 篇的幂等契约和第 04 篇的事务保证,本篇用三个崩溃点验证它;后一段引入设备送达游标和重投扫描,处理推送丢失的情况。 1. 需求与不变量 编号 不变量 观察位置 D1 发送方收到 sent{seq} 时,消息所在事务已经提交 events 行 D2 发送方没有收到回复时,用...
后端系统工程 07:会话内排序与断线增量同步
后端系统工程 07:会话内排序与断线增量同步 本批导航:00 技术方案怎样接受验证|01 API 契约与业务不变量|02 负载模型与容量预算|03 单库基线与故障实验工具|04 持久化边界|05 长连接接入|06 发送确认|07 排序与增量同步|08 群消息扇出|09 多设备已读|10 网关故障与路由收敛|11 撤回与集成验收 第 06 篇让投递变成“至少一次”:推送可能重复,可能丢失后由同步补齐。这带来两个新问题。第一,消息以什么顺序显示?推送和同步是两条独立的路径,到达顺序可能与发送顺序不同。第二,客户端怎样记住“已经拿到了哪里”,使得断线重连后只拉取缺少的部分,既不重复也不遗漏? 本篇的答案围绕一个数展开:会话内序号 seq。服务端保证序号在已提交事件中连续,并且提交顺序与序号顺序一致;客户端维护一个只在连续范围上前进的游标;同步接口以游标为键分页。三者合起来,使得任意交错的推送、同步与新写入都不会让客户端漏掉或重复应用事件。 1. 需求与不变量 编号 不变量 承担者 O1 同一会话内,所有成员看到的事件顺序相同,即序号顺序 服务端(第 04 篇的会话行锁...










