区块链性能突破(七):并行合约开发框架

阅读数:8663 2019 年 8 月 30 日 16:07

区块链性能突破(七):并行合约开发框架

“性能”是区块链领域最受关注的话题之一,如果没有高性能表现作为支撑,就无法运行快速的、执行复杂的智能合约逻辑,从而快速完成交易事务。本系列文章将以 FISCO BCOS 开源社区对区块链性能提升的研究成果为例,阐述区块链性能优化之路。

本专题系列文章追到现在,也许你会想问,FISCO BCOS 的并行到底怎么用?作为专题的完结篇,本文就来揭晓“庐山真面目”,并教你上手使用 FISCO BCOS 的并行特性!

FISCO BCOS 提供了可并行合约开发框架,开发者按照框架规范编写的合约,能够被 FISCO BCOS 节点并行地执行。

并行合约的优势有:

  • 高吞吐:多笔独立交易同时被执行,能最大限度利用机器的 CPU 资源,从而拥有较高的 TPS
  • 可拓展:可以通过提高机器的配置来提升交易执行的性能,以支持不断扩大业务规模

接下来,我将介绍如何编写 FISCO BCOS 并行合约,以及如何部署和执行并行合约。

预备知识

并行互斥

两笔交易是否能被并行执行,依赖于这两笔交易是否存在互斥。互斥,是指两笔交易各自操作合约存储变量的集合存在交集。

例如,在转账场景中,交易是用户间的转账操作。用 transfer(X, Y) 表示从 X 用户转到 Y 用户的转账接口。互斥情况如下:

此处给出更具体的定义:

互斥参数:合约接口中,与合约存储变量的“读 / 写”操作相关的参数。例如转账的接口 transfer(X, Y),X 和 Y 都是互斥参数。

互斥对象:一笔交易中,根据互斥参数提取出来的、具体的互斥内容。例如转账的接口 transfer(X, Y), 一笔调用此接口的交易中,具体的参数是 transfer(A, B),则这笔操作的互斥对象是 [A, B];另外一笔交易,调用的参数是 transfer(A, C),则这笔操作的互斥对象是 [A, C]。

判断同一时刻两笔交易是否能并行执行,就是判断两笔交易的互斥对象是否有交集。相互之间交集为空的交易可并行执行。

编写并行合约

FISCO BCOS 提供了可并行合约开发框架,开发者只需按照框架的规范开发合约,定义好每个合约接口的互斥参数,即可实现能被并行执行的合约。当合约被部署后,FISCO BCOS 会在执行交易前,自动解析互斥对象,在同一时刻尽可能让无依赖关系的交易并行执行。

目前,FISCO BCOS 提供了 solidity 与预编译合约(点击可查看预编译合约架构设计)两种可并行合约开发框架。

solidity 合约的并行框架

编写并行的 solidity 合约,开发流程与开发普通 solidity 合约流程相同。在此基础上,只需将 ParallelContract 作为需要并行的合约基类,并调用 registerParallelFunction(),注册可以并行的接口即可。

先给出完整的举例。例子中的 ParallelOk 合约实现了并行转账的功能:

复制代码
pragma solidity ^0.4.25;
import "./ParallelContract.sol"; // 引入 ParallelContract.sol
contract ParallelOk is ParallelContract // 将 ParallelContract 作为基类
{
// 合约实现
mapping (string => uint256) _balance;
function transfer(string from, string to, uint256 num) public
{
// 此处为简单举例,实际生产中请用 SafeMath 代替直接加减
_balance[from] -= num;
_balance[to] += num;
}
function set(string name, uint256 num) public
{
_balance[name] = num;
}
function balanceOf(string name) public view returns (uint256)
{
return _balance[name];
}
// 注册可以并行的合约接口
function enableParallel() public
{
// 函数定义字符串(注意 "," 后不能有空格), 参数的前几个是互斥参数(设计函数时互斥参数必须放在前面
registerParallelFunction("transfer(string,string,uint256)", 2); // 冲突参数: string string
registerParallelFunction("set(string,uint256)", 1); // 冲突参数: string
}
// 注销并行合约接口
function disableParallel() public
{
unregisterParallelFunction("transfer(string,string,uint256)");
unregisterParallelFunction("set(string,uint256)");
}
}

具体步骤如下:

step1 将 ParallelContract 作为合约的基类

