跳转至

ADR-0006 服务端实现语言与提交/分发日志选型

状态

已接受,现行决策(初定 2026-08-14,状态更新 2026-09-01):服务端主语言 Rust,提交与分发日志 Redpanda。当前生产装配与门禁均已按此实现;改变语言或 日志介质须新开 ADR,不再沿用“倾向性、可直接推翻”的旧状态。

对应 docs/PLAN.md §7.9、§18.1、§18.2、§19.2.1、§23.0。 本 ADR 补齐了上述三处对「语言选型 ADR」的悬空引用。

背景

CLAUDE.md 已锁定「服务端实现语言最终在 Rust 或 Go 中选择,不会是 Java/JVM」, 并要求「所有机制必须在两个生态下均可实现」。§23.0 进一步规定库级候选清单 不进入 PLAN.md,由本 ADR 承载。

在此之前,PLAN.md 中有三处引用本 ADR 但本 ADR 不存在(§7.9 的 RoaringBitmap portable 格式约束、§18.2 的双生态客户端基线、§23.0 的 Akka 对照小结), 构成全文唯一的悬空引用。

决策

1. 服务端主语言:Rust

全部服务端组件统一 Rust,不做混合语言部署(例如"ConnectionNode 用 Rust、 其余用 Go")。理由是混合会使 §5.1 的实体命名表对应两套实现、两套依赖树、 两套可观测埋点,运维复杂度的增量大于任何局部性能收益。

异步运行时统一 tokio。禁止无充分理由的 unsafe(CLAUDE.md 已有此约束)。

2. 提交日志与分发日志:Redpanda

不使用 Apache Kafka。Redpanda 以 Kafka API 兼容,因此客户端库选型不被锁死, 未来切回 Kafka 无需改应用代码。

3. 库级候选清单

承载的规范性机制 PLAN.md 出处 Rust 选型 备注
分发日志幂等生产与 durable ACK §8、§19.2.1、ADR-0012 rust-rdkafka 可继续使用;事务生产者不是 fencing 前提
ScyllaDB 驱动(阶段一) §18.2 scylla-rust-driver 官方维护,原生 token-aware 与 shard-aware
Redis MailboxStore(一期) §18.1 redis-rs 当前仅 standalone,需 pipeline + Lua;Cluster 未实现且启动拒绝
嵌入式 LSM(阶段二) §18.1 rust-rocksdb C++ 绑定;纯 Rust 的 fjall 可评估但生产验证不足
ShardRegistry 租约 §5.3.7、§19.2 etcd-client 租约 + watch,承载分片归属唯一权威
RoaringBitmap §7.9 roaring-rs 必须使用 portable 序列化格式(§7.9 契约性约束)
对象存储 §18.2、§19.3 aws-sdk-s3 或 object_store 检查点、媒体、冷归档
异步运行时 全文 tokio

4. 历史说明:事务生产者的 C 依赖已不再是 fencing 前提

本 ADR 初稿曾把 Kafka 事务生产者、transactional_id 与 ProducerFenced 视为 §19.2.1 的防脑裂关键路径。ADR-0012(已接受)覆盖该实现选择:

FanoutCoordinator 不使用 Kafka 事务。
正确性顺序 = 全部 GroupDispatch 获得 durable delivery(acks=all)
           -> 才持久化/确认 Outbox source 进度。
逻辑去重 = dispatch_id + MailboxStore 的持久去重;
stale-owner = writer self-fence + ConversationHead 条件更新
              + GroupDispatch 的消费侧 epoch 过滤。

因此,rust-rdkafka 的 C 绑定若继续选用,交叉编译与静态链接成本仍是真实成本;但“不支持 事务生产者”不再是纯 Rust 客户端的自动否决理由,也不得再将 ProducerFenced 当作当前 fencing 前提。候选客户端须改为证明固定分区、幂等生产、acks=all、重放和“dispatch durable 后才确认 source”的故障语义。

