消息 · 跳数 | 音视频 | 运维 | 待优化清单

消息链路必须按跳数比

第二轮。从本地 id 到对端可见,只比较 hop 顺序与失败边,不再摊功能表。

第一轮把「有没有 seq / 有没有 MQ / 支不支持大群」摊成功能表,比出来的是架构标签,不是失败边。同一条私聊从本地 id 到对端可见,只有 hop 顺序是可证伪的:H5 persist 或入队、H6 seq 出生、H7 sender-ack 谁先谁后,决定超时重试是同一条消息还是第二条,也决定杀进程后对端补不补得回来。出厂关发送(H4 硬拒、H5–H8 永不发生)和 MQ 打开后的 persist 路径不是同一条流水线的两个开关,不能压成一列「发送」。

同品类只取 OpenIM、野火、Tinode、JuggleIM、Turms 与 layababa。Rocket.Chat 与 HuLa 只作参照。下文 hop 核自各仓本地树与当时工作树;layababa 相对 origin/dev 的漂移本轮未用 git show 复核。

一、为什么必须按跳数比

功能表会把三件不同的事收成一个勾:发送成功、已持久化、对端已看见。它们在所有被核过的树上都不是同一个 RPC 结果。

可比较的只有同一条 hop 链:H1 铸本地身份;H2 客户端落库或 outbox;H3 上线;H4 persist 前硬拒(无行、无 seq);H5 服务端 persist 或入队;H6 seq 出生;H7 回给发送方的 ack;H8 对端通知或推送。超时、崩溃、重连、换设备、群扇出,都是这条链在失败边上的同一组 hop。第一轮漏掉的正是 H6 钉在哪一跳。OpenIM 的回执没有 Seq、野火的 PUBACK 没有每人序号、Tinode 的控制帧带着 params.seq——三者都「有消息 id」,hop 完全不同。

还有一条必须拆开的边:H4 拒发。layababa 出厂走 refuseWithoutNewChainOwner 抛限流;野火超频、Turms 请求过频、Tinode 503、JuggleIM 连接超限都是同一跳:身份未铸、行未写。把它和 MQ01 先可见再落库、MQ06 先落库再出箱画进同一条发送路径,后续所有崩溃对比都会错位。

二、一条私聊发出去:从本地 id 到对端可见

OpenIM(ack 在 seq 之前)

  1. H1 铸 ClientMsgID,本地 Seq=0。
  2. H2 插入本地发送中。同号且非失败则重复错误。
  3. H3 WebSocket 请求响应,等待十秒。
  4. H4 入站限流默认关;打开则耗尽、无消息。
  5. H5 每次新的服务器消息号,入队。
  6. H7 回执只有服务器号、客户端号、时间,没有 Seq;SDK 标成功,本地 Seq 仍为 0。
  7. H6 之后:传输进程分配序号、写缓存、再落库、再入推送队。
  8. H8 推全文(含 Seq)。发送方靠同步回填 Seq。

缺跳:发送回执上没有 seq;服务端不对客户端号做唯一约束。

野火(ack 在 seq 之前,收件箱序号另铸)

  1. H1 社区协议无客户端本地号。
  2. H2 官方客户端不在本树。
  3. H3 MQTT QoS1。
  4. H4 漏桶拒绝则 PUBACK 带超频,不生成号。
  5. H5 雪花号进 Hazelcast;SQL 只是调度。
  6. H7 PUBACK 带回 mid 与时间。
  7. H6 之后给每个接收方铸个人序号。
  8. H8 QoS0 通知带水位,对端再拉。

缺跳:ack 不含 seq;无 clientMessageId。

Tinode(先 persist 出 seq,再 ack)

  1. H1 发布帧的 id 只是请求相关,空 id 仍落库,只是不回 202。
  2. H3 发布。
  3. H4 广播满则 503,无保存。
  4. H5–H6 取 lastID+1,先改主题再插消息(非一事务)。
  5. H7 控制帧 202,含义是已保存,带着 seq。
  6. H8 已挂接推数据帧;未挂接推出席提示再拉取。

缺跳:同号检查仍是待办;发布 id 不幂等。

JuggleIM(seq 在 ack 前铸,persist 异步)

  1. H1 可选客户端号,空则服务端生成。
  2. H3 发布需确认。
  3. H4 限流则空号、seq=0。静默拦截器也回成功且 seq=0、不落库,ack 形状与真发送相同。
  4. H6 锁内递增会话序号。
  5. H7 确认带消息号与序号。含义是序号已分配且发件箱、历史、分发已入队,不是收件箱已提交。
  6. H8 首条或单端推全文,否则通知再同步。

缺跳:ack 不等于 persist;去重只是十分钟进程缓存。

Turms(打开持久化则插入后 ack;默认无会话 seq)

  1. H1 请求号只做远程调用相关。
  2. H4 网关令牌桶拒则无远程调用。
  3. H5 插入雪花号。打开序号则先自增再插入。
  4. H6 默认不发生;打开后序号也不上推送体。
  5. H7 成功通知;持久化打开则带消息号。
  6. H8 返回后再异步通知相关用户。

