赶紧趁有时间将之前的坑填完前面几章把 Besu 的部署、组件、API 都聊了一遍那接下来必然绕不开一个问题我这套链究竟能跑多快说实话这个问题没有一个固定的答案。每次我的回答都一样——“得测”。不是敷衍是因为区块链的性能跟共识算法、出块间隔、节点数量、网络延迟、合约复杂度全都有关系任何一个变量变了结论都可能不一样。所以与其拍脑袋不如老老实实搭一套基准测试。本次测试用的是 Hyperledger Caliper。下面把整个过程的思路、配置、翻车经历和结果都摊开来讲。一、为什么是 Caliper在做性能测试选型的时候我大概对比了以下几种方案自己写脚本压测最灵活但统计维度要自己补。TPS、延迟分布、成功率这些都得手算而且多轮测试的编排很麻烦。做一次两次还行长期维护不划算。用 k6 / JMeter 模拟 RPC 调用以前做过对 HTTP 压测很成熟但区块链的 benchmark 不完全等于 HTTP benchmark。比如需要区分读写操作、需要控制交易发送速率和确认策略、需要知道交易提交了和交易上链了之间的延迟差。这些通用工具做不到。CaliperHyperledger 旗下的专用区块链性能测试框架。它原生支持 Ethereum 的适配器以前版本内置了 Manager-Worker 架构、多轮测试编排、速率控制、延迟统计和 HTML 报告生成。最关键的是——它对 Besu 的 WebSocket 连接和合约部署流程有原生支持不用自己写适配层。选 Caliper 还有一个很现实的原因它跑在 Docker 里不污染我的主机环境。测试完docker-compose down一把清干净干净利落。当前测试我用的版本是hyperledger/caliper:0.6.0因为最新的版本已经不支持以太坊系列的区块链了。二、Caliper 的 Manager-Worker 模型在深入配置之前我觉得有必要先解释一下 Caliper 的工作原理因为后面分析结果时会反复提到这些概念。Caliper 的测试流程由两个角色协作完成Manager管理器负责读取配置、部署合约、编排测试轮次、收集结果、生成报告。它是整个测试的大脑。Worker工作节点负责实际发送交易。多个 Worker 可以并行工作每个 Worker 独立维护自己的 nonce 计数器。一次完整的 benchmark 流程分五个阶段init 阶段初始化连接验证网络配置。install 阶段部署智能合约到目标链上。test 阶段按轮次执行压测。每一轮可以有独立的速率控制、交易数量和 workload 模块。report 阶段汇总所有轮次的数据生成 HTML 报告。cleanup 阶段清理资源。Caliper 支持两种 Worker 模式local本地进程和 remote远程进程。我这次用的是 local 模式也就是 Manager 在自己的容器里 fork 出子进程当 Worker。对于单节点压测来说完全够用。如果是分布式压测比如同时对多个 RPC 节点打流量才需要用到 remote 模式配合消息队列。还有一个很重要的概念是SUTSystem Under Test。在 Caliper 里SUT 就是要测的目标系统。对于我来说SUT 就是 Besu 网络。Caliper 通过CALIPER_BIND_SUTethereum:latest来绑定对应的适配器。这个适配器内置了 Web3.js 的连接逻辑、nonce 管理、交易签名和回执等待。三、我的测试环境先交代一下测试拓扑Besu 网络3 个 QBFT 验证节点 1 个 RPC 节点部署在 4 台物理服务器上。出块参数blockperiodseconds: 55 秒一个块gasLimit: 0x989680约 1000 万 gas。Gas 策略min-gas-price: 0联盟链内部零 gas这是前面第 2 章部署时定的。共识QBFT即时最终性不会分叉。Caliper 测试机是一台独立服务器通过 Docker Compose 启动。目录结构长这样tests/caliper/ ├── benchmarks/ │ └── benchmark-config.yaml # 测试基准配置 ├── contracts/ │ ├── SimpleStorage.sol # 测试用的 Solidity 合约 │ └── SimpleStorage.json # 编译后的 ABI Bytecode ├── networks/ │ ├── networkconfig-node1.json # 节点 1 的网络配置 │ └── networkconfig-node2.json # 节点 2 的网络配置 ├── workloads/ │ └── simpleStorage.js # 工作负载模块 ├── scripts/ │ ├── init.sh # 初始化脚本 │ ├── run-test.sh # 测试主控脚本 │ └── verify-setup.sh # 配置验证脚本 ├── docker-compose.yml └── .env下面逐一过一下关键配置。3.1 智能合约SimpleStorage这个合约简单到不能再简单——就是一个set/get的存储合约专门给性能测试用的contract SimpleStorage { uint256 private storedData; event ValueSet(uint256 newValue, address indexed setter); function set(uint256 x) public { storedData x; emit ValueSet(x, msg.sender); } function get() public view returns (uint256) { return storedData; } }写操作set会修改链上状态、触发事件所以需要消耗 gas 并等待区块确认。读操作get是view函数不修改状态因此可以直接从节点本地状态读取几乎零延迟。3.2 网络配置networkconfig以节点 1 的配置为例{caliper:{blockchain:ethereum},ethereum:{url:ws://10.8.161.41:8546,contractDeployerAddress:0x9400509cbbebd2b17020d1ec494b3085bc47e9c9,contractDeployerAddressPrivateKey:0x...,fromAddress:0x9400509cbbebd2b17020d1ec494b3085bc47e9c9,fromAddressPrivateKey:0x...,transactionConfirmationBlocks:1,timeout:60000,gas:500000,gasPrice:0,contracts:{SimpleStorage:{path:./contracts/SimpleStorage.json,gas:{set:200000,get:100000},estimateGas:false}}}}这里有几点值得展开说连接走 WebSocketws://前面第 6 章说过HTTP 不支持eth_subscribe。而 Caliper 需要订阅新区块事件来追踪交易确认所以必须走 WebSocket。端口是 8546不是 8545。transactionConfirmationBlocks: 1这意味着 Caliper 会等交易被打包进 1 个区块后才认为确认成功。对 QBFT 来说1 个区块就够了因为它是即时最终性的不存在重组风险。gasPrice: 0因为我们配了min-gas-price0所以这里填 0。如果是公网或非零 gas 的联盟链这里要填实际值。estimateGas: false我是手动指定 gas limit 的set用 20 万get用 10 万不依赖节点估算。这样避免eth_estimateGas调用成为性能瓶颈。私钥明文写在配置里这里必须强调——这是测试环境。Calper 的网络配置会把私钥明文存储。生产环境绝对不要这么干。如果是在生产做压测建议用专门生成的一次性测试账户测完就废弃。3.3 基准配置benchmark-config.yamltest:name:Besu QBFT Performance Testworkers:type:localnumber:1rounds:-label:write-operationstxNumber:6000rateControl:type:fixed-rateopts:tps:50workload:module:./workloads/simpleStorage.jsarguments:contractId:SimpleStoragemethod:set-label:read-operationstxNumber:6000rateControl:type:fixed-rateopts:tps:200workload:module:./workloads/simpleStorage.jsarguments:contractId:SimpleStoragemethod:get这个配置定义了两轮测试第一轮6000 笔写操作set以固定速率 50 TPS 发送。第二轮6000 笔读操作get以固定速率 200 TPS 发送。这里有个细节workers.number: 1。这表示只有 1 个 Manager 进程Master但 Caliper 0.6.0 在 local 模式下会自动 fork 出 2 个 Worker 子进程来并行发交易。从后面的运行日志也能看到Launching worker 1 of 2和Launching worker 2 of 2。3.4 工作负载模块simpleStorage.jsconst{WorkloadModuleBase}require(hyperledger/caliper-core);classSimpleStorageWorkloadextendsWorkloadModuleBase{asyncinitializeWorkloadModule(workerIndex,totalWorkers,roundIndex,roundArguments,sutAdapter,sutContext){awaitsuper.initializeWorkloadModule(...);this.contractIdroundArguments.contractId;this.methodroundArguments.method||set;}asyncsubmitTransaction(){constvalueMath.floor(Math.random()*1000000);if(this.methodset){constrequest{contract:this.contractId,verb:set,args:[value],readOnly:false};awaitthis.sutAdapter.sendRequests(request);}else{constrequest{contract:this.contractId,verb:get,args:[],readOnly:true};awaitthis.sutAdapter.sendRequests(request);}}}每次submitTransaction被调用时会生成一个随机值0 ~ 999999然后根据method决定调用合约的set或get方法。注意readOnly: true/false这个标记是告诉 Caliper 的适配器读操作不需要等待区块确认可以直接返回。这个标记是性能差异的核心原因之一。3.5 Docker Compose 编排caliper-manager-1:image:hyperledger/caliper:0.6.0environment:-CALIPER_BIND_SUTethereum:latest-CALIPER_BENCHCONFIGbenchmarks/benchmark-config.yaml-CALIPER_NETWORKCONFIGnetworks/networkconfig-node1.jsonvolumes:-./networks:/hyperledger/caliper/workspace/networks:ro-./benchmarks:/hyperledger/caliper/workspace/benchmarks:ro-./workloads:/hyperledger/caliper/workspace/workloads:ro-./contracts:/hyperledger/caliper/workspace/contracts:ro-./reports:/hyperledger/caliper/workspace/reportscommand:launch managerextra_hosts:-host.docker.internal:host-gatewayprofiles:-node1这里有几个要点配置文件networks、benchmarks、workloads、contracts全部以只读模式:ro挂载。这是避免容器内的误操作污染宿主机文件的好习惯。reports目录是可写的因为测试报告要写出来。extra_hosts配置了host.docker.internal映射让容器能通过host.docker.internal访问宿主机的网络。如果 Besu 节点部署在 Docker 网络外的物理机上像我这样这个配置不是必需的但加上也没坏处。profiles: [node1]表示这个服务只在指定 profile 时才启动。这样我一个docker-compose.yml就能管多个节点的压测通过--profile切换目标。配套的 Shell 脚本就不逐行解释了核心逻辑就是docker-compose --profile node1 up --abort-on-container-exit启动压测、容器退出后自动停止。verify-setup.sh用来跑前检查所有必需文件是否存在init.sh负责拉镜像和省去手工创建.env的麻烦。四、第一次测试——翻车现场事不宜迟马上来跑第一次。这次的配置是写操作 50 TPS6000 笔。这是我当时的测试配置上面 3.3 里贴的那份。跑起来之后前 60 秒日志看着挺正常[write-operations Round 0 Transaction Info] - Submitted: 18 Succ: 0 Fail:0 Unfinished:18 [write-operations Round 0 Transaction Info] - Submitted: 44 Succ: 0 Fail:0 Unfinished:44但到了第 12 分钟的时候日志开始炸了(ノД)ノerror [caliper] [ethereum-connector] Failed tx on SimpleStorage; calling method: set; nonce: 0xe Error: Transaction was not mined within 50 blocks, please make sure your transaction was properly sent. Be aware that it might still be mined!然后是一连串的非同 nonce 报错0xd、0x24……最终结果惨不忍睹| Name | Succ | Fail | Send Rate (TPS) | Max Latency (s) | Min Latency (s) | Avg Latency (s) | Throughput (TPS) | |------------------|------|------|-----------------|-----------------|-----------------|-----------------|------------------| | write-operations | 25 | 25 | 5.2 | 13.08 | 3.37 | 8.22 | 0.1 |6000 笔的目标实际提交了 50 笔成功了 25 笔失败 25 笔。实际吞吐量只有 0.1 TPS。问题出在哪我捋了捋50 TPS 的发送速率超过了 QBFT 的处理能力。我的出块间隔是 5 秒每个块有 gas 上限。一笔set操作我就配置了 20 万 gas每块 1000 万 gas 上限理论上一个块最多塞 50 笔。但问题是…这些交易是通过 WebSocket 发的Caliper 会等上一个交易确认了才发下一个吗不会。Caliper 是按速率持续发射的不管前面有没有确认。这就导致交易池被瞬间堆满后续交易因为 nonce 冲突或者 gas 不够直接被节点拒绝或者永远排不上队 o(╥﹏╥)o。50 块的超时窗口不够。Calper 默认等待 50 个区块来确认交易。按 5 秒一块算就是 250 秒约 4 分钟。对于正常交易来说绰绰有余但当交易池积压严重时某些交易可能排了 50 个块还没轮到它就直接超时报错了。nonce 管理是个隐形杀手。Calper 的 Worker 发交易时用的是同一个地址nonce 必须严格递增。如果一个 Worker 发了 nonce10 的交易但没被打包后续 nonce11, 12, 13… 的交易就算进了交易池也无法执行因为以太坊要求 nonce 连续所谓的机制问题。这就是那个nonce: 0xe报错背后的原因——前面有一笔卡住了后面全堵死了。五、调整策略与第二次测试有了第一次的教训我做了 3 件事把 Worker 数量从 1 提到 2。之前的配置里workers.number: 1只生成了一个 Manager虽然 Manager 内部 fork 了两个 Worker但 nonce 管理仍然受限于单进程。调整之后两个 Worker 各自独立管理自己的 nonce 序列拥堵风险会直线下降。把 write TPS 从 50 降到合理范围。不设定 50 了让 Caliper 按**固定速率模式fixed-rate**去跑就行。关键是…实际吞吐量不等于发送速率。发送速率只是我每秒扔多少笔交易到网络吞吐量才是链每秒真正确认了多少笔。测完看吞吐量就完了呗。读操作大幅提速率。读操作不消耗 gas、不修改状态、不需要等待确认。所以我把读的速率拉到 200 TPS看看极限在哪。第二次正式测试我选择了节点 210.8.161.50来跑。完整运行了 173 秒两轮都成功| Name | Succ | Fail | Send Rate (TPS) | Max Latency (s) | Min Latency (s) | Avg Latency (s) | Throughput (TPS) | |------------------|------|------|-----------------|-----------------|-----------------|-----------------|------------------| | write-operations | 6000 | 0 | 50.0 | 12.62 | 0.20 | 2.44 | 47.7 | | read-operations | 6000 | 0 | 200.1 | 0.35 | 0.00 | 0.02 | 200.0 |Benchmark finished in 173.174 seconds. Total rounds: 2. Successful rounds: 2. Failed rounds: 0.这个结果就比较像样了。我们逐项来看写操作write-operations6000 笔全部成功0 失败。发送速率 50 TPS实际吞吐量 47.7 TPS。差值很小说明链的处理能力跟上了发送节奏。平均延迟 2.44 秒对于一个 5 秒出块间隔的 QBFT 链来说这个数字是合理的。交易发出后需要等到下一个区块被打包所以延迟大致在 0 到 5 秒之间。平均值落在 2.44 秒说明大部分交易在 1 ~ 2 个区块内就确认了。最大延迟 12.62 秒。这个对应的是某些交易刚好卡在了区块打包的边界上多等了两个块。在区块链性能测试里最大延迟一般会比平均值高很多这是正常的。最小延迟 0.20 秒。这个是刚好赶上了的情况——交易发出去的时候下一个区块正在打包直接就进去了。读操作read-operations6000 笔全部成功。发送速率 200.1 TPS实际吞吐量 200.0 TPS。几乎零损耗。平均延迟 0.02 秒20 毫秒。这是纯本地查询所以极快。但实际业务里读操作不太可能都走view函数有些查询需要遍历历史状态延迟会高得多。最大延迟 0.35 秒。个别查询可能碰上了节点在做其他操作比如同步状态或者 compact LevelDB导致轻微波动。六、从结果回看 Caliper 的底层机制复盘整个测试过程有几个机制值得单独拎出来讲6.1 速率控制Rate Control我这次用的是fixed-rate模式也是最简单的模式。它的工作原理是用一个令牌桶token bucket每秒钟生成固定数量的令牌Worker 每发一笔交易就去拿一个令牌拿不到就等着。所以发送速率是严格控制的不会出现前面慢后面猛冲的情况。Caliper 还支持其他速率模式fixed-feedback-rate根据未确认交易的数量动态调整发送速率避免积压。linear-rate从某个初始 TPS 开始线性递增到目标 TPS。composite-rate允许在同一个 round 里组合多个速率阶段。对于 Besu QBFT 这种出块间隔固定的链fixed-rate其实就够用了。6.2 Worker 并行机制前面提到Caliper 在 local 模式下会 fork 子进程当 Worker。每个 Worker 独立维护自己的 WebSocket 连接和 nonce 计数器。这意味着两个 Worker 可以同时向节点发送交易互不阻塞。但两个 Worker 共用同一个fromAddress也就是同一个钱包地址所以它们的 nonce 是共享的。Calper 内部通过nonce协调机制避免两个 Worker 发出相同 nonce 的交易。如果交易池积压严重两个 Worker 的 nonce 会互相牵制——Worker A 的 nonce10 卡住了Worker B 的 nonce11 也会跟着卡。所以多 Worker 并不总是提升吞吐量有时候反而因为 nonce 竞争降低效率。这个取决于链的出块速度和交易池策略。6.3 交易确认超时Caliper 的transactionConfirmationBlocks设置为 1意味着每笔交易只需要被打包进 1 个区块就算确认。但内部还有一个硬编码的超时机制如果交易在 50 个区块内还未被打包直接标记为失败。这就是我第一天报的那个Error: Transaction was not mined within 50 blocks的来源。对于 5 秒一个块的链50 块 250 秒。所以如果你的链出块很慢比如 15 秒一块那就是 750 秒这个超时可能要调大。那么怎么调在网络配置里加一个blockConfirmationTimeout或者直接改timeout字段我配的是 60000 毫秒这个是 RPC 层面的超时不是区块确认超时两个不一样。七、写在最后填坑的最后这里给大家总结 5 条关于使用 Caliper 的总结经验别迷信发送速率。50 TPS 的发送速率不等于 50 TPS 的吞吐量。吞吐量才是你真正要关注的指标。我第一次测的时候眼睛只盯着我要发 50 TPS结果交易池被撑爆了。读和写要分开测。读操作不消耗 gas、不等待确认所以 TPS 能跑到 200 甚至更高。如果把读写混在一起跑读的高吞吐量会掩盖写的瓶颈。真实业务场景下系统 90% 的瓶颈都在写操作上。QBFT 的 5 秒出块是硬天花板。以blockperiodseconds: 5为例一个小时最多出 720 个块。如果每块平均能塞 50 笔交易那理论上限就是 36000 笔每小时、约 10 TPS按每笔 20 万 gas 算。如果你需要更高的写吞吐量要么缩短出块间隔要么提高 gas 上限要么换共识算法。nonce 管理是联盟链压测的暗坑。所有交易共用一个fromAddressnonce 必须严格递增。只要有一个 nonce 卡住后面的全排队。所以我建议测写性能时用多个独立地址发送交易。Caliper 支持配置多个fromAddress每个 Worker 可以绑定不同的地址这样 nonce 就不会互相干扰。我这次只用了一个地址下次试试多地址方案。压测完记得看 Besu 节点的日志大坑不展开讲了。Calliper 告诉你交易成功了但它不告诉你节点在这期间经历了什么。我每次跑完都会去 Besu 节点上journalctl看一眼有没有异常——CPU 飙高、LevelDB 的 compact 操作、P2P 断连等。这些信息不进 Caliper 的报告但对你理解真实性能瓶颈至关重要。至此“以太来袭”系列先告一段落。看国内用 Besu 的企业还是比较少的希望这个系列的文章与之前 Besu 的文章能够给大家有大的帮助也希望 Besu 在国内能够走得更远。