在前两期中,秘猿小课堂给你们分享了 构建高性能区块链内核 CITA 背后的思考。这一期,咱们深刻研究 CITA 是如何进行性能优化,而且将交易处理的性能达到 15000 TPS量级
秘猿科技区块链小课堂第 6 期
点击关注秘猿科技在思否的技术社区吧~html
在区块链的设计中,有一个「不可能三角」的说法,即安全、去中心化、性能,这三者只能取其二。Nervos 是用分层设计来解决不可能三角问题。在底层 Layer1 里,CKB 就选取安全和去中心化,Layer2 选性能。Layer2 追求把性能作到极致,去中心化和安全由 CKB 来解决。git
CITA 做为支持智能合约的区块链框架,有很是良好的性能表现,交易处理的性能能够达到 15000 TPS[1],很是适合做为 Layer2 的高性能区块链解决方案。本文将会简要讨论秘猿科技是如何对 CITA 进行性能优化的。github
传统的公有区块链每每采用总体式架构。由于须要考虑去中心化,就须要考虑节点能够在普通硬件上能够执行,而在架构设计上没法兼顾性能。CITA 做为专门面向企业用户设计的高性能许可链(许可链能够是联盟链,也能够是公有许可链),采用微服务架构,能够更好的利用服务器集群,而不是使用单一机器运行节点。这样能够充分利用硬件的优点,节点再也不是一个物理概念,而是一个逻辑概念。算法
传统的 PBFT 类算法中,通常使用三阶段协议 prevote、precommit、commit ,以 Tendermint 为例。数据库
Commit 阶段主要是为了 Proposer 向其余节点再广播一轮 BlockProof,使得全部节点统一投票。可是实际上在 Precommit 阶段,各个节点已经收集足够的投票,只是投票集合可能不一致。好比对于 ABCD 四个节点,A 可能收到 ABCD 的投票,B 只收到 BCD 的投票。因为投票属于 Block 的一部分,也须要共识,为了保证节点统一投票,因此由 Proposer 再进行一轮广播。segmentfault
在 CITA-BFT 中,咱们优化了 Commit 阶段。在区块链中,Block 的共识是一个连续的共识。因此,咱们能够将当前 Block 的 Proof 放到下一个块的 Proposal 中,这样能够在下一个块的 Prevote 阶段对上一个 Block 的 Proof 进行统一,再也不广播共识后的 Block。缓存
这样作的优势有两个:安全
减小了一轮消息的广播,缩短了共识时间,而且减小了网络的负担。
传统的 PBFT 类共识算法在 Commit 阶段,若是 Proposer 发送掉线等状况,会须要额外的一轮进行共识,而在 CITA-BFT 则不会发生这种状况。性能优化
在传统的 PBFT 类共识的区块链中,共识和交易的处理都是串行的。在共识 Block 的过程当中,Executor 是闲置的。共识完成以后,将新的 Block 发送给 Executor 处理,Consensus 等待 Executor 处理完以后才能进行新的高度的共识,此时 Consensus 模块是闲置的。待 Executor 处理完 Block 以后,发送最新的 Status 以后,Consensus 才进行新高度的共识。服务器
在实际的共识过程当中,节点在 Prevote 阶段收到 Proposal 并验证以后,Proposal 就有很大可能变成最终 Commit 的 Block。在网络状况正常的状况下,一般一轮共识便可完成当前高度的 Block,此时若是提早对 Proposal 中的交易进行处理,则在共识流程中的后半部分则和交易的执行是同时进行的。当 Executor 处理完以后,等待 Consensus 发送已经确认的 Block,此时 Executor 只须要判断此 Proposal 是不是共识的 Block。若是是,则直接将处理结果进行 Finalize,并通知 Consensus 进行新高度的共识;若是否,则从新处理,这种状况和没有预执行的流程是一致的。
这样在多数状况下区块都能提早处理,将交易处理的时间提早。即使在较坏状况下,进行多轮共识时,Proposal 也能够按照时间戳进行比较,打断当前正在进行的预处理,执行更新的 Proposal。在最坏状况下,没有收到 Proposal 或者收到错误的 Proposal,Executor 也和原来的共识流程相同,在收到 CommitedBlock 以后,进行交易的处理,并无任何性能上的损失。
在性能优化中,缓存是一种常见的手段,一样在 CITA 中也存在大量缓存,来解决性能问题。
签名验证缓存。一般交易签名的验证比较耗时,对于已经验证经过的交易,根据其 Hash 将验证结果进行缓存。这样若是节点再收到一样的交易(多是用户重复发送或者从其余节点转发)时,则能够命中缓存,减小验证签名的时间消耗。
区块信息缓存。对于在交易处理过程当中,或者用户查询操做中,常常会须要查询 Block 或者 Transaction 等信息,能够将此类信息缓存,这样能大大提升查询的效率。
在交易处理过程当中,须要从数据库中读取以前的 State,而 MPT 的查询路径比较长,须要屡次 DB 的查询,很是耗时。在 CITA 中,会将常用到 Account 进行缓存。
经过缓存技术,大大减小了交易验证和处理的时间。
在微服务架构中,因为服务的拆分致使各个微服务之间消息通讯比较频繁,这样消息中间件很是容易成为瓶颈。一方面,因为咱们使用了微服务架构,消息中间件自己能够单独部署,能够经过提升硬件能力进行纵向扩展,也能够经过集群的方式进行横向扩展。
除此以外,咱们还对微服务之间的消息进行优化,来提升微服务间通讯的效率。
消息压缩。例如在压力比较大时,Block 中每每有上万笔交易,这样消息会很是大。所以咱们采用了消息压缩技术,在消息超过必定大小时,会对其进行压缩,这样减小消息中间件的压力,同时也减小了传输量,提升了传输速度。
减小没必要要的消息。例如,在 Consensus 服务收到 Proposal 时,须要对其合法性进行验证。因为其可能包含大量交易,会致使传输量很大。所以在 Consensus 能够先验证交易的 Hash 是否正确,再将 Proposal 的其余信息和交易的 Hash 发送给 Auth 模块便可,而不用将整个交易发送给 Auth 模块。
打包发送。将消息打包,也是一种比较常见优化手段。好比在 RPC 模块中,须要将交易发送给 Auth 模块进行验证,在压力比较大的时候单个消息发送则消息数量会很是大,此时 RPC 会将消息进行打包后再发送给 Auth 模块,能够大幅度的减小消息的数量,从而减小消息中间件的负载,提升消息的发送速度。
在 Bitcoin 中,为了解决轻节点的交易验证问题,引入了 MerkleTree。可是 Merkle Tree 的每一个节点的产生都要计算一次 Hash,而 Hash 计算很是耗时。
你们注意到最后的叶子节点 Hc 是直接复制了 Hc。这样是由于 Bitcoin 和 Ethereum 中交易是依次加入到 Merkle Tree 中,能够递增地去构造 Merkle Root。好比节点当前的 PendingBlock 中有交易 TxA、TxB、TxC、TxD,当前的 Merkle Root 是 H(ABCD)。新交易来 TxE 来了以后,计算 H(EE),再向上计算,这样原有的 H(ABCD) 部分不用再计算。
当交易 TxF 来了以后,替换掉最右边的 TxE,再依次向上计算 root,这样只计算一部分就能够了。以前的 H(ABCD) 部分不用再从新计算。
在 CITA 中,交易会先通过 Auth 验证进入交易池,而后由 Consensus 一次性选取交易打包后共识,最后再由 Executor 处理,同时将处理后的结果 Receipt Root 存入 header 中 。因为是先共识交易内容而后再计算交易的结果,因此 Block 中的交易的处理结果 Receipt Root 顺序已经彻底肯定,不会再发生修改了。由此咱们能够将全部 Receipts 一次性算出其 Receipt Root,而不用考虑其动态计算的过程。由此咱们能够优化 Receipt Root 的计算。
对比 Bitcoin 和 Ethereum 中的 Merkle Tree,会发现因为没有奇数节点的复制,节点 E 这里的 Hash 计算会减小。咱们称这棵树为 Static Merkle Tree。
另外,在 Ethereum 中每笔交易都会产生新的 State Root,而 State Root 的计算又是很是耗时的。所以在 CITA 设计之初,交易计算完以后,只会将其状态更新到 Account Model 中,而不会将其变动更新到 State 的 MPT 中。只有在整个 Block 计算完成以后,才会将全部的计算结果提交到 State 的 MPT 中并计算 State Root,这样大大减小了 MPT 的操做和计算。在 Ethereum 最新的设计中也采用了一样的方案。
当前 Bitcoin 和 Ethereum 都采用了 secp256k1 的签名算法,在交易验证中,签名的验证尤为消耗 CPU 资源而且耗时。CITA 支持多种签名算法,默认采用 secp256k1。在笔者的电脑(Thinkpad 470p i7-7820HQ)上简单的转帐交易的签名验证速度大约为 3000 多每秒。Auth 模块提供了交易的并行验签,这样能够充分发挥硬件的优点,提升系统的验签速度。
除此以外,CITA还实现了 Ed25519 签名,相比 secp256k1 性能更好,而且在安全性方面也更有优点,用户能够根据我的需求选择本身想要的签名算法。咱们性能测试的 15000 TPS 的数据指的是 secp256k1。
在软件设计中,异步处理一般也是比较好的性能优化手段。除了前文提到的交易预处理,在 CITA 中的其余模块中也存在一些这样的设计。好比在 Executor 中,Block 执行完成以后,须要将最新的 State 保存到 DB,而一旦 State 状态变动比较多时,此操做将比较耗时。由此,Executor 提早将 Status 发送给其余模块,而后再到 DB 进行存储。固然可能会在存储失败时致使异常状况,所以在 Conseneus 会保存最新的几个 Block,来防止其余微服务内存储失败的特殊状况发生。
另外,CITA提供了批量交易的接口,用户能够组装多个交易数据,共享同一个签名。这样原来的多个交易就变成一个交易,减小交易存储和签名验证,加快交易的处理,同时也下降了用户发送交易的手续费。例如用户 A 须要调用 N 个不一样的合约,原来须要发送 N 笔交易,经过批量交易的方式,能够将合约调用的 data 按照既定格式拼装在一块儿,而后再进行一次签名发送到批量交易合约,合约再将 data 解析成多个合约调用。
固然批量交易只能算做一笔复杂的组合交易,只完成了一次正常交易的处理流程,所以在性能测试中只能算做一笔交易。CITA 在性能测试中并未采用此手段。
CITA 中绝大部分代码是用 Rust 语言实现。Rust 语言的极小运行时,与 C 语言媲美的优异性能,也是 CITA 良好性能的一大保证。做为国内最先使用 Rust 的团队,从 2016 年开始至今也在和 Rust 一同成长。 Rust 语言的其余特性:保证内存安全,基于 trait 的泛型,模式匹配,类型推断,高效 C 绑定等等,也极大的提升了咱们的开发效率。
将来咱们还会在微服务架构改进,网络层,Block/Transaction 广播,状态存储,硬件加速,VM,并行计算等等各个方面作更多研究,将 CITA 性能提上一个更高的水平。
CITA技术白皮书:https://github.com/cryptape/c...
Ed25519: https://exonum.com/blog/09-27...
eip-658:https://github.com/ethereum/E...
batch_tx:https://docs.nervos.org/cita/...
Rust versus C gcc fastest programs: https://benchmarksgame-team.p...