SeiDB 为什么把状态拆成两层:状态承诺与历史存储的分家账 图 1
SeiDB 为什么把状态拆成两层:状态承诺与历史存储的分家账 · 图 1

一条运行多年的链,节点磁盘为什么会越跑越胖?答案通常不在交易本身,而在存储引擎:状态树和它的全部历史版本被塞进同一个通用键值库里,读写放大与元数据膨胀逐年累加。Sei 在 v2 版本引入的 SeiDB 就是冲着这个问题来的。按官方仓库的自述,它的定位是替代 Cosmos 系链默认 IAVL Store 的下一代链上数据库,思路源头是 Cosmos 的 StoreV2 架构提案,核心动作只有一句话:把活跃状态和历史数据拆成两层,各用各的引擎。

状态承诺层:把树搬进内存映射文件

第一层叫状态承诺层(State Commitment,官方文档缩写 SC)。它的职责被仓库文档列得很清楚:为每个新区块给出根应用哈希、给交易执行提供数据访问接口、为状态同步提供导入导出链上状态的 API、为尚未裁剪的高度提供历史证明。说白了,这一层只回答一个问题——当前状态是什么、承诺哈希是多少,历史归档不归它管。

状态分层示意图

实现上,SeiDB 分叉了 Cronos 团队的 MemIAVL 作为这一层:数据结构仍然是 Cosmos SDK 生态熟悉的默克尔化 AVL 树,保证与既有链向后兼容,但存储形态换了——不再把整棵树拆成键值对塞进数据库引擎,而是用内存映射的扁平文件来表示树。官方工程博客把这层的收益说得很直白:验证者节点通过 mmap 访问状态,延迟从数百微秒级降到数百纳秒级,同时大幅改善状态同步时间与读写放大。这一层还是权威承诺的来源:无论下面怎么换后端,应用哈希都以这层为准。

状态存储层:历史归档交给 LSM

第二层叫状态存储层(State Store,SS),专职服务全节点与归档节点的历史查询。它不再存整棵树的节点哈希,而是把带版本号的原始键值对直接写进一个快速嵌入式数据库,官方基准测试显示 PebbleDB 在候选后端里表现最好,因此把它列为推荐默认值,同时也支持切换。

Sei 工程博客回忆了改造前的样子:Sei v1 把带版本的 IAVL 树放在 LevelDB 后端上,为了维护树结构不得不写进大量元数据,schema 不带专业库几乎没法直接解读。SeiDB 改为在 SS 层用最简元数据的纯键值存储,让 LSM 树的局部性真正发挥作用,并把裁剪(pruning)改成了异步执行,节点开裁剪不会再被链甩在后面。官方给出的基准结果是:状态存储体积需求至少降六成,总数据增长率降九成——并且强调节点跑得越久,省盘效果越明显。这组数字属于官方在测试口径下公布的对比结果,不是对任何硬件的承诺。

崩溃之后怎么站起来

分层之后,恢复流程也跟着重排。按官方博客描述:新区块提交时,SeiDB 先从这笔块的交易提取变更集(changeset),应用到内存映射的 IAVL 树上算出区块哈希,随后把变更集异步刷进预写日志(WAL)。验证者不需要服务历史查询,内存树会周期性打快照落盘,WAL 只保存快照点之后的变更。节点崩溃后重启,从最近的快照高度加载,再重放 WAL 里的变更集追平,不需要从零重建。

对普通节点运维者,这套设计的可见结果是三条:磁盘长得慢了;跑历史查询的归档节点和只共识的验证者节点可以用不同的方式配置;以及,改底层引擎是节点本地行为,网络层面完全无感——Sei 后来的 Giga 存储演进文档甚至明确写了,存储分层调整对网络不可见,因为应用哈希的权威来源始终留在状态承诺层。

什么时候值得关心它

SeiDB 适合作为观察所有链存储架构的一个样本:状态树本质是承诺结构,历史查询是归档需求,两者的访问模式南辕北辙,绑在一个通用键值库里是早期工程妥协。评估任何链的节点方案时,都可以照这三个问题问下去:活跃状态与历史数据是不是分开的、树节点是原生存储还是键值转译、崩溃恢复靠全量重放还是快照加变更日志。官方仓库与文档中的分层描述和性能数字都标注了测试口径,具体参数随版本更新,以官方文档为准。

风险提示:本文只解释机制,不构成任何投资建议;性能数字为官方公布的基准口径,实际表现与硬件与负载相关。