后端系统工程 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 每次实验从已知状态开始 不同实...
后端系统工程 04:会话、成员与消息的持久化边界
后端系统工程 04:会话、成员与消息的持久化边界 本批导航:00 技术方案怎样接受验证|01 API 契约与业务不变量|02 负载模型与容量预算|03 单库基线与故障实验工具|04 持久化边界|05 长连接接入|06 发送确认|07 排序与增量同步|08 群消息扇出|09 多设备已读|10 网关故障与路由收敛|11 撤回与集成验收 从本篇开始,第 00–03 篇建立的方法落到一个逐步演进的最小即时通信工程上(代码在 examples/im/)。第一个问题是持久化边界:哪些状态必须在同一个事务里写入,哪些可以分开;哪个状态是权威来源,哪个只是派生。边界划错的后果往往要到故障时才显现——例如消息写入了,但请求键的记录在另一个存储里没写成功,客户端重发后就多出一条消息。 本篇确定会话、成员、消息三者的表结构,说明会话内序号在哪个锁下分配、请求键为何与消息放在同一行,以及成员资格为何用序号区间而不是一个“当前成员”标志表示。所有实验直接调用存储层,连接真实的 PostgreSQL 16,不经过网关。 1. 需求与不变量 本篇负责需求卡中的 R1(部分)、R2、R3(数据模型部分),以...
后端系统工程 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 篇的会话行锁...
后端系统工程 18:消息队列接入与积压恢复
系列导航:上一篇 17 从持久日志构建最小消息队列 | 本篇 18 | 下一篇 19 延迟任务的领取与取消 上一篇把“受理”“落盘”“消费确认”拆成了三条线,用一个自制的单机日志逐一观察。换成成熟的消息中间件之后,这三条线依然存在,只是分别落到了客户端协议里:publisher confirm 说明 broker 接下了消息,消费确认把消息的所有权交给消费者,两者之间还隔着队列、重投和消费者自己的数据库事务。中间件不会替应用决定先提交还是先确认,也不会替应用判断一条消息是不是已经处理过。 本篇用真实的 RabbitMQ 3.12.1 节点和 PostgreSQL 16 检验接入方的契约:生产确认与不可路由消息、提交后再确认与去重表、毒消息的有限次重试和隔离、积压时的有界预取。输入是与第 17 章共用的 3000 条合成订单事件,代码在 examples/18-rabbitmq-consumer/,输出在 evidence/18/output.txt。 实验只有一个 RabbitMQ 节点。它能说明单个 broker 进程重启或被 kill -9 之后,已确认的持久消息还在不在;它...
后端系统工程 20:发号服务的唯一性与失败模式
系列导航:上一篇 19 延迟任务的领取与取消 | 本篇 20 | 下一篇 21 有限能力的数据库路由代理 “生成一个唯一 ID”这句话省略了三个限定:在多大范围内唯一,在哪些故障之后仍然唯一,除了唯一还要求什么。一张订单表的主键只需在该表内不重复;跨系统传递的标识要在所有系统中不重复;作为分片键或排序键时,还会被要求大致按时间递增。不同方案对这些问题的回答不同,它们的失败模式也不同:数据库号段在进程崩溃后留下空洞,时间型标识在时钟回退时可能重复,节点号方案在两个进程误用同一节点号时必然重复。 本篇实现一个基于 PostgreSQL 的号段发号器,作为主方案验证并发取号、进程重启、数据库故障下的唯一性;再实现 UUIDv4、UUIDv7 和 Snowflake 式 64 位标识作为契约对照,用受控时钟测试时钟回退,用真实多进程测试节点号冲突。代码在 examples/20-id-service/,输出在 evidence/20/output.txt。 需求契约 号段服务的使用方只调用一个操作:为某个业务标签取下一个号。服务承诺: 同一业务标签下发出的号全局唯一,这一点在发号进程崩...
后端系统工程 19:延迟任务的领取与取消
系列导航:上一篇 18 消息队列接入与积压恢复 | 本篇 19 | 下一篇 20 发号服务的唯一性与失败模式 “三十分钟后关闭未支付的预约”“两小时后提醒参会者”,这类需求都可以写成一句话:到了某个时刻之后,执行一次某个动作。真正实现起来,这句话里的“到了”“执行”和“一次”都要重新定义。到期时刻由谁的时钟判断?多个执行者同时扫描时,谁拿到任务?执行者拿到任务后被杀死或被暂停,任务何时由别人接手?原执行者恢复后又提交结果,系统听谁的?取消请求与到期同时到达,返回给调用方的“已取消”究竟保证了什么? 本篇用 PostgreSQL 保存任务,多个独立 JVM 进程担任执行者,用真实的 kill -9 和 SIGSTOP 制造执行者失联,逐一回答这些问题。结论先写在契约里:系统允许同一任务有多次执行尝试,“一次”只落在副作用上,并且要靠唯一约束或幂等键来保证。代码在 examples/19-delayed-tasks/,输出在 evidence/19/output.txt。 需求契约 调用方可以做两件事:安排一个任务(类型、载荷、延迟、最大尝试次数),以及取消一个任务。执行者可以做四件...
后端系统工程 23:分片扩容时的数据交接
系列导航:上一篇 22 非分片键查询与全局分页 | 本篇 23 | 下一篇 24 在线变更的数据兼容 两个分片扩到三个,需要把一部分数据从旧分片搬到新分片,同时写入不停。这件事拆开看有三个难点。复制需要时间,复制期间源端还在变化,复制完的副本一出来就是旧的。路由表改了之后,已经拿着旧路由的写入方不会立刻知道,它们的写会继续打到源端。写入方遇到超时会重试,一次请求可能在源端提交了、重试却落到目标端。任何一处处理不当,结果都是某些已经告诉调用方“成功”的写,在迁移完成后不见了,或者已经删除的行又回来了。 本篇实现按桶迁移:版本化路由、快照复制、按事务水位追赶、冻结栅栏、按键与版本校验、切流。三个独立的写入进程在迁移全程持续写入、删除与重试,迁移结束后逐键核对“所有已确认写”。代码在 examples/23-shard-rebalance/,输出在 evidence/23/output.txt。 需求与不变量 数据是合成的键值记录 items(item_id, bucket, val, row_version),写操作有三种:创建(值为 0,版本 1)、增量(val += delta,...