复制代码
pragma solidity ^0.4.25;
import "./ParallelContract.sol"; // 引入 ParallelContract.sol
contract ParallelOk is ParallelContract // 将 ParallelContract 作为基类
{
// 合约实现
// 注册可以并行的合约接口
function enableParallel() public;
// 注销并行合约接口
function disableParallel() public;
}

step2 编写可并行的合约接口

合约中的 public 函数,是合约的接口。编写可并行的合约接口,是根据一定的规则,实现一个合约中的 public 函数。

确定接口是否可并行
可并行的合约接口,必须满足:

  • 无调用外部合约
  • 无调用其它函数接口

确定互斥参数
在编写接口前,先确定接口的互斥参数,接口的互斥即是对全局变量的互斥,互斥参数的确定规则为:

  • 接口访问了全局 mapping,mapping 的 key 是互斥参数
  • 接口访问了全局数组,数组的下标是互斥参数
  • 接口访问了简单类型的全局变量,所有简单类型的全局变量共用一个互斥参数,用不同的变量名作为互斥对象

确定参数类型和顺序
确定互斥参数后,根据规则确定参数类型和顺序,规则为:

  • 接口参数仅限:string、address、uint256、int256(未来会支持更多类型)
  • 互斥参数必须全部出现在接口参数中
  • 所有互斥参数排列在接口参数的最前
复制代码
mapping (string => uint256) _balance; // 全局 mapping
// 互斥变量 fromto 排在最前,作为 transfer() 开头的两个参数
function transfer(string from, string to, uint256 num) public
{
_balance[from] -= num; // from 是全局 mapping 的 key,是互斥参数
_balance[to] += num; // to 是全局 mapping 的 key,是互斥参数
}
// 互斥变量 name 排在最前,作为 set() 开头的参数
function set(string name, uint256 num) public
{
_balance[name] = num;
}

step3 在框架中注册可并行的合约接口

在合约中实现 enableParallel() 函数,调用 registerParallelFunction() 注册可并行的合约接口。同时也需要实现 disableParallel() 函数,使合约具备取消并行执行的能力。

复制代码
// 注册可以并行的合约接口
function enableParallel() public
{
// 函数定义字符串(注意 "," 后不能有空格), 参数的前几个是互斥参数
registerParallelFunction("transfer(string,string,uint256)", 2); // transfer 接口,前 2 个是互斥参数
registerParallelFunction("set(string,uint256)", 1); // transfer 接口,前 1 个四互斥参数
}
// 注销并行合约接口
function disableParallel() public
{
unregisterParallelFunction("transfer(string,string,uint256)");
unregisterParallelFunction("set(string,uint256)");
}

step4 部署 / 执行并行合约

用控制台或 Web3SDK 编译和部署合约,此处以控制台为例:

部署合约

复制代码
[group:1]> deploy ParallelOk.sol

调用 enableParallel() 接口,让 ParallelOk 能并行执行

复制代码
[group:1]> call ParallelOk.sol 0x8c17cf316c1063ab6c89df875e96c9f0f5b2f744 enableParallel

发送并行交易 set()

复制代码
[group:1]> call ParallelOk.sol 0x8c17cf316c1063ab6c89df875e96c9f0f5b2f744 set "jimmyshi" 100000

发送并行交易 transfer()

复制代码
[group:1]> call ParallelOk.sol 0x8c17cf316c1063ab6c89df875e96c9f0f5b2f744 transfer "jimmyshi" "jinny" 80000]

查看交易执行结果 balanceOf()

复制代码
[group:1]> call ParallelOk.sol 0x8c17cf316c1063ab6c89df875e96c9f0f5b2f744 balanceOf "jinny"
80000

用 SDK 发送大量交易的例子,将在下文的例子中给出。

预编译合约的并行框架

编写并行的预编译合约,开发流程与开发普通预编译合约流程相同。普通的预编译合约以 Precompile 为基类,在这之上实现合约逻辑。基于此,Precompile 的基类还为并行提供了两个虚函数,继续实现这两个函数,即可实现并行的预编译合约。

step1 将合约定义成支持并行

复制代码
bool isParallelPrecompiled() override { return true; }

step2 定义并行接口和互斥参数

