aptos-core replay-benchmark 实战指南:历史交易回放、状态覆盖与执行性能基准测试
【免费下载链接】aptos-coreAptos is a layer 1 blockchain built to support the widespread use of blockchain through better technology and user experience.项目地址: https://gitcode.com/GitHub_Trending/ap/aptos-core
在 Aptos 核心节点仓库中,aptos-move/replay-benchmark模块提供了一个名为aptos-replay-benchmark的命令行工具,用于下载链上真实的历史交易、初始化执行输入状态、在修改后的状态上重放比较执行输出,并对执行时间进行基准测量。阅读本篇指南后,你将掌握该工具download/initialize/diff/benchmark四个子命令的完整用法与参数细节,并理解其底层的读集(read-set)捕获机制、状态覆盖原理以及 Block-STM 并行执行的测量方式,从而能够对新的功能开关、Gas 版本或 Move 代码变更在历史负载上做出性能与行为评估。
工具概览与模块结构
该工具的定义见 Cargo.toml:包名aptos-replay-benchmark,版本0.1.0,描述为 “A tool to replay and locally benchmark on-chain transactions.”。它复用了仓库内的多个核心组件,主要依赖包括:
aptos-block-executor:提供 Block-STM 并行执行引擎;aptos-vm:Aptos VM 与AptosVMBlockExecutor;aptos-move-debugger(aptos_move_debugger):基于 REST 客户端的链上调试/查询能力,用于拉取交易与状态;aptos-rest-client:fullnode REST API 客户端;aptos-gas-schedule:Gas 费用表与LATEST_GAS_FEATURE_VERSION。
入口程序位于 main.rs,用 clap 定义了四个子命令并分发到对应的实现:
pub enum Command { Download(DownloadCommand), Initialize(InitializeCommand), Diff(DiffCommand), Benchmark(BenchmarkCommand), }四个子命令的完整定义分别位于 commands/download.rs、commands/initialize.rs、commands/diff.rs 和 commands/benchmark.rs。其中download与initialize共享--rest-endpoint E(fullnode 的 REST API 查询端点)与可选的--api-key K两个参数,其结构体定义在 commands/mod.rs。build_debugger函数会用这些参数构造AptosDebugger(一个基于 REST 客户端的查询器),API key 的作用是提升 HTTP 请求的速率限制配额。
常见的 REST 端点示例:
- devnet:
https://api.devnet.aptoslabs.com/v1 - testnet:
https://api.testnet.aptoslabs.com/v1 - mainnet:
https://api.mainnet.aptoslabs.com/v1
命令一:download —— 下载历史交易
download命令用于从链上按版本区间下载交易并保存到本地文件。参数说明:
| 参数 | 含义 |
|---|---|
--begin-version B | 版本区间起点(含) |
--end-version E | 版本区间终点(不含,半开区间[B, E)) |
--transactions-file T | 交易保存的本地文件路径 |
--rest-endpoint E | fullnode REST API 端点 |
--api-key K | 可选,提升请求配额 |
示例(摘自 README):
aptos-replay-benchmark download \ --begin-version 2232125001 \ --end-version 2232125093 \ --rest-endpoint https://api.mainnet.aptoslabs.com/v1 \ --transactions-file transactions.file成功后输出:
Got 93/93 txns from RestApi. Downloaded 12 blocks with 93 transactions in total: versions [2232125001, 2232125093)为什么必须是整块区间?下载的后续基准测试是按“块”为单位、由执行器逐个块执行的,因此要求指定区间必须恰好覆盖若干完整的块,不能只下载某块中的前几笔交易。从源码可以看到这一约束的两道校验(download.rs):
- 区间第一笔交易必须是块起点:
txn.is_block_start()不成立则报 “First transaction … must be a block start”; - 区间最后一笔(
end_version - 1)之后必须紧跟块起点,否则报 “All transactions in the block must be selected”。
此外,版本参数本身也要满足begin_version < end_version(download.rs)。
下载完成后,交易会被partition函数按块切分为TransactionBlock(每个块记录begin_version、交易列表及其辅助信息),并用 BCS 序列化写入文件(download.rs)。块切分逻辑有单元测试覆盖,例如 download.rs 中的test_block_partition_1/2/3验证了以BlockMetadata交易为界正确切分块、并正确累计begin_version的行为。
命令二:initialize —— 初始化基准测试的输入状态
要对历史交易做基准测试,还需要准备每个块的“执行前状态”。initialize命令负责下载并生成这些输入状态。参数:
| 参数 | 含义 |
|---|---|
--transactions-file T | download生成的交易文件 |
--inputs-file I | 输入状态保存的文件 |
--rest-endpoint E | 必须与 download 使用同一网络的端点 |
--api-key K | 可选,提升请求配额 |
--log-level L | 日志级别,默认Error |
示例:
aptos-replay-benchmark initialize \ --rest-endpoint https://api.mainnet.aptoslabs.com/v1 \ --transactions-file transactions.file \ --inputs-file baseline-state.file输出逐块报告进度:
Generated inputs for block 1/12 in 8s Generated inputs for block 2/12 in 9s ... Generated inputs for block 12/12 in 25s读集捕获原理。输入状态为“每个块”生成一份:当每个块被执行时,它都运行在这份预计算的状态之上,因此不存在块执行结果的 “commit”。具体实现在 generator.rs:InputOutputDiffGenerator::generate为每个块派生一个阻塞任务并行生成输入(块间相互独立),单块内部的交易执行是顺序的。generate_inputs(generator.rs)的流程是:
- 通过
debugger.state_view_at_version(begin_version)获取块执行前的链上状态视图; - 应用状态覆盖(如有,见下一节),先用原始状态执行一遍并检查链上输出是否会写被覆盖的键(若写入被覆盖的状态,则基准结果可能失真,会打
error!日志); - 再在带覆盖的状态视图上执行一遍,用
ReadSetCapturingStateView记录所有读取到的StateKey -> StateValue,最终沉淀为ReadSet。
ReadSet与捕获视图定义在 state_view.rs:ReadSet是一个HashMap<StateKey, StateValue>,实现了TStateView接口,命中键返回ColdOccupied槽位、未命中返回ColdVacant;get_usage在基准场景下不可调用(unreachable!)。捕获视图ReadSetCapturingStateView在首次访问某个键时把状态值记入读集;若 REST 拉取失败则直接 panic——注释中解释了原因:读集一旦缺读,基准结果就不再正确。另外,视图初始化时会预加载aptos_cached_packages::head_release_bundle()中的框架模块,以避免并行执行的投机读取在 VM 序言中找不到0x1::error等基础模块。
关于 HTTP 429 限流。状态初始化通过执行交易来捕获每个块的读集,读多时可能触发 REST API 的速率限制,表现为大量HTTP error 429 Too Many Requests的错误日志(如Failed to fetch state value for StateKey::AccessPath { ... })甚至线程 panic(Failed to fetch state value ... receiving on a closed channel)。解决办法是在 Aptos Build 中创建 API key 来提升配额,然后给工具加--api-key K参数。
覆盖状态:在历史负载上试验新特性
initialize命令支持四类状态覆盖(参数定义见 initialize.rs):
- 强制启用功能开关:
--enable-features F1 F2 ...; - 强制禁用功能开关:
--disable-features F1 F2 ...; - 强制覆盖 Gas 特性版本:
--gas-feature-version V; - 覆盖现有链上包:
--override-packages P1 P2 P3,参数为 Move 包的源码目录路径。
功能开关需使用大写名称,例如ENABLE_LOADER_V2;完整的功能开关列表定义在 aptos_features.rs。启用/禁用两个列表不能重叠(overrides.rs 会显式校验);--gas-feature-version若大于LATEST_GAS_FEATURE_VERSION会给出警告(overrides.rs),且源码注释说明目前只支持带 feature version 的 V2 Gas 费用表。
覆盖的底层实现集中在 overrides.rs。OverrideConfig::get_state_override返回一个HashMap<StateKey, StateValue>作为状态视图的覆盖层:
- 功能开关:读取链上
Features配置资源,逐条enable/disable后重新序列化,替换对应状态键的值;对已经处于目标状态的开关会打error!日志; - Gas 版本:读取链上
GasScheduleV2配置,直接修改feature_version字段后写回; - 包覆盖:以
BuildOptions::move_2()编译本地包,按包地址找到链上PackageRegistry资源并替换/追加对应包的元数据,再把每个模块的字节码序列化为StateKey::module(...)的覆盖值;同一模块被重复覆盖会 panic。
典型场景:如果有一个新特性(或新版 Move 代码)能让 MoveVM 更快,把它覆盖到历史交易的执行状态上,就能在真实历史负载上观察执行性能与 Gas 使用的变化。
示例——在 baseline 之外额外启用ENABLE_CALL_TREE_AND_INSTRUCTION_VM_CACHE开关,生成一份实验状态:
aptos-replay-benchmark initialize \ --rest-endpoint https://api.mainnet.aptoslabs.com/v1 \ --transactions-file transactions.file --enable-features ENABLE_CALL_TREE_AND_INSTRUCTION_VM_CACHE \ --inputs-file experiment-state.file命令三:diff —— 比较两种状态下的执行输出
覆盖状态可能改变执行行为。diff命令把同一批交易分别在两个输入状态上执行,并比较输出。参数:
| 参数 | 含义 / 默认值 |
|---|---|
--transactions-file T | 交易文件 |
--inputs-file I1 | 第一份输入状态文件 |
--other-inputs-file I2 | 第二份输入状态文件 |
--concurrency-level L | 计算 diff 时 Block-STM 执行交易的线程数,默认1(顺序执行) |
--allow-different-gas-usage | 为true时,Gas 相关差异不计入比较 |
diff 的实现要点(commands/diff.rs):
- 对两份输入状态分别调用
compute_outputs,各自用AptosVMBlockExecutor::new_with_local_config(local_config(concurrency_level))执行所有块; - 逐块打印 CSV 形式的 Gas 汇总:
block, {I1} (gas), {I2} (gas); - 对每笔交易用
TransactionDiffBuilder(allow_different_gas_usage决定是否忽略 Gas 差异)构造 diff,非空的以Non-empty output diff for transaction {version}:开头逐条打印。
比较维度由 diff.rs 的Diff枚举界定,分为四类:GasUsed(Gas 用量)、ExecutionStatus(执行状态,重放时应保持一致)、Event(事件)、WriteSet(写集,按StateKey比较WriteOp)。打印输出采用<<<<<<< BEFORE/========/>>>>>>> AFTER的三行块格式,并用彩色高亮区分前后值。
示例:
aptos-replay-benchmark diff \ --transactions-file transactions.file \ --inputs-file baseline.state \ --other-inputs-file experiment-state.file \ --allow-different-gas-usage此时控制台以 CSV 形式打印两个状态的每块 Gas 用量:
block, baseline.state (gas), experiment.state (gas) 1, 35, 35 2, 26, 26 ... 11, 622, 622 12, 2076, 2071理想情况下差异应当很小,说明覆盖没有改变历史交易的行为。如果覆盖让交易变便宜,所有交易行为一致,输出差异通常只体现在:Gas 用量、交易费用事件(FeeStatement)、代币总供应量(费用被烧毁)、以及费用支付者余额。若不提供--allow-different-gas-usage,diff 还会把每处差异完整打印出来,例如:
Non-empty output diff for transaction 2232125041: <<<<<<< BEFORE gas_used: 59 ======== gas_used: 58 >>>>>>> AFTER <<<<<<< BEFORE event "0000000000000000000000000000000000000000000000000000000000000001::transaction_fee::FeeStatement" data: [59, 0, 0, 0, 0, 0, 0, 0, 53, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] ======== event "0000000000000000000000000000000000000000000000000000000000000001::transaction_fee::FeeStatement" data: [58, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] >>>>>>> AFTER <<<<<<< BEFORE write StateKey::AccessPath { address: 0x68c7..., path: "Resource(0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>)" } op Modification(5ca5adc5..., metadata:StateValueMetadata { ... }) ======== write StateKey::AccessPath { address: 0x68c7..., path: "Resource(0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>)" } op Modification(c0a5adc5..., metadata:StateValueMetadata { ... }) >>>>>>> AFTER <<<<<<< BEFORE write StateKey::TableItem { handle: 1b854694..., key: 0619dc29... } op Modification(aa8dd92120d693010000000000000000, metadata:StateValueMetadata { inner: None }) ======== write StateKey::TableItem { handle: 1b854694..., key: 0619dc29... } op Modification(0e8ed92120d693010000000000000000, metadata:StateValueMetadata { inner: None }) >>>>>>> AFTER Non-empty output diff for transaction 2232125042: ...上例中唯一差异是 Gas 相关(覆盖让块执行变便宜),配合--allow-different-gas-usage后即可确认行为等价。
命令四:benchmark —— 执行测量与统计
benchmark命令在保存的状态上执行保存的交易并测量时间。参数(定义见 commands/benchmark.rs):
| 参数 | 含义 / 默认值 |
|---|---|
--transactions-file T | 交易文件 |
--inputs-file I | 每个块的输入状态文件 |
--num-blocks-to-skip N | 前 N 个块不计入测量(但仍会执行,用作 warm-up),默认0 |
--concurrency-levels L1 L2 ... | Block-STM 执行单个块的线程数列表,必填且至少一个 |
--num-repeats N | 每个并发级别重复执行的次数,默认3,最少3 |
--measure-overall-time | true时测量所有块的总时间,否则逐块测量 |
--disable-paranoid-mode | true时 Move VM 不做运行时类型检查,可能更快 |
--async-runtime-checks | true时 Move VM 追踪执行、Block-STM 事后校验 |
--log-level L | 日志级别,默认Error |
测量机制的关键约束([commands/benchmark.rs](https://link.gitcode.com/i/a2bf5f6195cd8569190588a4d0e573ac#L19-L20, L88-L118)):
MIN_NUM_REPEATS常量固定为 3,重复次数小于 3 直接报错 “Number of repeats must be at least 3”;- 并发级别列表不能为空,否则报 “At least one concurrency level must be provided”;
- 交易块数量与输入状态数量必须一致;
--num-blocks-to-skip不能大于等于总块数。
从源码结构看,每个并发级别下每一轮 repeat 都会新建一个AptosVMBlockExecutor(runner.rs),逐块执行并用Instant::now()计时(微秒)。逐块模式下,仅对idx >= num_blocks_to_skip的块输出统计:对每块N次计时排序后,打印median (us), mean (us), min (us), max (us)四列,CSV 表头为concurrency level, block, median (us), mean (us), min (us), max (us);总时间模式则先执行前num_blocks_to_skip个块作 warm-up,再对剩余块整体计时(runner.rs)。
并发级别指定 Block-STM 执行一个块所用的线程数,通常应接近 CPU 核心数;提供多个级别时,工具会为每个级别分别报告测量值,便于观察并行度对执行时间的影响。
执行引擎配置。无论 benchmark 还是 diff,执行器都使用同一份本地配置(execution.rs):
BlockExecutorLocalConfig { blockstm_v2: true, // 使用 Block-STM v2 concurrency_level, // 由命令行传入 allow_fallback: true, // 允许回退 discard_failed_blocks: false, module_cache_config: ..., enable_pre_write: true, // 启用预写 }执行时块执行配置使用BlockExecutorConfigFromOnchain::on_but_large_for_test()(无块限制),且execute_workload断言块执行不应失败——因为重放的是已经成功上链的历史交易。
测量口径说明。两点值得注意:一是基准测试中没有块执行输出的 “commit”(状态每次都从预计算读集读取);二是签名验证在执行之前完成,不计入报告时间。另外--disable-paranoid-mode通过set_paranoid_type_checks(!disable_paranoid_mode)关闭 Move VM 的运行时类型检查(commands/benchmark.rs),可换取更快的执行速度。
示例:基线对比实验
同一批交易(ENABLE_CALL_TREE_AND_INSTRUCTION_VM_CACHE关闭):
aptos-replay-benchmark benchmark \ --transactions-file transactions.file \ --inputs-file baseline-state.file \ --num-blocks-to-skip 2 \ --concurrency-levels 4 \ --num-repeats 31打印 10 个块的测量结果(跳过了前 2 个块):
concurrency level, block, median (us), mean (us), min (us), max (us) 4, 2, 10701, 11137.74, 10170, 22922 4, 3, 11678, 11949.84, 11459, 20218 4, 4, 5341, 5348.23, 5164, 5616 4, 5, 53871, 54126.52, 53237, 58044 4, 6, 16334, 16314.32, 15856, 16596 4, 7, 7845, 7844.77, 7634, 8032 4, 8, 13140, 13113.45, 12854, 13521 4, 9, 127062, 117830.45, 70342, 169842 4, 10, 6305, 6343.45, 5860, 7042 4, 11, 51917, 51963.16, 51508, 53646同一交易在启用ENABLE_CALL_TREE_AND_INSTRUCTION_VM_CACHE的状态(experiment-state.file)上重放:
aptos-replay-benchmark benchmark \ --transactions-file transactions.file \ --inputs-file experiment-state.file \ --num-blocks-to-skip 2 \ --concurrency-levels 4 \ --num-repeats 31可以看到部分块出现了加速(如第 5 块中位时间从 53871us 降到 44129us,第 11 块从 51917us 降到 42064us):
concurrency level, block, median (us), mean (us), min (us), max (us) 4, 2, 11102, 12927.03, 10705, 42065 4, 3, 11733, 12226.06, 11494, 20036 4, 4, 5400, 5476.61, 5259, 6343 4, 5, 44129, 45534.39, 43544, 61366 4, 6, 16373, 16173.23, 10992, 23235 4, 7, 8086, 9900.55, 7799, 35909 4, 8, 12986, 13318.58, 9773, 25228 4, 9, 127551, 124062.03, 71639, 229356 4, 10, 6468, 6828.61, 5964, 11862 4, 11, 42064, 43327.74, 41656, 68311完整工作流小结
将四个命令串联起来,一次完整的“历史负载回放 + 实验对比”流程是:
download:选定覆盖整块的版本区间,从目标网络拉取交易到transactions.file;initialize:用同一网络端点生成 baseline 状态baseline-state.file;如需实验,再带--enable-features/--disable-features/--gas-feature-version/--override-packages生成experiment-state.file;diff:比较两份状态的执行输出,用--allow-different-gas-usage过滤纯 Gas 差异,确认覆盖没有改变交易行为;benchmark:分别对两份状态测量执行时间,通过--concurrency-levels观察并行度影响,通过--num-blocks-to-skip消除冷启动噪声,通过--num-repeats(≥3)取得中位数/均值/最小/最大时间。
需要牢记的适用前提与限制:版本区间必须对齐整块;initialize与download必须使用同一网络的 REST 端点;基准测量不含块提交成本,签名验证也在计时之外,因此测得的是纯执行侧耗时;若覆盖的状态键恰好被历史交易写入,生成器会打错误日志提示基准结果可能失真。基于源码中的 TODO(initialize.rs),Gas 费用表覆盖与不同切块策略的实验支持仍在规划中。掌握以上用法与底层机制后,你即可在aptos-core内对任何影响执行路径的改动(功能开关、Gas 版本、包字节码)建立可复现的历史负载性能基线。
【免费下载链接】aptos-coreAptos is a layer 1 blockchain built to support the widespread use of blockchain through better technology and user experience.项目地址: https://gitcode.com/GitHub_Trending/ap/aptos-core
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考