缺跳:无请求号幂等;推送体无序号。

layababa 出厂关发送(H4 终结)

  1. H1 乐观行加发送出箱,客户端号已固定。
  2. H2 同号持久出箱。
  3. H3 长连接或 HTTP,从不并行。
  4. H4 热私聊不接受 → 拒发 → 限流。无预备、无库、无队列、无序号。
  5. H5–H8 整段缺跳。客户端按限流每五秒重试同一号。

layababa 打开 MQ01 私聊

  1. H1–H3 同出厂。
  2. H6 先于 ack:Redis 自增并预备;同摘要走已有,回收同一序号。
  3. 发布确认;超时或否认则保持预备,禁止新序号。
  4. 标可见并等耐久门槛;日志写明库仍可能待写。
  5. H7 回执带着序号。库由消费者在 ack 之后写成已持久。
  6. H8 等到可见或已持久且摘要匹配才推全文。

缺跳:H7 不等于库耐久;确认未知被做成 HTTP 200;已声明不可重试的未知结果码,发送路径零抛出。

Rocket.Chat 参照:随机 id 即主键,插入后接口成功;事后钩子在返回之后。无 H6。HuLa 参照:本地临时号,接口回写;服务端 hop 不在树内。

发送 ack 只允许这一张表:

路径H7 实际承诺H6 是否已发生耐久行是否已提交
OpenIM入队
野火接受 mid 与时间否(收件箱序号更晚)缓存已放,SQL 未保证
Tinode 持久化开202 等于保存成功是(两步,非一事务)
JuggleIM序号加已入队
Turms 持久化开插入完成默认否
layababa MQ01Redis 已可见库可能还没有
layababa MQ06 群库加出箱保证
layababa 出厂关限流永不永不

没有任何一家把「对端设备已投递」放进这条发送 ack。

三、超时之后:同号重放还是第二条消息

问的不是支不支持重试,是 H3 丢失之后 H1 是否复用,以及会不会再走一遍 H6。

同品类里,超时后仍能撞回「已出生的那条序号」的,只有 layababa 打开 MQ 之后(持久出箱加预备或指纹)和 JuggleIM 的单进程缓存。OpenIM、野火、Turms、Tinode 都会在 H6 另铸身份。这是 hop 事实,不是排名。

四、在线对端怎么拿到

H8 不是推送功能,是 H7 之后接收工作何时出生、序号在不在那一包上、套接字写失败滚不滚 persist。

缺跳(layababa):私聊没有通知再拉;空会话写失败没有账本可重试。所有被核过的引擎:套接字写失败都不回滚已 persist 的行。

五、杀掉进程、换设备、多端同时在线

重连不是再拉一次,是一条时序:掉线 → 再鉴权会不会推漏消息 → 发现水位 → 拉 → 换设备分叉 → 多端分叉 → 这条路径上会丢什么。拉到本地游标等于服务端最大,是「一拉」之后必须写明的 hop;没有它,「新设备能追上」不可证伪。各家连接都不自动倾倒未读正文。

出厂关发送的重连:同一客户端号再撞限流,同步什么也没有。这是卡住的出箱,不是漏消息补拉。打开 MQ01 在可见之后、落库之前崩溃:发送方已有序号;扇出可能已发生;历史可能没有;补洞会跳过热链路占用的号。单发同步不能假定补上这个洞。

六、一条群消息的分支

群发送的 H1–H7 与私聊同链,分叉在 H8。出厂拒发仍然是另一条发送:无 persist、无扇出。

发送侧禁言发生在序号之前。layababa 非成员、个人禁言(含主和管理员)、全员禁言且非特权,都会在铸号前拒。OpenIM 超级群跳过成员校验。接收方禁言不是这一跳。

正文只存一份:同品类都是一行。复制若发生,发生在线上。layababa 群可见路径是序号、保存与出箱保证同事务,然后才返回——ack 带序号,扇出未开始。OpenIM 回执无序号。野火确认无序号,之后 N 条收件箱指向同一 mid。

收件人集合:layababa 消费时可见成员减发送方,不读发送快照。普通档推全文;超级群仅当该用户全部活跃设备具备信号能力才信号,否则回退全文。OpenIM 超级群批量推仍是全文。野火统一信号。JuggleIM 拷贝下行全文,多端改通知。

没有一家因接收方禁言而跳过 persist。大群丢掉:layababa 真分叉,一百零一人粘性升级不降级。JuggleIM 最富丢弃树:延迟队列过旧丢、大群关离线不写收件箱、再往上关推送——发送方此时已经拿到成功加序号。

七、崩溃发生在 ack 前 / 后、push 前 / 后

序号一旦分配,合法结局只有三种:复用同一会话与序号、另铸第二条、永久空洞。撤回和已读必须改原序号上的旗标或游标,不得占新号。H4 拒发没有序号,不能拿来和「先可见再落库」比丢不丢。