注意,一旦定义成支持并行,所有的接口都需要进行定义。若返回空,表示此接口无任何互斥对象。互斥参数与预编译合约的实现相关,此处涉及对 FISCO BCOS 存储的理解,具体的实现可直接阅读代码或询问相关有经验的程序员。

复制代码
// 根据并行接口,从参数中取出互斥对象,返回互斥对象
std::vector<std::string> getParallelTag(bytesConstRef param) override
{
// 获取被调用的函数名(func)和参数(data)
uint32_t func = getParamFunc(param);
bytesConstRef data = getParamData(param);
std::vector<std::string> results;
if (func == name2Selector[DAG_TRANSFER_METHOD_TRS_STR2_UINT]) // 函数是并行接口
{
// 接口为:userTransfer(string,string,uint256)
// 从 data 中取出互斥对象
std::string fromUser, toUser;
dev::u256 amount;
abi.abiOut(data, fromUser, toUser, amount);
if (!invalidUserName(fromUser) && !invalidUserName(toUser))
{
// 将互斥对象写到 results 中
results.push_back(fromUser);
results.push_back(toUser);
}
}
else if ... // 所有的接口都需要给出互斥对象,返回空表示无任何互斥对象
return results; // 返回互斥
}

step3 编译,重启节点

手动编译节点的方法,参考 FISCO BCOS 技术文档

编译之后,关闭节点,替换掉原来的节点二进制文件,再重启节点即可。

举例:并行转账

此处分别给出 solidity 合约和预编译合约的并行举例。

配置环境

该举例需要以下执行环境:

  • Web3SDK 客户端
  • 一条 FISCO BCOS 链

若需要压测最大的性能,至少需要:

  • 3 个 Web3SDK,才能产生足够多的交易
  • 4 个节点,且所有 Web3SDK 都配置了链上所有的节点信息,让交易均匀发送到每个节点上,才能让链接收足够多的交易

并行 Solidity 合约:ParallelOk

基于账户模型的转账,是一种典型的业务操作。ParallelOk 合约,是账户模型的一个举例,能实现并行的转账功能。ParallelOk 合约已在上文中给出。

FISCO BCOS 在 Web3SDK 中内置了 ParallelOk 合约,此处给出用 Web3SDK 来发送大量并行交易的操作方法。

step1 用 SDK 部署合约、新建用户、开启合约的并行能力

