☰
Polkadot 纠删码基准测试实战:erasure-coding 的 scaling_with_validators 性能剖析
2026/9/29 2:41:32 网站建设 项目流程
  • 区块链

【免费下载链接】polkadot

Polkadot Node Implementation

项目地址:https://gitcode.com/gh_mirrors/po/polkadot
点击查看免费下载

Polkadot 可用性(availability)系统的核心是:每个区块的 PoV(Proof of Validity)被纠删码切成 n 份分片交给 n 个验证者保管,任何 f+1 个分片即可重建完整数据。本指南以仓库 erasure-coding/benches/README.md 为主体,完整讲解如何运行其唯一基准scaling_with_validators,逐行拆解基准源码,并结合 erasure-coding/src/lib.rs 的实现与 availability-recovery 子系统 的真实调用链,带你读懂基准结果(含 10_000 分片反而慢于 50_000 分片这一反直觉现象)背后的算法原理。读完你将能够:独立复现基准、读懂构造(construct)与重构(reconstruct)两条性能曲线,并把基准结论与生产代码中的纠删码调用对应起来。

一、背景:为什么 Polkadot 需要"分片构造 + 重构"基准

在 Polkadot 的可用性体系中,每个区块的AvailableData(包含 PoV 与验证数据)必须被足够多的验证者持有,才能保障后续的争议裁决(disputes)与平行链状态获取。仓库 erasure-coding/src/lib.rs 开头的文档注释给出了设计约定:

  • 数据被纠删码切成 n 份,并构造一棵 Merkle 树,得到统一的erasure_root;
  • 每个验证者只保存属于自己的那一份分片;
  • 假设n = 3f + k(0 < k ≤ 3),f是系统内最多故障验证者数;
  • 任意 f+1 个分片即可重建完整数据。

也就是说,可用性系统运行在一个"编码一次、分片存储、少量分片即可恢复"的不对称模型上。编码与解码(重构)都是计算密集型操作,且会随验证者数量(分片数)变化——这正是scaling_with_validators基准存在的意义:量化分片构造与 PoV 重建在不同验证者规模下的延迟与吞吐,为容量规划和算法优化提供数据。

二、运行基准测试

仓库 erasure-coding/benches/README.md 给出了最直接的两步运行方式:

$ cd erasure-coding # 确保进入 erasure-coding 目录 $ cargo bench

基准基于 Criterion 框架,在 erasure-coding/Cargo.toml 中有完整声明:

[dev-dependencies] criterion = { version = "0.4.0", default-features = false, features = ["cargo_bench_support"] } [[bench]] name = "scaling_with_validators" harness = false
  • harness = false表示不沿用 Rust 默认的#[test]测试入口,而是由 Criterion 的criterion_main!宏接管;
  • 因此cargo bench会进入该文件并执行criterion_main!(re_construct)注册的construct_and_reconstruct_5mb_pov目标。

进阶运行方式

由于本 crate 目前只有一个基准,直接cargo bench即可。若后续加入其他基准,可以精确过滤:

# 只运行本基准 $ cargo bench --bench scaling_with_validators # 只运行 construct 分组(Criterion 支持按分组名过滤) $ cargo bench --bench scaling_with_validators -- construct # 只运行某个参数档位 $ cargo bench --bench scaling_with_validators -- "construct/200"

Criterion 默认会生成target/criterion/目录下的 HTML 报告与回归对比图表,cargo bench运行完毕后可直接打开查看。

三、scaling_with_validators 基准源码逐段拆解

基准文件为 erasure-coding/benches/scaling_with_validators.rs,核心思想是:用一份固定 5 MiB 的 PoV,在 6 档验证者数量下,分别测量"构造分片 + erasure root"与"仅用最少分片数重构 PoV"的性能。

3.1 基准负载与参数档位

