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
- 无 GC。千万连接目标下 ConnectionNode 的内存占用与尾延迟最可控。Go 的 STW 虽为 亚毫秒级、不威胁 §19.2 的租约参数,但百万连接下的堆扫描与内存放大仍是实际成本。
scylla-rust-driver是官方驱动且原生 shard-aware;Go 侧需用scylladb/gocqlfork。- 与 Redpanda 运维形态一致:两者都是无 JVM、无 GC 停顿的系统级组件, 容量模型与故障模式更容易统一推理(§23.7 的 failure detector 教训在两侧同构)。
- 本设计的热路径(帧编解码、Bitmap 求交、批量写盘)都是 CPU 密集且零拷贝收益明显的形态。
选 Redpanda
- 无 JVM、无 ZooKeeper、单二进制,与
CLAUDE.md的无 JVM 约束方向一致, 运维面显著小于 Kafka。 - thread-per-core 架构与 Rust 的 tokio + 分片化设计亲和。
- 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 复评,而不是直接改写本决策:
- 基准测试显示日志客户端的幂等生产 /
acks=all吞吐无法满足 §25 目标档的 dispatch 写入速率; - "全部 dispatch durable 后才确认 source"在 Redpanda 上无法通过故障注入验证;
- 团队实际交付速度显示 Rust 的开发成本超出计划 50% 以上;
scylla-rust-driver或roaring-rs出现维护中断;- 附录 B.7 回填后显示 Go 的 GC 开销在实测中不构成瓶颈——此时应重新比较两方案;
- 决定引入第二种语言(本 ADR 明令禁止混合部署,改变需重开)。