Solana 长期 RPC 交易历史:基于 BigTable 的六个月交易数据存储方案设计与实现
【免费下载链接】solanaWeb-Scale Blockchain for fast, secure, scalable, decentralized apps and marketplaces.项目地址: https://gitcode.com/GitHub_Trending/so/solana
本篇指南解读 Solana 仓库中的长期 RPC 交易历史方案(docs/src/implemented-proposals/rpc-transaction-history.md):它说明了为什么验证节点本地 RocksDB 账本无法支撑六个月以上的交易历史查询、为什么选择 Google BigTable 作为外部存储、四张核心数据表(block/tx-by-addr/tx/entries)的行键设计与 GC 策略,以及通过solana-ledger-tool完成数据灌入、通过 tonic/gRPC 访问层的完整实现链路。读完本文,你能理解该方案的表结构、数据写入与查询路径,并能在本地模拟器或生产环境中部署验证。
一、问题背景:为什么需要外部数据存储
RPC 节点需要对外提供至少 6 个月的交易历史服务,而验证节点本地 RocksDB 账本实际只能保留数天的历史,对下游用户明显不足。同时,6 个月的交易数据量在物理上无法合理地塞进验证节点本地的 RocksDB 账本中,因此必须引入一个外部数据存储。
方案的核心定位是:验证节点本地 RocksDB 账本继续作为主数据源,查询时先查本地,本地没有再回落到外部数据存储(fall back to the external data store)。
受此方案影响的 RPC 端点共 7 个:
getFirstAvailableBlockgetConfirmedBlockgetConfirmedBlocksgetConfirmedSignaturesForAddressgetConfirmedTransactiongetSignatureStatusesgetBlockTime
方案提出时给出了五条系统级设计约束(这也是后续所有技术选型的依据):
- 数据规模可达 TB 级且不可变(immutable)——存储与检索的数据量会迅速膨胀到太字节级别,且写入后不再修改;
- 对 SRE 运维负担要尽量轻——例如需要 SRE 持续监控、再平衡节点的 SQL 数据库集群是明确不期望的方案;
- 数据必须支持实时检索——以分钟或小时计的批量查询是不可接受的;
- 易于在全球复制——便于把数据部署到各区域的 RPC 端点附近,降低访问延迟;
- 与外部存储的对接必须简单可靠——不能依赖风险较高、使用率低的社区支持库。
基于以上约束,方案最终选择了 Google 的 BigTable 产品作为数据存储。
二、Table Schema:四张表的行键设计与数据生命周期
一个 BigTable 实例承载全部交易数据,按查询路径拆分成不同的表以支持快速检索:
- 新数据可以随时拷贝进实例而不影响已有数据,所有数据不可变;
- 一般约定是每个 epoch 完成后上传一次当前 epoch 的数据,但对数据 dump 的频率没有硬性限制;
- 旧数据的清理通过配置实例表的数据保留策略(GC policy)自动完成,数据到期后直接消失,无需维护清理任务。
由于清理是自动按时间过期完成的,数据写入的顺序变得重要:如果 epoch N-1 的数据在 epoch N 的数据之后才写入,较旧的 epoch 数据反而会存活得比新数据更久。文档指出这种乱序删除除了会在查询结果中产生"空洞"(holes)外,不会产生其他负面影响。另外,这种基于保留期的清理方式实际上允许存储无限量的交易数据,唯一的限制是金钱成本。
需要说明的是:该表布局只为现有的 RPC 端点服务;未来新增 RPC 端点可能需要扩充 schema,甚至需要遍历所有交易来构建必要的元数据。
2.1 实例初始化:四张表与保留策略
仓库中的 init-bigtable.sh 脚本定义了实例solana-ledger中预期的四张表,脚本对每张表执行建表、建列族、设 GC 策略三步(见 init-bigtable.sh):
instance=solana-ledger for table in blocks entries tx tx-by-addr; do ( set -x "${cbt[@]}" createtable $table "${cbt[@]}" createfamily $table x "${cbt[@]}" setgcpolicy $table x maxversions=1 "${cbt[@]}" setgcpolicy $table x maxage=360d ) done要点:
- 表名共四张:
blocks、entries、tx、tx-by-addr,分别对应提案文档中的 Block / Entries / Transaction Signature Lookup / Account Address Transaction Signature Lookup 四类数据; - 每张表只有一个列族
x; - GC 策略为
maxversions=1(每个 cell 只保留最新版本)加maxage=360d(数据保留 360 天后自动过期),这正是"六个月交易历史"目标在基础设施层面的落地方式。
2.2 Block 表(block):按 slot 检索整块
该表保存给定 slot 的压缩后的块数据。
- 行键:取 slot 的 16 位小写十六进制表示,保证在按行键列表检索时,最早的已确认块的 slot 一定排在最前面。例如 slot 42 的行键是
000000000000002a; - 行数据:压缩后的
StoredConfirmedBlock结构体。
StoredConfirmedBlock的内容结构与 confirmed_block.proto 中的ConfirmedBlock消息一一对应:前一个块哈希previous_blockhash、块哈希blockhash、父 slotparent_slot、交易列表transactions、奖励rewards、块时间block_time与块高度block_height。其中每笔交易的元数据(TransactionStatusMeta,见 confirmed_block.proto)包含费用、前后余额、内部指令、日志、token 余额变化、reward、return_data,以及自 v1.10.35 / v1.11.6 起可用的compute_units_consumed字段——这保证了长期历史数据同样能提供完整的交易状态明细。
2.3 地址交易签名索引表(tx-by-addr):按地址倒序取最新交易
该表保存影响某个给定地址的所有交易,服务于getConfirmedSignaturesForAddress这类"按地址查签名列表"的端点。
- 行键:
<base58 address>/<slot 取反码的十六进制 slot(前补 0 到 16 位)>; - 行数据:压缩后的
TransactionByAddrInfo结构体。
对 slot 取一补码(one's compliment)的效果是:按行键顺序列出时,影响该地址的最新 slot 的交易永远排在最前——这与 Block 表用原始 slot 保证"最旧在前"正好互为镜像,两张表分别优化了"从最旧开始扫"和"从最新开始扫"两种遍历方向。
行数据结构见 transaction_by_addr.proto:每条TransactionByAddrInfo包含签名signature、交易错误err、交易在块内索引index、可选memo和块时间block_time。其中的TransactionError枚举完整覆盖了交易级错误码(如ALREADY_PROCESSED、BLOCKHASH_NOT_FOUND、SIGNATURE_FAILURE等,见 transaction_by_addr.proto)以及指令级错误码,保证历史错误交易也能被完整还原。
文档还特别指出:Sysvar 地址不建索引;但 Vote、System 这类高频程序会被索引,它们几乎在每个已确认 slot 都有行——这正是 TB 级数据量来源之一。
2.4 交易签名索引表(tx):签名到块与索引的映射
该表把交易签名映射到它所在的已确认块以及块内索引,服务于getSignatureStatuses、getConfirmedTransaction等按签名查询的端点。
- 行键:base58 编码的交易签名;
- 行数据:压缩后的
TransactionInfo结构体。
2.5 Entries 表(entries):slot 内 entry 摘要
entries表的支持自 v1.18.0 起加入。
该表保存 slot 内各 entry 的摘要数据,服务于getConfirmedBlock的jsonWithEntries编码。
- 行键:与
block表的行键相同; - 行数据:压缩后的
Entries结构体,即一个 entry 摘要列表。
其 schema 与 entries.proto 对应:每个Entry包含index、自上一个 entry 以来的哈希数num_hashes、entry 哈希hash、交易数num_transactions以及起始交易索引starting_transaction_index。
三、访问层:tonic + 原始 protobuf 的 gRPC 客户端
文档在 "Accessing BigTable" 一节明确:BigTable 提供 gRPC 端点,在 Rust 侧使用tonic加原始 protobuf API 访问——因为当时不存在更高层的 BigTable Rust crate。这意味着解析 BigTable 查询结果更复杂一些,但不是大问题。
仓库中 storage-bigtable/src/bigtable.rs 就是这一访问层的实现:
连接建立(BigTableConnection::new):
- 设置
BIGTABLE_EMULATOR_HOST时直连本地模拟器(http://明文通道,project 固定为emulator,见 new_for_emulator); - 否则走
https://bigtable.googleapis.com,TLS 根证书从 crate 内置的 pki-goog-roots.pem 加载,并支持可配置的连接超时; - 表前缀统一为
projects/{project}/instances/{instance}/tables/; - 通过
read_only参数决定申请bigtable.data还是bigtable.data.readonlyOAuth scope,只读 RPC 节点只申请只读权限; - 支持
BIGTABLE_PROXY环境变量为 gRPC 流量配置正向代理(隧道内仍走 TLS,见 bigtable.rs 与 storage-bigtable/README)。
- 设置
单元格读写:写入时数据会先压缩再落盘。put_bincode_cells 把 serde 结构体 bincode 序列化后经
compress_best压缩,以列名bin写入列族x;put_protobuf_cells 则把 prost 消息编码后以列名proto写入。读取侧的 deserialize_protobuf_or_bincode_cell_data 会先尝试按 protobuf 解码,失败再回退 bincode,使同一张表能兼容新旧两种序列化格式——这对滚动升级期间新旧客户端混跑是必要的。范围查询与过滤:get_row_keys 与 get_row_data 通过
ReadRows请求配合行范围(StartKeyClosed/EndKeyClosed)、rows_limit和链式行过滤器实现:CellsPerRowLimitFilter(1)只取每行最少单元格、CellsPerColumnLimitFilter(1)只取每个 cell 的最新版本、StripValueTransformer(true)在只列举行键时直接剥离单元格值以减少传输量。这些过滤器正是"实时检索"约束在查询路径上的体现。可靠性:所有高层操作(如 put_bincode_cells_with_retry)都包了指数退避重试,且把 "table not found" 这类 gRPC NotFound 识别为不可重试的永久错误(to_backoff_err)。
四、数据灌入:solana-ledger-tool 的 BigTable 子命令
文档规定:"实例数据的持续灌入将按epoch 节奏进行,通过一个新的solana-ledger-tool命令,把给定 slot 范围内的 RocksDB 数据转换成实例 schema";同样的流程还会手动运行一次以回填(backfill)现有账本数据。
该命令在仓库中落地为 ledger-tool/src/bigtable.rs 的bigtable子命令模块,核心操作包括:
upload(upload):
- 起始 slot 未指定时取
blockstore.get_first_available_block(),结束 slot 未指定时取blockstore.max_root(),即默认回填"本地账本内全部可用根上块"; - 上传并非一次性提交整个 slot 范围,而是以
max_num_slots_to_check * 2为步长分段推进(starting_slot逐段向后滚动直到ending_slot),避免长时间占用单个上传任务; - 底层调用
solana_ledger::bigtable_upload::upload_confirmed_blocks,配合ConfirmedBlockUploadConfig完成块数据到 BigTable 的写入; - 提供
--force-reupload参数(见 bigtable.rs)强制重传,用于覆盖/修复历史上传。
- 起始 slot 未指定时取
first-available-block(first_available_block):查询外部存储中最早的可用块,用于运维侧验证数据下限与
getFirstAvailableBlock端点行为;block(block):按 slot 从 BigTable 拉取
ConfirmedBlock,以 Base64 编码输出,支持--show-entries时同时从entries表读取 entry 摘要一并展示——这正是 RPC 端getConfirmedBlock数据路径的人工验证入口;delete-slots(delete_slots):删除指定 slot 的数据,在只读配置(
read_only,即 dry-run)下仅演练不落盘,与 GC 自动过期形成互补的手工修正手段。
五、开发与生产环境的部署方式
storage-bigtable/README 给出了完整的两类环境操作方式:
开发/测试环境(BigTable 模拟器)
- 后台运行
gcloud beta emulators bigtable start启动模拟器; - 运行
$(gcloud beta emulators bigtable env-init)建立BIGTABLE_EMULATOR_HOST环境变量; - 运行 init-bigtable.sh 在模拟器上建好四张表;
- 开始开发/测试——访问层代码检测到
BIGTABLE_EMULATOR_HOST后会自动切换到模拟器连接(bigtable.rs),init-bigtable.sh也会自动改用emulatorproject(init-bigtable.sh)。
生产环境
- 将标准的
GOOGLE_APPLICATION_CREDENTIALS环境变量指向服务账号凭据; - 项目中应包含名为
solana-ledger、并已用init-bigtable.sh初始化过表结构的 BigTable 实例; - 根据操作模式申请
bigtable.data(读写,用于灌入)或bigtable.data.readonly(只读,用于 RPC 查询)scope; - 如需走正向代理,按
HTTP_PROXY的方式导出BIGTABLE_PROXY即可。
六、方案要点小结
从提案文档到仓库实现,该方案的关键设计可以概括为:
- 主从数据源:本地 RocksDB 账本为主,BigTable 为六个月历史的回落层,两者分工明确;
- 四表按查询路径切分:
block(slot 正序行键,最旧在前)、tx-by-addr(slot 补码行键,最新在前)、tx(签名为行键)、entries(slot 内 entry 摘要),每张表都直接对齐一个 RPC 端点的扫描模式,利用 BigTable 行键有序性免去二级索引; - 不可变 + 自动过期:数据只增不改,GC 策略(
maxversions=1,maxage=360d)自动回收,运维面接近零负担,理论存储上限只受成本约束; - 轻量对接:tonic + 原始 protobuf 直连 gRPC,序列化上 bincode/protobuf 双格式兼容,配合压缩与指数退避重试保证读写稳健;
- epoch 节奏灌入:
solana-ledger-tool bigtable的 upload 子命令按 epoch 持续增量上传、可一次性 backfill,并配套 first-available-block / block / delete-slots 等运维子命令形成完整的数据生命周期闭环。
相关源码与文档入口:
- 提案文档:docs/src/implemented-proposals/rpc-transaction-history.md
- 访问层实现:storage-bigtable/src/bigtable.rs
- 实例初始化脚本:storage-bigtable/init-bigtable.sh
- 存储 proto 定义:storage-proto/proto/confirmed_block.proto、storage-proto/proto/transaction_by_addr.proto、storage-proto/proto/entries.proto
- 数据灌入命令:ledger-tool/src/bigtable.rs
- 部署说明:storage-bigtable/README.md
【免费下载链接】solanaWeb-Scale Blockchain for fast, secure, scalable, decentralized apps and marketplaces.项目地址: https://gitcode.com/GitHub_Trending/so/solana
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考