复制代码
# 参数:<groupID> add < 创建的用户数量 > < 此创建操作请求的 TPS> < 生成的用户信息文件名 >
java -cp conf/:lib/*:apps/* org.fisco.bcos.channel.test.parallel.parallelok.PerformanceDT 1 add 10000 2500 user
# 在 group1 上创建了 10000 个用户,创建操作以 2500TPS 发送的,生成的用户信息保存在 user 中

执行成功后,ParallelOk 被部署到区块链上,创建的用户信息保存在 user 文件中,同时开启了 ParallelOk 的并行能力。

step2 批量发送并行转账交易

注意:在批量发送前,请将 SDK 的日志等级调整为 ERROR,才能有足够的发送能力。

复制代码
# 参数:<groupID> transfer < 总交易数量 > < 此转账操作请求的 TPS 上限 > < 需要的用户信息文件 > < 交易互斥百分比:0~10>
java -cp conf/:lib/*:apps/* org.fisco.bcos.channel.test.parallel.parallelok.PerformanceDT 1 transfer 100000 4000 user 2
# 向 group1 发送了 100000 比交易,发送的 TPS 上限是 4000,用的之前创建的 user 文件里的用户,发送的交易间有 20% 的互斥。

step3 验证并行正确性

并行交易执行完成后,Web3SDK 会打印出执行结果。TPS 是此 SDK 发送的交易在节点上执行的 TPS。validation 是转账交易执行结果的检查。

复制代码
Total transactions: 100000
Total time: 34412ms
TPS: 2905.9630361501804
Avg time cost: 4027ms
Error rate: 0%
Return Error rate: 0%
Time area:
0 < time < 50ms : 0 : 0.0%
50 < time < 100ms : 44 : 0.044000000000000004%
100 < time < 200ms : 2617 : 2.617%
200 < time < 400ms : 6214 : 6.214%
400 < time < 1000ms : 14190 : 14.19%
1000 < time < 2000ms : 9224 : 9.224%
2000 < time : 67711 : 67.711%
validation:
user count is 10000
verify_success count is 10000
verify_failed count is 0

可以看出,本次交易执行的 TPS 是 2905。执行结果校验后,无任何错误 (verify_failed count is 0)。

step4 计算总 TPS

单个 Web3SDK 无法发送足够多的交易以达到节点并行执行能力的上限。需要多个 Web3SDK 同时发送交易。在多个 Web3SDK 同时发送交易后,单纯将结果中的 TPS 加和得到的 TPS 不够准确,需要直接从节点处获取 TPS。

用脚本从日志文件中计算 TPS

复制代码
cd tools
sh get_tps.sh log/log_2019031821.00.log 21:26:24 21:26:59 # 参数:< 日志文件 > < 计算开始时间 > < 计算结束时间 >

得到 TPS(3 SDK、4 节点,8 核,16G 内存)

复制代码
statistic_end = 21:26:58.631195
statistic_start = 21:26:24.051715
total transactions = 193332, execute_time = 34580ms, tps = 5590 (tx/s)

并行预编译合约:DagTransferPrecompiled

与 ParallelOk 合约的功能一样,FISCO BCOS 内置了一个并行预编译合约的例子(DagTransferPrecompiled),实现了简单的基于账户模型的转账功能。该合约能够管理多个用户的存款,并提供一个支持并行的 transfer 接口,实现对用户间转账操作的并行处理。

注意:DagTransferPrecompiled 仅做示例使用,请勿直接运用于生产环境。

step1 生成用户

用 Web3SDK 发送创建用户的操作,创建的用户信息保存在 user 文件中。命令参数与 parallelOk 相同,不同的仅仅是命令所调用的对象是 precompile。

复制代码
java -cp conf/:lib/*:apps/* org.fisco.bcos.channel.test.parallel.precompile.PerformanceDT 1 add 10000 2500 user

step2 批量发送并行转账交易

用 Web3SDK 发送并行转账交易。

注意:在批量发送前,请将 SDK 的日志等级请调整为 ERROR,才能有足够的发送能力。

复制代码
java -cp conf/:lib/*:apps/* org.fisco.bcos.channel.test.parallel.precompile.PerformanceDT 1 transfer 100000 4000 user 2

step3 验证并行正确性

并行交易执行完成后,Web3SDK 会打印出执行结果。TPS 是此 SDK 发送的交易在节点上执行的 TPS。validation 是转账交易执行结果的检查。

复制代码
Total transactions: 80000
Total time: 25451ms
TPS: 3143.2949589407094
Avg time cost: 5203ms
Error rate: 0%
Return Error rate: 0%
Time area:
0 < time < 50ms : 0 : 0.0%
50 < time < 100ms : 0 : 0.0%
100 < time < 200ms : 0 : 0.0%
200 < time < 400ms : 0 : 0.0%
400 < time < 1000ms : 403 : 0.50375%
1000 < time < 2000ms : 5274 : 6.592499999999999%
2000 < time : 74323 : 92.90375%
validation:
user count is 10000
verify_success count is 10000
verify_failed count is 0

可以看出,本次交易执行的 TPS 是 3143。执行结果校验后,无任何错误 (verify_failed count is 0)。

step4 计算总 TPS

单个 Web3SDK 无法发送足够多的交易以达到节点并行执行能力的上限。需要多个 Web3SDK 同时发送交易。在多个 Web3SDK 同时发送交易后,单纯将结果中的 TPS 加和得到的 TPS 不够准确,需要直接从节点处获取 TPS。

用脚本从日志文件中计算 TPS

复制代码
cd tools
sh get_tps.sh log/log_2019031311.17.log 11:25 11:30 # 参数:< 日志文件 > < 计算开始时间 > < 计算结束时间 >

得到 TPS(3 SDK、4 节点,8 核,16G 内存)

复制代码
statistic_end = 11:29:59.587145
statistic_start = 11:25:00.642866
total transactions = 3340000, execute_time = 298945ms, tps = 11172 (tx/s)

结果说明

本文举例中的性能结果,是在 3SDK、4 节点、8 核、16G 内存、1G 网络下测得。每个 SDK 和节点都部署在不同的 VPS 中,硬盘为云硬盘。实际 TPS 会根据你的硬件配置、操作系统和网络带宽有所变化。

收藏

评论

微博

用户头像
发表评论

注册/登录 InfoQ 发表评论