跳转至

ADR-0017:FanoutCoordinator 的 dispatch 生产者关闭幂等

  • 状态:已接受(2026-09-26,由项目负责人决定;实测数据见“验证”)
  • 相关:ADR-0012(Fanout 不用 Kafka 事务)、ADR-0016(Outbox 分区与冻结快照缓存)、 docs/PLAN.md §6.5、§10.1.2

背景

2026-09-26 在本机以 15,000 msg/s 压测时,FanoutCoordinator 成为第一个瓶颈:每批 “读 Outbox → 规划 → 写出 dispatch 并等待持久确认 → 同步提交源位点”串行执行, 每批 30.5 ms,其中等待 dispatch 确认 27.4 ms、提交位点 1.8 ms。

拆分计时与 broker 自报指标排除了 Redpanda:

测量 结果
Redpanda 每个 produce 请求的处理时延(broker 自报) 平均 0.25 ms,99.2% < 2 ms
Redpanda 4 核/4G → 8 核/8G 等确认 27.4 → 29.3 ms,无改善;Redpanda 只用约 1 核
同时段 writer 单条 Outbox 发布 平均 4.8 ms

原因在客户端:librdkafka 按分区各发一个 produce 请求,一批约 520 条 dispatch 落在 64 个 dispatch 分区上即约 64 个请求;enable.idempotence=true 时每条 broker 连接最多 5 个请求在途,于是一批需要约 13 轮往返。broker 侧 produce 请求约 2,048/s,与 “30 批/s × 64 + writer 的 Outbox 请求”吻合。

决策

dispatch 生产者默认 enable.idempotence=false(acks=all 不变),由 QIM_FANOUT_PRODUCER_IDEMPOTENCE(仅接受 true/false,默认 false)控制,可随时 改回。writer 的 Outbox 生产者保持幂等:它只写一个分区,不受在途上限制约。

为什么正确性不依赖幂等

幂等从来不是 dispatch 的逻辑唯一性来源(ADR-0012、docs/PLAN.md §10.1.2):同一 dispatch 在故障切换或重放时本就会以不同 offset 追加多次,MailboxStore 以持久的 dispatch_id + payload_digest 收敛,重复记录复用首次 mailbox_seq,水位仍按观察到的 offset 连续推进。

关闭幂等只新增两种、仅在 produce 请求失败重试时出现的现象:

  1. 重复记录:与既有重放语义相同,由上述去重吸收。
  2. 同一分区内两条不同 dispatch 的相对顺序对调:mailbox_seq 由 offset 构造, 受影响用户邮箱中两条消息的 mailbox_seq 先后可能与 conversation_seq 先后相反。 会话内顺序、历史分页、未读定义式与会话投影(ZADD GT)都以 conversation_seq / created_at 为准,连续水位按 offset 推进,均不受影响;设备游标仍只由 MAILBOX_BATCH 连续推进。

“fanout 先取得整批 durable delivery、再提交源位点”的顺序约束(ADR-0012)不变, 因此不会丢消息。

后果

  • 预期每批等待由约 13 轮往返降为约 1~2 轮;实测见下。
  • 重试时的重复与分区内对调属于已接受行为,不设恒零指标;若将来有依赖 “邮箱内 mailbox_seq 先后与 conversation_seq 先后一致”的功能,必须先改回幂等 或另立 ADR。
  • 这是解除客户端在途上限的最小改动,不替代 ADR-0016 决策 2 的 Outbox 分区扩展。

验证

2026-09-26 同机、同配置(15,000 msg/s × 60 s、8+8 Redis、批上限 512、服务直连 Redis):

指标 开幂等 关幂等
每批等 dispatch 确认 27.4 ms 25.3 ms(−8%)
每批 dispatch 数 523 486
提交源位点 1.8 ms 1.8 ms

背景一节“5 在途 → 约 13 轮往返”的解释被实测否定:若它是主因,关闭幂等后应降到 数毫秒。收益仅约 2 ms/批。更符合数据但尚未证实的模型是 64 个分区请求全走同一条连接、 被依次处理,叠加 librdkafka 默认 linger.ms=5 与本机 CPU 饱和下的唤醒排队;下一步的 决定性验证是生产者按分区拆成多条连接,或把 fanout 生产者 linger.ms 调到 0。

决定保持默认关闭(项目负责人判断重试故障率低、语义代价可接受);若后续验证表明 收益可由多连接或 linger 调整单独取得,应重新评估是否改回幂等。