fn construct_and_reconstruct_5mb_pov(c: &mut Criterion) { const N_VALIDATORS: [usize; 6] = [200, 500, 1000, 2000, 10_000, 50_000]; const KB: usize = 1024; const MB: usize = 1024 * KB; let pov = vec![0xfe; 5 * MB];
  • PoV 为5 * MB(5 MiB)全0xfe字节流,通过Throughput::Bytes(pov.len() as u64)告诉 Criterion 每次迭代处理 5 MiB,从而同时给出时间与吞吐(MiB/s);
  • 验证者数量覆盖 200 → 50_000 共 6 档,跨度 250 倍,用于观察构造/重构耗时随分片数的增长曲线。

3.2 被测操作封装

fn chunks(n_validators: usize, pov: &Vec<u8>) -> Vec<Vec<u8>> { polkadot_erasure_coding::obtain_chunks(n_validators, pov).unwrap() } fn erasure_root(n_validators: usize, pov: &Vec<u8>) -> Hash { let chunks = chunks(n_validators, pov); polkadot_erasure_coding::branches(&chunks).root() }
  • obtain_chunks(n_validators, pov):对 PoV 做 Reed-Solomon 编码,产出 n 个分片(源码见 erasure-coding/src/lib.rs#L126-L140);
  • branches(&chunks).root():把 n 个分片的哈希插入 Merkle(实际为 Trie)并返回根,即erasure_root(源码见 erasure-coding/src/lib.rs#L254-L274)。

3.3 construct 组:编码 + 求根,并自校验

let mut group = c.benchmark_group("construct"); for n_validators in N_VALIDATORS { let expected_root = erasure_root(n_validators, &pov); group.throughput(Throughput::Bytes(pov.len() as u64)); group.bench_with_input( BenchmarkId::from_parameter(n_validators), &n_validators, |b, &n| { b.iter(|| { let root = erasure_root(n, &pov); assert_eq!(root, expected_root); }); }, ); } group.finish();

要点:

  • 每次迭代执行"编码出 n 个分片 + 建 Trie 求 root"这一完整操作链;
  • 迭代体内的assert_eq!(root, expected_root)相当于把正确性校验放进计时循环:root 必须与基准开始前预计算的期望值一致,确保被测路径没有因优化或实现变更而产出错误结果;
  • BenchmarkId::from_parameter(n_validators)生成construct/200、construct/50000这样的报告名。

3.4 reconstruct 组:用最少分片数重建 PoV

let mut group = c.benchmark_group("reconstruct"); for n_validators in N_VALIDATORS { let all_chunks = chunks(n_validators, &pov); let mut c: Vec<_> = all_chunks.iter().enumerate().map(|(i, c)| (&c[..], i)).collect(); let last_chunks = c.split_off((c.len() - 1) * 2 / 3); group.throughput(Throughput::Bytes(pov.len() as u64)); group.bench_with_input( BenchmarkId::from_parameter(n_validators), &n_validators, |b, &n| { b.iter(|| { let _pov: Vec<u8> = polkadot_erasure_coding::reconstruct(n, last_chunks.clone()).unwrap(); }); }, ); } group.finish();

要点:

  • 先把 n 个分片转成(&[u8], usize)的(分片数据, 分片索引)对(这是reconstruct的输入格式,见 erasure-coding/src/lib.rs#L163-L205);
  • c.split_off((c.len() - 1) * 2 / 3)把约最后 1/3 的分片切到last_chunks。逐档核算:200 档 68 片、500 档 168 片、1000 档 334 片、2000 档 668 片、10000 档 3334 片、50000 档 16668 片——恰好都等于对应recovery_threshold(f+1)再多 1 片;
  • 因此reconstruct 组模拟的是最坏情况:仅凭"刚好达到恢复阈值"的最少分片集重建 5 MiB PoV,这比用更多冗余分片重建更能暴露解码真实开销;
  • reconstruct(n, last_chunks.clone())返回Vec<u8>,基准用unwrap()确认重建成功(若分片不足会返回Error::NotEnoughChunks)。

3.5 Criterion 采样配置

fn criterion_config() -> Criterion { Criterion::default() .sample_size(15) .warm_up_time(Duration::from_millis(200)) .measurement_time(Duration::from_secs(3)) }
  • sample_size(15):每组只采样 15 次,优先控制总时长(5 MiB 级别的编码很耗时);
  • warm_up_time(200ms):极短的预热,适合单次迭代本身就耗时百毫秒级的重负载基准;
  • measurement_time(3s):每个档位测量窗口 3 秒;
  • 结果输出采用 Criterion 的典型格式:time: [best median worst]与对应的thrpt: [worst median best](吞吐区间方向相反,因为时间越小吞吐越大)。

四、基准结果与解读(5950x 实测输出)

erasure-coding/benches/README.md 完整保留了在某台 AMD Ryzen 5950x 机器上运行该基准的输出,原样如下:

construct/200 time: [93.924 ms 94.525 ms 95.214 ms] thrpt: [52.513 MiB/s 52.896 MiB/s 53.234 MiB/s] construct/500 time: [111.25 ms 111.52 ms 111.80 ms] thrpt: [44.721 MiB/s 44.837 MiB/s 44.946 MiB/s] construct/1000 time: [117.37 ms 118.28 ms 119.21 ms] thrpt: [41.941 MiB/s 42.273 MiB/s 42.601 MiB/s] construct/2000 time: [125.05 ms 125.72 ms 126.38 ms] thrpt: [39.564 MiB/s 39.772 MiB/s 39.983 MiB/s] construct/10000 time: [270.46 ms 275.11 ms 279.81 ms] thrpt: [17.869 MiB/s 18.174 MiB/s 18.487 MiB/s] construct/50000 time: [205.86 ms 209.66 ms 213.64 ms] thrpt: [23.404 MiB/s 23.848 MiB/s 24.288 MiB/s] reconstruct/200 time: [180.73 ms 184.09 ms 187.73 ms] thrpt: [26.634 MiB/s 27.160 MiB/s 27.666 MiB/s] reconstruct/500 time: [195.59 ms 198.58 ms 201.76 ms] thrpt: [24.781 MiB/s 25.179 MiB/s 25.564 MiB/s] reconstruct/1000 time: [207.92 ms 211.57 ms 215.57 ms] thrpt: [23.195 MiB/s 23.633 MiB/s 24.048 MiB/s] reconstruct/2000 time: [218.59 ms 223.68 ms 229.18 ms] thrpt: [21.817 MiB/s 22.354 MiB/s 22.874 MiB/s] reconstruct/10000 time: [496.35 ms 505.17 ms 515.42 ms] thrpt: [9.7008 MiB/s 9.8977 MiB/s 10.074 MiB/s] reconstruct/50000 time: [276.56 ms 277.53 ms 278.58 ms] thrpt: [17.948 MiB/s 18.016 MiB/s 18.079 MiB/s]

用中位数整理成表(时间为中位数,吞吐按 5 MiB / 中位时间换算):

分片/验证者数construct 中位耗时construct 吞吐reconstruct 中位耗时reconstruct 吞吐
20094.525 ms52.896 MiB/s184.09 ms27.160 MiB/s
500111.52 ms44.837 MiB/s198.58 ms25.179 MiB/s
1000118.28 ms42.273 MiB/s211.57 ms23.633 MiB/s
2000125.72 ms39.772 MiB/s223.68 ms22.354 MiB/s
10000275.11 ms18.174 MiB/s505.17 ms9.8977 MiB/s
50000209.66 ms23.848 MiB/s277.53 ms18.016 MiB/s

4.1 三个可观察的结论

  1. 构造比重构快:各档位下construct耗时约为reconstruct的一半。这与算法特性相符——编码(生成冗余分片)比解码(从残缺分片恢复)在 Reed-Solomon 中开销更低,且重构组用的是"刚好达到阈值"的最少分片集,属于最坏路径。
  2. 规模从 200 增长到 2000,耗时增长平缓:构造约 94→126 ms(+33%)、重构约 184→224 ms(+22%),吞吐仅小幅下降。说明该区间内分片数增加带来的线性成本被 5 MiB 数据的固有编解码成本摊薄。
  3. 10_000 分片反而比 50_000 分片更慢(README 明确指出):构造 275 ms vs 210 ms,重构 505 ms vs 278 ms。README 特意点出 "with10_000chunks (validators) its slower than with50_000for both construction and reconstruction",这是该基准最有意思的现象。从实现看,recovery_threshold与 novelpoly 的CodeParams::derive_parameters会随 n、k 选择不同的编解码参数(如多项式次数与矩阵布局),10_000 档位很可能落在一个对 5 MiB 负载不利的参数组合上。需要说明的是:该现象仅来自本次 5950x 实测,属于基准报告事实;其是否可复现、是否为算法固有的参数敏感性问题,需要更多机器的数据与对 novelpoly 内部实现的深入分析才能下结论,不建议直接当作普适结论引用。

五、底层原理:从基准到 erasure-coding 实现

基准调用的四个 API 全部来自 erasure-coding/src/lib.rs,逐一对应实现:

5.1 恢复阈值 recovery_threshold

pub const fn recovery_threshold(n_validators: usize) -> Result<usize, Error> { if n_validators > MAX_VALIDATORS { return Err(Error::TooManyValidators) } if n_validators <= 1 { return Err(Error::NotEnoughValidators) } let needed = n_validators.saturating_sub(1) / 3; Ok(needed + 1) }
  • 对应文档注释的n = 3f + k:(n-1)/3 + 1即 f+1,是重建所需的最少分片数;
  • MAX_VALIDATORS = novelpoly::f2e16::FIELD_SIZE = 65536——编码基于 GF(2^16) 有限域,分片数上限 65536,测试field_order_is_right_size专门断言该值。

5.2 编码:obtain_chunks

pub fn obtain_chunks<T: Encode>(n_validators: usize, data: &T) -> Result<Vec<Vec<u8>>, Error> { let params = code_params(n_validators)?; let encoded = data.encode(); if encoded.is_empty() { return Err(Error::BadPayload) } let shards = params .make_encoder() .encode::<WrappedShard>(&encoded[..]) .expect("Payload non-empty, shard sizes are uniform, and validator numbers checked; qed"); Ok(shards.into_iter().map(|w: WrappedShard| w.into_inner()).collect()) }

流程:code_params依据n_validators与恢复阈值推导 Reed-Solomon 参数(CodeParams::derive_parameters),对 SCALE 编码后的数据做编码,输出均匀分片。

5.3 求根与逐验证者证明:branches

pub fn branches<'a, I: 'a>(chunks: &'a [I]) -> Branches<'a, I> { // 构造 trie:把每个 chunk 的索引映射到其 Blake2 哈希 // ... trie.insert(encoded_index, chunk_hash.as_ref()) ... Branches { trie_storage, root, chunks, current_pos: 0 } }

实现把分片索引(u32编码)映射到BlakeTwo256::hash(chunk)构造一棵 Trie,Branches迭代器逐个产出(MerkleProof, chunk),每个验证者各得一份与自己的分片对应的 Merkle 证明;branch_hash则可独立验证某个索引处的分支证明是否与 root 匹配(erasure-coding/src/lib.rs#L278-L295)。这就是基准中erasure_root(n, pov)的完整链路。

5.4 解码:reconstruct

reconstruct先校验输入(分片索引越界、分片长度必须为偶数且一致、非空),再把缺失位置补None交给params.make_encoder().reconstruct(received_shards),最终对恢复出的字节做 SCALE 解码。错误映射清晰:NeedMoreShards→NotEnoughChunks、长度不一致 →NonUniformChunks等。重构输入必须携带索引正是基准里构造(chunk_data, idx)元组的原因。

六、基准之外的实战印证:纠删码在节点中的真实调用链

scaling_with_validators测的不是孤立函数,而是生产路径的真实热点。在 node/network/availability-recovery/src/lib.rs(Availability Recovery 子系统)中:

  • 恢复任务的阈值直接取自同一 API:threshold: recovery_threshold(session_info.validators.len())?;
  • 收到分片后先用branch_hash(&params.erasure_root, chunk.proof(), chunk.index.0 as usize)验证 Merkle 证明,再比对BlakeTwo256::hash(&chunk.chunk)(is_chunk_valid),防止恶意分片;
  • 昂贵计算被封装为ErasureTask枚举,放到阻塞线程执行:Reconstruct(n_validators, chunks, tx)内部调用reconstruct_v1,Reencode(n_validators, root, data, tx)内部调用obtain_chunks_v1+branches;
  • 重构完成后还要求重编码并比对 root(reconstructed_data_matches_root),以确认整份数据没被篡改——这与 construct 基准里"迭代内assert_eq!(root, expected_root)"的自校验思路完全一致。

也就是说,基准的每个被测片段都能在生产代码里找到对应位置:分片请求(reconstruct 路径)与数据校验(erasure root 比对)正是节点每天在处理的操作。

七、配套测试与模糊测试:正确性兜底

性能基准的前提是"路径本身正确",仓库为此提供了两层保障:

  • 单元测试(erasure-coding/src/lib.rs#L347-L418):round_trip_works用 10 个验证者切分、任意取 4 片重建并断言与原数据相等;roundtrip_proof_encoding对 2..16 档规模验证证明的编解码往返与branch_hash结果;
  • 模糊测试(erasure-coding/fuzzer/src/round_trip.rs 与 erasure-coding/fuzzer/src/reconstruct.rs):前者用 honggfuzz 随机喂入 PoV 数据做 10 验证者 4 片重建往返;后者随机组合(验证者数, 分片集)直接轰炸reconstruct_v1,验证任何输入都不 panic、只返回确定性的错误或成功。

八、复现与注意事项

  • 运行环境:cargo bench需要本仓库能正常cargo build(依赖 novelpoly、substrate 的 sp-core/sp-trie 等);README 中的数值来自 AMD 5950x 单机实测,不同 CPU、内存频率、系统负载下数值会明显不同,基准输出不构成任何性能承诺;
  • 解读口径:时间区间取中位数,吞吐按 5 MiB/次迭代计算;10_000 慢于 50_000 的现象建议结合CodeParams::derive_parameters的参数选择逻辑做进一步分析后再引用;
  • 延伸阅读:想看分片如何被分发、存储与验证,可继续阅读 erasure-coding/src/lib.rs 全量实现、availability-recovery 子系统 以及 av-store 存储子系统;想自己跑模糊测试,可参照 erasure-coding/fuzzer 的Cargo.toml配置。
  • 区块链

【免费下载链接】polkadot

Polkadot Node Implementation

项目地址:https://gitcode.com/gh_mirrors/po/polkadot
点击查看免费下载

相关推荐

上一篇:快速解决Expo EAS构建失败:react-native-image-picker原生依赖集成终极指南
下一篇:校园小情书性能优化指南:提升小程序响应速度与用户体验

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询