ack 前、序号已占:Turms 打开序号时自增后插入前崩溃是永久空洞。Tinode 主题序号已加、内存未加,下一条可能补洞或撞唯一。JuggleIM 缓存丢了就再铸。OpenIM 再消费再分配。layababa MQ01 确认未知保持预备,禁新序号。野火此时还没有个人序号,重试新 mid。

persist 后、发送方没看到 ack:Tinode 与 Turms 已有行;客户端超时再发是第二条身份。layababa MQ01 已在可见时 ack——发送方握着一条历史可能没有、补洞会跳过的序号。MQ06 的 ack 在 persist 与出箱保证之后,发送前崩溃由工人同世代重试,世代不匹配变死信。OpenIM、野火、JuggleIM 的 ack 本就是入队或接受,这一跳已经是未建模的未知。

ack 后、push 前:对端靠拉。没有人回滚序号。撤回:layababa 原地标撤回并抬版本,推送带原序号;已读是已读序号的单调更新。OpenIM 在同一聊天序号上墓碑。Tinode 删除藏正文、删除号加一,最后数据号不变。野火把 mid 改类型后再写入收件箱——同一 mid 新的个人序号,是这条维度上的反模式。

八、layababa 出厂关发送 vs 开 MQ:必须分开写

路径 A · 出厂关

私聊热链路不接受、群可见发布者不接受,都走拒发,抛的是限流。之后没有任何服务端身份。客户端把限流映射为五秒退避,不以尝试次数结束意图。重连后再派同一客户端号,再吃同一个码。同步为空。

这不是节流。这是新链路未覆盖、旧流已退役的硬关。用限流发出去,接入方无法区分真限流与链路关闭。

路径 B · 私聊 MQ01

预备占序号 → 发布确认 → 未知则保持预备 → 标可见 → 返回时库可能仍待写。成功回可见;确认未知回 HTTP 200 服务端确认中。客户端再包装成内部错误,分类当可重试的五类。

崩溃边:预备后、可见前 → 同号不得新序号。可见后、落库前 → 发送方已 ack,库可能没有,补洞跳过热占用。

路径 B · 群 MQ06

命中幂等则回收。否则保存(取号加落库)与出箱保证同事务,此时才 ack。到期处理之后才发可见信封。消费时成员集减发送方。普通档全文;超级群信号或回退全文。发送方其他设备不在这个在线集合里。

两条路径唯一允许共用的客户端 hop 是 H1–H3。H4 之后必须分叉写。把 A 的无限五秒重试抄到 B 的确认中上,会把「链路已关」和「结果未知、应对账」搅成同一种内部错误。

九、可偷的跳与追赶顺序

只偷 hop,不偷产品定位。下列是函数级、可证伪的。

现在就该改,因为本仓 hop 自相矛盾。第一,H4 不要再用限流表示链路关闭,应抛专用码,客户端对其终态或停泵。参照拒发与节流分码。第二,未知结果走已有的不可重试码。不要把确认未知写成 HTTP 200;长连接与四版处理必须接住。客户端不要把确认中包装成内部错误再抖动重试。第三,重连必须翻页。批量是否继续翻页恒为假,加上新设备登录只弹提示,新设备与长离线未打开的会话停在一页。可偷 OpenIM 的取最大序号再按会话拉区间,或 Tinode 拉到计数控制帧。最低限度:列表服务端最大远大于已同步时,打开前也要继续翻页。

第二批:MQ01 的 H7 与 H5 对齐,要么像 MQ06 与 Tinode 那样耐久提交后再回可见,要么补洞与同步必须能看见热占用的序号。缺口检测跳变时不要先抬已知最大。私聊 H8 可在多端与未打开会话上复用超级群信号,先写收件箱再通知,写失败落推送、序号已在。

第三批不要偷错。JuggleIM 同号直接成功加旧序号值得要,但必须做成持久唯一;layababa 群指纹已经更强,缺的是 Redis 与库的同一套唯一键。可偷首条全文、多端改通知来限制扇出;不要偷成功加零序号的静默拦截,也不要偷过期扇出直接丢。已读与撤回与聊天序号分家,不要学野火撤回后再写入收件箱。不要偷 OpenIM 的入队 ack:超时同客户端号必双序号。layababa 私聊已经把序号绑在预备上,退回去是退步。

追赶顺序只按谁会在真实失败边上双写或假成功排:先改 H4 码与 H7 未知(否则出厂关和确认未知在客户端是同一种重试),再补重连翻页(否则新设备合同不可验收),再决定 MQ01 是把 ack 挪到 persist 之后还是让同步看见热序号。通知再拉和大群信号是带宽优化,排在合同之后。

同站:音视频 · 运维 · 第一版架构切片存档。本篇由 compare-im-reliability-3 按跳数重跑后收口。

限制。未对 layababa 用 git show origin/dev 复核消息路径。未跑服务、未杀进程。野火、Tinode、JuggleIM 官方客户端多数不在本地树,客户端 hop 不得由服务端反推。