- 区块链
【免费下载链接】polkadot
Polkadot Node Implementation
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 = falseharness = 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 吞吐 |
|---|---|---|---|---|
| 200 | 94.525 ms | 52.896 MiB/s | 184.09 ms | 27.160 MiB/s |
| 500 | 111.52 ms | 44.837 MiB/s | 198.58 ms | 25.179 MiB/s |
| 1000 | 118.28 ms | 42.273 MiB/s | 211.57 ms | 23.633 MiB/s |
| 2000 | 125.72 ms | 39.772 MiB/s | 223.68 ms | 22.354 MiB/s |
| 10000 | 275.11 ms | 18.174 MiB/s | 505.17 ms | 9.8977 MiB/s |
| 50000 | 209.66 ms | 23.848 MiB/s | 277.53 ms | 18.016 MiB/s |
4.1 三个可观察的结论
- 构造比重构快:各档位下
construct耗时约为reconstruct的一半。这与算法特性相符——编码(生成冗余分片)比解码(从残缺分片恢复)在 Reed-Solomon 中开销更低,且重构组用的是"刚好达到阈值"的最少分片集,属于最坏路径。 - 规模从 200 增长到 2000,耗时增长平缓:构造约 94→126 ms(+33%)、重构约 184→224 ms(+22%),吞吐仅小幅下降。说明该区间内分片数增加带来的线性成本被 5 MiB 数据的固有编解码成本摊薄。
- 10_000 分片反而比 50_000 分片更慢(README 明确指出):构造 275 ms vs 210 ms,重构 505 ms vs 278 ms。README 特意点出 "with
10_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(¶ms.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
相关推荐
MinIO 纠删码(Erasure Coding)深度解析:原理、部署实践与 Bit Rot 防护
MinIO 纠删码(Erasure Coding)深度解析:原理、部署实践与 Bit Rot 防护 MinIO 使用纠删码与校验和双重机制保护数据,使其在硬件故
后端存储对象存储分布式存储云原生RustFS 纠删码(Erasure Coding)规范解析:算法、xl.meta 磁盘格式与兼容性契约
RustFS 纠删码(Erasure Coding)规范解析:算法、xl.meta 磁盘格式与兼容性契约 导读 本文以 RustFS 仓库中具有规范(norma
后端对象存储分布式存储突破存储性能极限:RustFS纠删码基准测试全解析
突破存储性能极限:RustFS纠删码基准测试全解析 引言:分布式存储的性能瓶颈 在分布式对象存储系统中,纠删码(Erasure Coding,EC)技术是保障数
后端对象存储分布式存储
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考