加入 Hanzo 测试,任意付费方案立减 50%

在同一进程里终结 Raft:用无领导者的后量子共识 Lux Quasar 换下 ZooKeeper 的共识引擎

本文说明 Hanzo Datastore 如何把 ClickHouse Keeper 底下的 NuRaft 引擎换成 Lux Quasar。换上的是后量子、没有单一领导者的共识,直接链进同一套 C++ 服务器,复制层继续调用原来的 ZooKeeper 接口,调用方式保持不变。

本页内容

Hanzo Datastore 今天协调复制表的方式与 ClickHouse 相同:一个内嵌的、兼容 ZooKeeper 的 Keeper,在服务器进程里跑 Raft。它能工作。它也是这套栈里最后一块古典的、依赖领导者的、量子时代之前的部件。本文说明我们如何终结它,以及为什么“终结 ZK”并不意味着重写复制。

把 Keeper 拆开:接口与引擎

一听到“替换 ZooKeeper”,本能是想象要去碰每一个 Replicated*MergeTree。不必。ClickHouse Keeper 本来就是两件可以分开的东西,编进了同一个守护进程:

  1. ZooKeeper 接口 — 一棵可线性化的键值树,带顺序节点、版本和监视。源码里它是 KeeperStorage,由 KeeperStateMachine 包着(src/Coordination/KeeperStateMachine.h,class IKeeperStateMachine : public nuraft::state_machine)。这是契约。每张复制表、每一次 system.zookeeper 查询、每一次领导者选举,说的都是它。

  2. 共识引擎 — 接收一串写请求,让节点法定人数就其顺序达成一致,再按该顺序在状态机上调用 commit(log_idx, data)。今天这是 NuRaft:KeeperServer::launchRaftServer(src/Coordination/KeeperServer.cpp)构造出 nuraft::raft_server、asio_service、KeeperStateManager 和 KeeperLogStore。

干净的观察,也就是 Rich Hickey 的那个观察:KeeperStateMachine 已经对着通用共识接口来写(nuraft::state_machine:pre_commit、commit、rollback,以及快照)。它不知道也不在乎底下是 Raft。引擎是可以换掉的值,不是焊死在接口上的位置。

所以:留下状态机,换掉引擎。 复制所依赖的 ZooKeeper 接口原样保留。死去的是 NuRaft、领导者选举,以及必须另跑一套 Raft 法定人数的要求。

用什么替换

引擎变成 Quasar,来自 Lux 的无领导者、基于有向无环图、后量子的共识。Quasar 在这里的工作与 Raft 相同:就日志顺序达成一致,并在一条记录成为最终结果时发出信号。不同的是达成一致的方式:没有单一领导者需要故障转移,用 ML-DSA 签名代替经典签名,用置信阈值的最终性代替固定任期里的多数。

关键在于,Quasar 不是我们用外壳调出去的 Go 服务。它交付的是带 C++ 可用二进制接口的原生 C 引擎(libluxconsensus)。Datastore 服务器是 C++。于是引擎链进同一进程:没有边车,没有到协调器的网络一跳,运行时也没有第二种语言。

接缝已经证明,对着真正的状态机

这不是草图,也不再是模型。我们写了一个程序,链接真正的 ClickHouse 服务器库(libdbms.a),驱动生产用的 KeeperStateMachine<KeeperMemoryStorage>,也就是 ReplicatedMergeTree 用来协调的那个类,全程跑在 Quasar 上,排序路径里没有 NuRaft。它位于 Datastore 树的 src/Coordination/examples/keeper_quasar_poc.cpp,并在真实的 Linux 主机上编译运行(aarch64,clang-21)。

这条流程忠实于分发器今天驱动 NuRaft 的方式:两个阶段,请求被追加时是 preprocess,共识将其顺序定稿时是 commit:

state_machine->preprocess(rfs, /*lock_mutex*/ true);     // append-time, pre-consensus (≙ nuraft pre_commit)
auto buf = IKeeperStateMachine::getZooKeeperLogEntry(rfs); // the real ZooKeeper log entry
lux_chain_add_block(chain, &blk);   // the entry rides Quasar (the slot raft_server occupies)
lux_consensus_process_vote(...);    // validators agree — no leader
lux_consensus_is_accepted(chain, id, &accepted);
state_machine->commit(height, *buf); // finality → the real KeeperStorage mutates, in order

拿十个请求的 ReplicatedMergeTree 协调序列来跑,包括副本注册、块号递增、领导者选举。它预处理每个请求,经 Quasar 排序,定稿,提交到真正的 KeeperStorage,并断言得到的树完全正确,每条按共识顺序各应用一次:

== REAL Keeper state machine reached consensus on Quasar — zero Raft, zero ZK ensemble ==

其下是最小的 C 二进制接口演示(pkg/c/examples/cpp_consumer.cpp,make test-cpp),链接 libluxconsensus 并定稿单个块:同一台引擎,不依赖 ClickHouse。两者都可复现。Datastore 的例子是可选的(-DENABLE_EXAMPLES=1 -DLUXCONSENSUS_DIR=...),从不进入普通构建。

按一对一映射到 Keeper:

今天的 Keeper / NuRaftQuasar 原生
nuraft::raft_server 为日志排序lux_chain_t 为日志排序
KeeperLogStore / ChangelogQuasar 的有向无环图日志
Raft 领导者选举无领导者的置信投票
state_machine->commit(idx, data)同一次调用,在 is_accepted 时触发
经典签名,固定任期的领导者ML-DSA 签名,没有领导者
为了运转而单独准备的 ZK/Keeper 法定人数进程内、服务器内的共识

KeeperStateMachine::commit 是接合点。今天由 NuRaft 调用,明天由 Quasar 驱动调用同一个方法。状态机分辨不出差别,而这正是它属于替换、不属于重写的性质。

为什么这比“后量子”更要紧

  • 没有领导者。 Raft 每个任期选出一个领导者,它一失败,写入就停到重新选举。Quasar 的无领导者协议去掉了这种队首依赖。
  • 不必再跑第二套法定人数。 协调不再是你要单独供应、监视和备份的系统。它就是这台服务器。
  • 构造上就是后量子。 保护协议的签名是 ML-DSA,不是经典曲线,未来的量子对手无法伪造共识消息。
  • 传输只有一个故事。 它与其余方向合在一起:现在是原生 TCP/HTTP,下一步是 ZAP,以及各条链正在使用的同一套 Lux 原语。

如实的现状

上面的架构是真的,证明现在跑在生产用的 KeeperStateMachine 上,不是模型,并在真实 Linux 主机上构建通过。剩下的是引擎内部的替换:在 KeeperServer 内部、与 NuRaft 并排放入 Quasar 共识后端,把它设为默认,再把 KeeperServer 还在做的事,快照、恢复、会话过期、重新配置,在整套 Coordination 测试变绿的前提下搬上去。这是真工作。我们不会在证明提交路径的那天告诉你 Raft 已经消失。我们告诉你的是,接缝对着真实类型站得住,接合点是 KeeperStateMachine::commit,这就是全部关键。

它落地时,contrib/NuRaft/ 和 Raft 引擎离开这棵树。只要复制还在说 ZooKeeper 接口,接口就留下。这就是不碰任何一个 MergeTree、却终结最后一块前量子部件的办法。

评论

使用 Hanzo ID 登录后即可评论。

正在加载评论…

继续阅读