命名/配置漂移:当前 RdkafkaCommitLog::publish_dispatches_and_ack_batch 已落实 “全部 dispatch durable 后才 Sync commit source”的 ADR-0012 语义。TransactionalFanout、 FanoutBatchTransaction 与相关 metrics 是历史名称;QIM_FANOUT_TRANSACTIONAL_ID 虽未参与 事务语义仍被强制读取,必须在发布前移除该无用配置门禁。两者不表示 Kafka transaction 仍在使用, 也不改变持续执行故障注入验证的要求。

理由

选 Rust

  1. 无 GC。千万连接目标下 ConnectionNode 的内存占用与尾延迟最可控。Go 的 STW 虽为 亚毫秒级、不威胁 §19.2 的租约参数,但百万连接下的堆扫描与内存放大仍是实际成本。
  2. scylla-rust-driver 是官方驱动且原生 shard-aware;Go 侧需用 scylladb/gocql fork。
  3. 与 Redpanda 运维形态一致:两者都是无 JVM、无 GC 停顿的系统级组件, 容量模型与故障模式更容易统一推理(§23.7 的 failure detector 教训在两侧同构)。
  4. 本设计的热路径(帧编解码、Bitmap 求交、批量写盘)都是 CPU 密集且零拷贝收益明显的形态。

选 Redpanda

  1. 无 JVM、无 ZooKeeper、单二进制,与 CLAUDE.md 的无 JVM 约束方向一致, 运维面显著小于 Kafka。
  2. thread-per-core 架构与 Rust 的 tokio + 分片化设计亲和。
  3. Kafka API 兼容,因此 §6.5 的「逻辑 MailboxShard 与日志分区 1:1 固定映射、 生产者显式指定分区、禁止 key hash 分区器」等约束原样成立,切回 Kafka 无需改代码。

后果

正面

  • 消除三处悬空引用,§18.1 阶段二的自研 LSM 有了明确载体(rust-rocksdb)。
  • 单一语言,§5.1 实体命名表与实现一一对应。
  • 无 GC 使 §15 的心跳参数与 §19.2 的租约参数有更大安全裕度。

负面

  • 若继续采用 rust-rdkafka,仍须承担其 C 绑定的交叉编译与静态链接成本;但该成本不再是 fencing 正确性前提。
  • Rust 的开发速度低于 Go,交付周期需相应放宽;团队招聘面更窄。
  • fjall 等纯 Rust LSM 生产验证不足,阶段二当前只能用 rust-rocksdb(同样是 C++ 绑定)。

替代方案及其否决理由

方案 否决理由
Go 全栈 生态更顺(Pebble 原生、franz-go 纯 Go、无 C 依赖)、开发更快,仍是最强替代;但当前 Rust/Redpanda 已进入实现与验证,切换会成为迁移而非一次无成本复评,必须另开 ADR 并给出兼容/回滚计划
Rust + Go 混合(网关 Rust、其余 Go) 两套实现、两套依赖树、两套埋点,运维复杂度增量大于局部收益
Apache Kafka 替代 Redpanda JVM 运维面、ZooKeeper/KRaft 迁移历史包袱;本设计无任何依赖 Kafka 独有特性之处
纯 Rust 的 rskafka 替代 rust-rdkafka 不再因“不支持事务生产者”否决;但当前尚无其满足固定分区、幂等生产、acks=all、重放与 source checkpoint 故障语义的生产验证
NATS JetStream / Pulsar 替代 Redpanda 分区语义与 offset 稳定性不满足 §6.5 的 mailbox_seq 复合序号要求(低 48 位直接取 log offset)

复评条件

满足下列任一条件时应新开 ADR 复评,而不是直接改写本决策:

  1. 基准测试显示日志客户端的幂等生产 / acks=all 吞吐无法满足 §25 目标档的 dispatch 写入速率;
  2. "全部 dispatch durable 后才确认 source"在 Redpanda 上无法通过故障注入验证;
  3. 团队实际交付速度显示 Rust 的开发成本超出计划 50% 以上;
  4. scylla-rust-driver 或 roaring-rs 出现维护中断;
  5. 附录 B.7 回填后显示 Go 的 GC 开销在实测中不构成瓶颈——此时应重新比较两方案;
  6. 决定引入第二种语言(本 ADR 明令禁止混合部署,改变需重开)。