【精解】EOS TPS 多维实测

时间:2022-04-03 09:12:04

本文主要研究EOS的tps表现,会从插件、cleos、EOSBenchTool以及eosjs四种方式进行分析研究。

关键字:eos, tps, cleos, txn_test_gen_plugin, EOSBenchTool, qt, eosjs, C++源码分析

身心准备

  • tps: Transaction per Second. 每秒事务处理量
  • 链环境部署使用Python3脚本 bios-boot-tutorial,使用方法请参考boot-sequence脚本
  • 测试机器的硬件配置:双核cpu + 8G内存
  • eos中一个transaction的结构,展示如下:
{
"transaction_id": "7943f613f8cde71bc37d76daf3581ceb62ae6d481fa9b3a11ba73d19d909c666",
"broadcast": false,
"transaction": {
"compression": "none",
"transaction": {
"expiration": "2018-07-12T09:51:14",
"ref_block_num": 526,
"ref_block_prefix": 52869816,
"net_usage_words": 0,
"max_cpu_usage_ms": 0,
"delay_sec": 0,
"context_free_actions": [],
"actions": [
{
"account": "eosio.token",
"name": "transfer",
"authorization": [
{
"actor": "eosiotestay",
"permission": "active"
}
],
"data": "00bcc95865ea305500fcc95865ea3055010000000000000004535953000000000c7061636b696e672074657374"
}
],
"transaction_extensions": []
},
"signatures": [
"SIG_K1_KB6ENT2Ns3QmaPSfvxqCkgZTjK5RUDRFwkZ7p9Jv6p1GpnD67jhMUsw1Spfp7yw4hChsubPeiTc2HSt5hc6YdMH5rk5Kfz"
]
}
}

cleos方式

由于我们在研究eos阶段,大量使用到cleos,因此使用cleos来测试tps是我们第一个能想到的手段。这一节我们将加深理解tps的意义,tps的计算方法,讨论单节点与多节点环境对tps的影响。

单节点环境

单节点的搭建这里不再赘述,直接使用脚本执行,

./bios-boot-tutorial.py -k -w -b -s -c -t -S -T --user-limit 1000 -X

注意参数的顺序不能变。

执行成功以后,我们将得到一个拥有1000个stake账户(简单理解为已抵押完可直接投票的账户)的单节点eos环境,最后一个参数-X会让当前环境不断执行随机转账操作(注意:每一笔转账都是一个action,一个action对应一个transaction)

查看日志

修改脚本的stepLog函数,改为:

def stepLog():
run('tail -f ' + args.nodes_dir + '00-eosio/stderr')

然后在终端执行:

./bios-boot-tutorial.py -l

即可进入同步日志输出的界面。

一、shell方式

环境准备完毕,我们来测试一下当前正在不断进行转账的eos链上的tps表现。这里采用的tps计算方式为:

tps = BlockTxs*2

因为eos是半秒出块,所以两个块的打包交易量之和就是tps,为确保数值可靠性,每个块的打包交易量我们要通过大量区块取平均值的方式。

基于以上思想,可以总结出一个shell命令直接在终端执行即可:

for (( i = 12638; i <= 13638; i++ )); do cleos --wallet-url http://localhost:6666 --url http://localhost:8000 get block $i | grep "executed" | wc -l; done  | awk '{sum+=$1} END {print NR,"blocks average tps =", sum/NR*2}'

取出区块号从200到1200的区块,分别计算每个区块的打包交易量(通过统计其包含的“executed”即可,因为每个交易对应一个“executed”),然后将这些区块交易量进行累加除以数量得到平均值,再乘以2,辅以可视化备注输出即可。

最终结果不是很理想,至少距离官方声称的几千tps有很大差距。

1001 blocks average tps = 39.2727

所以1000个块统计tps为 39.2727

二、python脚本

由于tps的结果不理想,我也有过很多思考,下面我们换一种计算方式来看:

tps = trxs/time

这里通过一种简单的方式来计算tps:即统计共发出了trxs笔交易所耗费的时间,以秒为单位,然后相除即可得到tps。

基于以上思想,由于这部分代码是无法通过一行shell解决的,所以我通过修改bios脚本来解决,

  • 增加内容:
def stepTPS():
start = time.time()
numtps = args.num_tps
i = 0
while i < numtps :
print ("on: ",i)
randomTransfer(0, args.num_senders,1)
i=i+1
elapsed = (time.time() - start)
print ("Time used:",elapsed,"s tps=",numtps/elapsed)
  • 修改randomTransfer函数,增加参数t,用来决定循环次数:
def randomTransfer(b, e, t):
for j in range(t):
src = accounts[random.randint(b, e - 1)]['name']
dest = src
while dest == src:
dest = accounts[random.randint(b, e - 1)]['name']
run(args.cleos + 'transfer -f ' + src + ' ' + dest + ' "0.0001 ' + args.symbol + '"' + ' || true')
  • 增加命令:
('A', 'tps',            stepTPS,                    False,   "calculate the tps"),
  • 增加参数:
parser.add_argument('--num-tps', metavar='', help="Number of tps test trx", type=int, default=1000)
  • 执行A:

注意,在执行前,我们要先停掉单节点环境,将-X去掉,而采用我们的-A来执行随机转账。

./bios-boot-tutorial.py -k -w -b -s -c -t -S -T --user-limit 1000 -X
  • 执行B:
./bios-boot-tutorial.py -A --num-tps 2000

发起2000笔交易,然后使用脚本函数stepTPS进行测试。

  • 结果:
Time used: 26.172592401504517 s tps= 38.20790790072884

结果与shell方式差不多,都是不到40的tps表现。

多节点环境

tps的结果不尽人意,我又转念想到了是否因为单节点出块的原因。因此我搭建了多节点出块加全节点的环境,搭建环境的方法可以参考《【精解】EOS多节点组网:商业场景分析以及节点启动时序》

我仍旧通过以上两种方式,分别是shell方式和Python脚本的方式去测试,最后结果是并无改变,这也证实了eos不具备多线程处理事务的能力。

插曲:我将python脚本的修改提交了EOSIO/eos的官方pr,结果被拒绝合并,原因是“unrelated change”,转念一想,如果合并至源码,用户可以通过这种方式直白地得到eos的tps就是几十个的结论,那绝对是很不好的。

txn_test_gen_plugin插件测试

我对eos的高tps有了深深地怀疑,于是找来了官方的tps测试插件,要亲自感受一下tps的“洗礼”。插件的使用方式很简单,按照官方文档的步骤执行即可,最后我调整参数:

curl --data-binary '[""30, 50]' http:/ /localhost:8888/v1/txn_test_gen/start_generationn

链上日志结果:

【精解】EOS TPS 多维实测

通过trxs一列可以看出,每个区块打包的交易量大大提升了,平均tps在2000左右。

插件的测试方法也是bm所推崇的,他说通过cleos无法发挥出真正的eos的性能。那么具体是为什么,我们通过插件的源码txn_test_gen_plugin.cpp进行分析,我将这一部分内容单独成文,请阅读《【源码解读】EOS测试插件:txn_test_gen_plugin.cpp

EOSBenchTool方式

EOSBenchTool来自于OracleChain的贡献,虽然他们的节点oraclegogogo没竞选上bp,但我认为bp的竞选更多是市场行为,不是技术实力的“成绩单”,在所有bp中,目前我也仅看到了OracleChain做出的技术方面的贡献,包括对EOSIO/eos的pr,都是OracleChain自身技术气质的体现。多余夸奖的话不多讲了,下面来研究这套工具内容。

EOSBenchTool的思想与以上的cleos有很大不同,与插件的方式(打包交易)比较相似,但它的实现方式却是独具一格的,他并不是像插件那样直接在“服务器端”自我模拟交易来测试tps。他们敢于直接使用C++ 来编写客户端请求主网来打包、发起请求,最终测试得到一个非常不错的结果,大约可以到200到300,这个结果也是我在众多压测手段中得到的比较理想的结果,包括下面要介绍到的eosjs的方式,都不及EOSBenchTool的测试结果。

EOSBenchTool既能不牺牲在真实场景中的模拟,又能通过技术手段优化交易通讯,可以说他的tps结果是比较具备真实性、业务可行性,以及他的技术实现手段也是非常值得业务方来学习并尝试使用的。

EOSBenchTool的使用

官方文档的介绍比较技术范儿,就是不太亲民。这里我给他填点肉,希望层级尝试使用EOSBenchTool却失败的朋友能够在这里找到答案。

准备

一、EOS主网环境

首先,要准备EOS主网环境,可以通过脚本快速获得:python3 ./bios-boot-tutorial.py -k -w -b -s -c -t (不部署system合约,因为部署后无法使用create account创建账户。)

二、获取代码,QT工具,编译代码

  • 源码位置:EOSBenchTool

  • QT去官网下载community版本即可,注意:QT在安装时要同时勾选安装 QCreatorQT source 以及 QT prebuild tool(这里我选择的是mingw

  • 打开QCreator,一般情况下,上面的步骤准备妥当以后,QCreator会自动检测一套构建套件(Kit),构建套件依赖于Qt Version、编译器、Debuggers,Cmakes,这些工具也都是可以自动检测到的,如果无法检测到,一定是某个工具未安装,请检查相应的工具,并重新下载安装(一般来讲,所有这些工具在QT安装包都会包含,只需再次打开QT安装包,选择更新,重新勾选缺乏的工具安装即可。)最终我的构建套件(Kit) 截图如下:

    【精解】EOS TPS 多维实测

  • QCreator中,Open Project 导入项目源码中的文件 src/EOSBenchTool.pro,点击左下角小锄头构建项目

启动EOSBenchTool

以上工作都顺利完成以后,在QCreator中,点击左下角三角按钮运行启动EOSBenchTool工具。建议将UI最大化,可以更方便地查看日志。填写好setting内容,如下:

【精解】EOS TPS 多维实测

关于几个参数:

  • Thread number:会创建对应的账户数量。
  • Transaction pool size:总共发送的测试交易笔数
  • Transaction batch size:打包时每个包内包含的交易笔数

其他参数不多介绍。设置好参数以后,点击OK保存,然后切换到 Benchmark Testing 点击Prepare:创建测试账户、给测试账户转账、每个测试账户发起测试交易并打包。

等待Prepare结束,1万笔测试交易大约两到三分钟,视客户端机器本地性能。然后点击Start,得到tps结果,这里由于界面都是可视化的,我不再赘述。

源码分析

这部分我们将一起通过源码学习EOSBenchTool打包交易的原理。

  • 整个EOSBenchTool工具,我们从main.cpp入口,然后转到主要文件mainwindow.cpp,这里面包含了UI界面配置,传参,以及按钮事件,这里面我们主要关注按钮事件,总共有三个:
    • on_pushButtonOK_clicked,这是对应界面 setting 中的ok按钮,这是负责传参的,这里不做介绍。
    • on_pushButtonInitialize_clicked,这是对应界面 Benchmark Testing 中的Prepare按钮,稍后主要分析。
    • on_pushButtonRun_clicked,这是对应界面 Benchmark Testing 中的Start按钮,稍后主要分析。

on_pushButtonInitialize_clicked

Prepare阶段,正如上面在EOSBenchTool使用中介绍到的那样,包括创建账户,转账,打包。

  • 通过CreateAccount对象创建测试账户
  • 通过PushManager来转账
  • 通过Packer来打包交易

创建账户

下面先来看创建账户的源码:

CreateAccount createAccount;
int count = createAccount.create(thread_num, [=](const QString& name, bool res) { // lambda格式的回调函数:打印日志
commonOutput(QString("Create %1 %2.").arg(name).arg(res ? "succeed" : "failed"));
});

进入createaccount.cpp文件,查看create函数:

int CreateAccount::create(int threadNum, const create_account_callback& func)
{
if (threadNum <= 0) { // 根据threadNum个数创建对应数量的账户。
return 0;
}
// 清空其他账户
AccountManager::instance().removeAll(); for (int i = 0; i < threadNum; ++i) {
eos_key owner, active;
keys.clear(); // 头文件中的 QVector<eos_key> keys;
keys.push_back(owner); // 添加owner和active权限到keys对象
keys.push_back(active); newAccountName = createNewName(); bool res = false; QEventLoop loop;
// WINSOCK_API_LINKAGE int PASCAL connect (SOCKET, const struct sockaddr *, int);
// 通过connect开启一个socket通道
connect(this, &CreateAccount::oneRoundFinished, &loop, &QEventLoop::quit); if (httpc) { // httpc(new HttpClient)
httpc->request(FunctionID::get_info); // 通过http请求get info
// 以上的get_info回调函数,实际功能函数:get_info_returned,由connect开启socket访问进去。
connect(httpc, &HttpClient::responseData, this, &CreateAccount::get_info_returned);
} loop.exec(); // 返回执行结果res,成功为true,失败为false
res = !(AccountManager::instance().listKeys(newAccountName).first.empty()); // 执行回调函数:打印日志
func(newAccountName, res);
} return AccountManager::instance().count() - 1; // 除了super account以外的集合中的账户个数
}

查看一下AccountManager的源码:

class AccountManager
{
public:
AccountManager();
static AccountManager& instance(); void addAccounts(const QString& name, const QPair<std::string, std::string>& keypairs);
void removeAll();
QPair<std::string, std::string> listKeys(const QString& account);
QVector<std::string> listAccounts();
int count() const; private: // 私有属性,QMap集合对象 accounts
QMap<QString, QPair<std::string, std::string>> accounts;
};

removeAll的实现方法:

void AccountManager::removeAll()
{
QPair<std::string, std::string> superKey = accounts[super_account];
accounts.clear();
accounts.insert(super_account, superKey);
}

super_account和superKey是全局变量,在mainwindow.cpp前面标明:

QString super_account = "eosio";

实际上,是对QMap集合对象 accounts的操作。接着,账户名的生成方式:

QString CreateAccount::createNewName()
{
// eos的命名规则
static const char *char_map = "12345abcdefghijklmnopqrstuvwxyz";
int map_size = strlen(char_map);
QString newName; for (int i = 0; i < 5; ++i) {
int r = rand() % map_size; // 随机选出char_map的下标位置
newName += char_map[r];
} // 返回的是一个五位的名字 return newName;
}

AccountManager的实例也是个static的单例

AccountManager &AccountManager::instance()
{
static AccountManager manager;
return manager;
}

get_info_returned函数,

void CreateAccount::get_info_returned(const QByteArray &data)
{
//先关闭进来的socket通道
disconnect(httpc, &HttpClient::responseData, this, &CreateAccount::get_info_returned); getInfoData.clear();
getInfoData = data; QByteArray param = packGetRequiredKeysParam();
if (param.isNull()) {
emit oneRoundFinished();
return;
} if (httpc) {
// 通过http请求链的get_required_keys接口,传入对应事务的json格式作为入参。
httpc->request(FunctionID::get_required_keys, param);
// get_required_keys的回调函数,通过socket建立通道去访问get_required_keys_returned函数。
connect(httpc, &HttpClient::responseData, this, &CreateAccount::get_required_keys_returned);
}
}

转到函数packGetRequiredKeysParam(),该函数是创建账户的实际生效函数:

QByteArray CreateAccount::packGetRequiredKeysParam()
{
if (getInfoData.isEmpty()) {
return QByteArray();
} // 组装了newAccount的请求数据
EOSNewAccount newAccount(EOS_SYSTEM_ACCOUNT, newAccountName.toStdString(),
keys.at(0).get_eos_public_key(), keys.at(1).get_eos_public_key(),
EOS_SYSTEM_ACCOUNT); std::vector<unsigned char> hexData = newAccount.dataAsHex(); // 将data对象转为十六进制
// 通过ChainManager创建事务,是创建账户的事务。
signedTxn = ChainManager::createTransaction(EOS_SYSTEM_ACCOUNT, newAccount.getActionName(), std::string(hexData.begin(), hexData.end()),
ChainManager::getActivePermission(EOS_SYSTEM_ACCOUNT), getInfoData);
QJsonObject txnObj = signedTxn.toJson().toObject(); QJsonArray avaibleKeys;
std::string pub = eos_key::get_eos_public_key_by_wif(super_private_key.toStdString());// 通过私钥获得公钥
avaibleKeys.append(QJsonValue(QString::fromStdString(pub))); QJsonObject obj;
obj.insert("available_keys", avaibleKeys);
obj.insert("transaction", txnObj);
return QJsonDocument(obj).toJson();// 最终获得json格式的创建账户的事务对象
}

进入get_required_keys_returned函数,

void CreateAccount::get_required_keys_returned(const QByteArray &data)
{
disconnect(httpc, &HttpClient::responseData, this, &CreateAccount::get_required_keys_returned); getRequiredKeysData.clear();
getRequiredKeysData = data; QByteArray param = packPushTransactionParam();
if (param.isNull()) {
emit oneRoundFinished();
return;
} if (httpc) {
// 相同的套路,通过packPushTransactionParam()函数组装好的推送交易接口的入参param,然后通过http发起请求。
httpc->request(FunctionID::push_transaction, param);
// 通过connect建立socket连接访问push_transaction的回调函数push_transaction_returned,继续处理。
connect(httpc, &HttpClient::responseData, this, &CreateAccount::push_transaction_returned);
}
}

packPushTransactionParam(),开始组装push transaction的参数,由于代码中对于数据的处理较多,这里只展示结果的部分:

// 给上面由函数packGetRequiredKeysParam()组装的交易signedTxn签名。
signedTxn.sign(pri, TypeChainId::fromHex(info.value("chain_id").toString().toStdString()));
PackedTransaction packedTxn(signedTxn, "none"); QJsonObject obj = packedTxn.toJson().toObject(); return QJsonDocument(obj).toJson(); // 获得签名后的交易数据

push_transaction_returned,我们经过大量的组合校验,与链上的信息进行同步组装获得了合法的签名交易对象,然后通过http接口请求了push_transaction接口将签名交易对象推送到链上执行,执行结果通过回调函数处理,回调函数的主要作用是将处理结果 -> 成功创建了的这个账户,存入集合accounts中,由于accounts是私有属性,所以通过方法AccountManager::instance().addAccounts执行。

客户端本地保存了一个对象accounts用来同步自己创建过的账户。大部分代码是对accounts的处理。

账户转账

在上一个创建账户的部分,我们详细解读了通讯的过程,仍旧是通过http去发起请求,通过每个请求的回调函数进行处理,组装,维护了本地的集合accounts。由于篇幅过大,在之后的介绍中,不会再过多介绍,而专注于实现方式的核心代码。转账的核心代码:

QVector<std::string> accounts = AccountManager::instance().listAccounts(); // 通过accounts获得测试账户们
int accountSize = accounts.size();
int balance = total_tokens / accountSize; // 平均分配测试用币
for (int i = 0; i < accountSize; ++i) {
PushManager push;
QString quantity = QString("%1.0000 %2").arg(balance).arg(token_name); // 拼串,转账额度
QString to = QString::fromStdString(accounts.at(i)); // 遍历接收转账的账户
commonOutput(QString("Transfering %1 to %2 ...").arg(quantity).arg(to)); // 日志
bool ret = push.transferToken(super_account, to, quantity); // 核心生效代码,是PushManager的transferToken函数。
commonOutput(ret ? "Succeed." : "Failed.");
}

PushManager的transferToken函数是本地组装了标准的转账请求参数,json字符串格式的from, to, quality以及memo信息。然后跳转到make_push函数。make_push函数需要通过http请求接口abi_json_to_bin,而针对该接口的入参,都需要在这个函数处理获取到,入参包括action,code以及args。code就是对应的合约的code,例如我们使用账户eosio部署了合约eosio.system,那么eosio.system的code就可以通过get code eosio获得。action就是转账:transfer。args就是上面PushManager的transferToken函数组装的参数对象。http请求成功以后,通过回调函数abi_json_to_bin_returned处理响应结果。

if (httpc) {
httpc->request(FunctionID::abi_json_to_bin, QJsonDocument(obj).toJson());
connect(httpc, &HttpClient::responseData, this, &PushManager::abi_json_to_bin_returned);
}

接口abi_json_to_bin:序列化json数据为二进制数据。这个结果的数据通常用在push_transaction的data字段。

action.setData(hexData); // action的hexData字段就是以上接口**abi\_json\_to\_bin**获得的结果。

剩余部分与上面介绍“创建账户”相同,get_info -> get_required_keys -> push_transaction 的流程。

总结一下,转账由于涉及到合约,所以多了一步abi_json_to_bin,而创建账户不需要这一步,但创建账户需要本地的集合对象同步存储。

打包交易

首先说明,打包的交易是测试交易,不是以上的创建账户和账户转账。先看源码部分:

trxpool = new TransactionPool; // 创建交易池
trxpool->setTargetSize(trx_size); // 设置交易池的大小
// packedTrxTransferFinished,打包测试交易发送链全部结束
connect(trxpool, &TransactionPool::finished, this, &MainWindow::packedTrxTransferFinished);
// packedTrxReady,prepare阶段完成,可以点击start
connect(trxpool, &TransactionPool::packedTrxPoolFulfilled, this, &MainWindow::packedTrxReady); enablePacker(true);// 核心打包内容

enablePacker(),触发打包流程

QVector<std::string> accounts = AccountManager::instance().listAccounts();
for (int i = 0; i < accounts.size(); ++i) {
Packer *p = new Packer;
connect(p, &Packer::finished, p, &QObject::deleteLater); // auto delete
// A:稍后重点讲
connect(p, &Packer::newPackedTrx, trxpool, &TransactionPool::incomingPackedTrxs); // 为Packer的对象设置属性的值
p->setAccountName(QString::fromStdString(accounts.at(i)));
p->setCallback([=] (const QString& msg) {
commonOutput(msg);
});
p->start(); // 执行Packer packers.push_back(p);
}

进入incomingPackedTrxs函数,

void TransactionPool::incomingPackedTrxs(const QByteArray &data)
{
// 上锁,data推入packedTransactions,QVector<QByteArray> packedTransactions;
QMutexLocker locker(&mutex);
packedTransactions.push_back(data); if (packedTransactions.size() >= targetSize) { // 通过我们设置的交易池的大小来控制总测试交易量
emit packedTrxPoolFulfilled();
}
}

Packer开始执行,

void Packer::run()
{
while(!needStop) {
PushManager push(false);
// 这是一个包含lambda为回调函数的connect语句
connect(&push, &PushManager::trxPacked, this, [&](const QByteArray& data){
emit newPackedTrx(data); // emit 发送signal给newPackedTrx B:稍后重点讲
func(QString("PACKED: %1 to %2.").arg(accountName).arg(super_account));// 打印日志
});
// 以下部分与账户转账接口一致,后续内容均同上。
push.transferToken(accountName, super_account, QString("0.0001 %1").arg(token_name));
}
}

当Packer开始run的时候,它是一个无线循环,直到灌满trxPool为止,而其中,我们注意观察,这一connect翻译过来就是:我先注册一个signals trxPacked在这,等待某处代码将该信号发射,会被这里捕捉到,将它传入回调函数,就是这个lambda回调函数的参数data中,这个lambda回调函数我们先放一放,来讲这个signals trxPacked:

signals 对应的触发是 emit

trxPacked 作为一个signals 是在PushManager::get_required_keys_returned中被发射emit的(注意这个是与上面讲到的CreateAccount::get_required_keys_returned是不同的。)

QByteArray param = packPushTransactionParam();
emit trxPacked(param);
...
httpc->request(FunctionID::push_transaction, param);

这个emit发送的param是仅在push_transaction发送之前的transaction,会将这个对象传入回调函数。下面来看一下lambda回调函数的内部,获取到transaction数据对象以后,会将该对象再次emit到一个signals newPackedTrx,我们去找一下这个signals的注册位置:MainWindow::enablePacker,就是上面展示过的代码,我注释为“A:稍后重点讲”,因此相同的原理,这个data又被传入了incomingPackedTrxs函数,最终被打包进packedTransactions集合中。

关于QT的signals emit slot connect 的具体语法介绍的内容可以查看这篇文章我们没有QT开发的需求,所以没必要在此过多介绍语法内容,只需要捋清楚业务逻辑即可。

packedTransactions的内容是属于TransactionPool的,它会在TransactionPool被启动时(也就是start按钮被按下时)使用,而这个对象是在prepare阶段被储存。(据说这个时间只有5分钟,机器性能不太好的不要将trxPool设置地太高,否则执行不完,打包好的packedTransactions并未做持久化,就会消失掉,最终导致测试结果失真)

on_pushButtonRun_clicked

这个按钮点击事件的内容看上去比较简单,只有一个enableTrxpool(true)是生效代码,其他都是一些日志。下面直接进入enableTrxpool函数,不张贴了,直接转到核心代码trxpool->start(); 那么我们进入到transactionpool.cpp,start对应run函数,源码如下:

void TransactionPool::run()
{
DataManager::instance().setBeginBlockNum(get_block_info());// get_block_info()是通过http请求链获取的
HttpClient httpc;
int sz = packedTransactions.size();
for (int i = 0; i < sz && !needStop; i += batch_size) {
QEventLoop loop;
connect(&httpc, &HttpClient::responseData, &loop, &QEventLoop::quit); QJsonArray array;
int range = sz - i > batch_size ? batch_size : sz - i;
for (int j = 0; j < range; ++j) {
QJsonObject val = QJsonDocument::fromJson(packedTransactions.at(i+j)).object();
array.append(val);
}
// http请求push_transactions接口,推送打包交易到链
httpc.request(FunctionID::push_transactions, QJsonDocument(array).toJson());
loop.exec();
}
DataManager::instance().setEndBlockNum(get_block_info());
packedTransactions.clear();
}

这段代码就是上面提到的对 packedTransactions 的“消费”,核心代码是按照设置的打包(后称小包)大小来逐渐“消费”packedTransactions,然后通过http的push_transactions接口,将这些“小包”推送到链执行。

总结

没想到EOSBenchTool的源码解读一下子搞了这么长的篇幅,我没控制住,读者又要吃力了。其实到这里我们来总结一下,EOSBenchTool主要是使用了QT的界面系统,同时也用到了QT的signals,emit,connect等专有语法,不懂qt的同学看起来有些吃力。然而,抛开这些语言或者类库的语法来讲,我们专注于代码逻辑,EOSBenchTool的实现是容易被人理解的:

  • 首先,可以确定他是一个客户端,都是通过我们前面文章介绍过很多遍的最熟悉的那些http接口的请求来与链交互的。
  • ++接着,它采用了本地内存对象的方式来存储我们设定好的所有的交易量的集合对象。这个部分是可以改善的,毕竟如果测试量过大就会丢失。++
  • 它设计了一个“小包”的概念,相对应的,我们前面打包好的“大包”,我们设置了一个小包的大小,可以按照小包为单位对链发起批量交易的请求。

eosjs方式

上面我们介绍了:

Way Business TPS memo
cleos 可直接使用 70-80 (单节点、多节点)shell方式,python脚本
txn_test_gen_plugin 不可使用 1500-2000 官方用来测试的一种方式,这个插件纯粹是为了测tps而设的
EOSBenchTool 可修改使用 200-300 C++门槛较高且无对外封装接口

通过以上总结,我们可以推论出,如果有一种方式,支持:

  • 有对外接口可易于调用
  • 开发语言门槛较低
  • 客户端行为
  • 支持打包请求
  • tps能达到200-300

那么它对于业务方来讲,是完全可以接受并享受基于eos的区块链带来的红利的。

下面就到了引出eosjs的时刻了,eosjs是官方EOSIO组织承认的客户端调用技术,它不仅仅是对rpc协议的封装,更多的还有大量的eos本身的特性,这些特性都可以做到在客户端本地实现,例如本地签名,本地生成交易id等等,这些技术可以让我们在业务方的客户端角度充分挖掘需求,自定义接口,上乘业务方,下启公有链eos环境,这种目前为止最为合适的承上启下的技术就是eosjs。

源码位置

准备环境

eos环境,可通过脚本快速搭建:

python3 ./bios-boot-tutorial.py -k -w -b -s -c -t

继续调用

python3 ./bios-boot-tutorial.py -l

将终端界面的输出内容保持链日志的同步输出。

源码架构

eosjs是使用JavaScript语言,nodejs框架构成。

nodejs框架天生可以让我们便携地封装导出以及依赖导入某个“组件”,监于这种特性,我们也可以为业务方开发自己的sdk。

常用组件

  • src/index.js 中的 module.exports = EOS,这是主要组件,通过该组件可创建相应对象
  • eosjs-ecc,可获得加密工具对象,该对象能够调用所有加密相关的动作,例如签名,私钥公钥等。
const Eos = require('../src')
const ecc = require('eosjs-ecc')

EOS对象

const keyProvider = [
"5K463ynhZoCDDa4RDcr63cUwWLTnKqmdcoTKTHBjqoKfv4u5V7p",
ecc.seedPrivate('test-tps')
]
const eos = Eos({
httpEndpoint: 'http://39.107.152.239:8000',
chainId: '1c6ae7719a2a3b4ecb19584a30ff510ba1b6ded86e1fd8b8fc22f1179c622a32',
keyProvider: keyProvider,
expireInSeconds: 60,
broadcast: false,
verbose: true
})
  • expireInSeconds:过期时间,该行为如在此过期时间内仍未执行成功,则会被判定过期而抛弃。
  • broadcast:这是一个本地行为(false)还是要广播到远端链上(ture)。
  • verbose:是否要打印所有发生http请求的请求返回结构体。

eos对象的能力:

{ getCurrencyBalance: [Function],
getCurrencyStats: [Function],
getProducers: [Function],
getInfo: [Function],
getBlock: [Function],
getAccount: [Function],
getCode: [Function],
getTableRows: [Function],
getAbi: [Function],
abiJsonToBin: [Function],
abiBinToJson: [Function],
getRequiredKeys: [Function],
pushBlock: [Function],
pushTransaction: [Function],
pushTransactions: [Function],
getActions: [Function],
getControlledAccounts: [Function],
getKeyAccounts: [Function],
getTransaction: [Function],
createTransaction: [Function],
api: { createTransaction: [Function: createTransaction] },
transaction: [AsyncFunction],
nonce: [Function],
bidname: [Function],
buyram: [Function],
buyrambytes: [Function],
canceldelay: [Function],
claimrewards: [Function],
delegatebw: [Function],
deleteauth: [Function],
linkauth: [Function],
newaccount: [Function],
onerror: [Function],
refund: [Function],
regproducer: [Function],
regproxy: [Function],
reqauth: [Function],
rmvproducer: [Function],
sellram: [Function],
setalimits: [Function],
setglimits: [Function],
setprods: [Function],
setabi: [Function],
setcode: [Function],
setparams: [Function],
setpriv: [Function],
setram: [Function],
undelegatebw: [Function],
unlinkauth: [Function],
unregprod: [Function],
updateauth: [Function],
voteproducer: [Function],
create: [Function],
issue: [Function],
transfer: [Function],
contract: [Function],
fc:
{ structs:
{ extensions_type: [Object],
transaction_header: [Object],
transaction: [Object],
signed_transaction: [Object],
field_def: [Object],
producer_key: [Object],
producer_schedule: [Object],
chain_config: [Object],
type_def: [Object],
struct_def: [Object],
clause_pair: [Object],
error_message: [Object],
abi_def: [Object],
table_def: [Object],
action: [Object],
action_def: [Object],
block_header: [Object],
packed_transaction: [Object],
nonce: [Object],
authority: [Object],
bidname: [Object],
blockchain_parameters: [Object],
buyram: [Object],
buyrambytes: [Object],
canceldelay: [Object],
claimrewards: [Object],
connector: [Object],
delegatebw: [Object],
delegated_bandwidth: [Object],
deleteauth: [Object],
eosio_global_state: [Object],
exchange_state: [Object],
key_weight: [Object],
linkauth: [Object],
namebid_info: [Object],
newaccount: [Object],
onerror: [Object],
permission_level: [Object],
permission_level_weight: [Object],
producer_info: [Object],
refund: [Object],
refund_request: [Object],
regproducer: [Object],
regproxy: [Object],
require_auth: [Object],
rmvproducer: [Object],
sellram: [Object],
set_account_limits: [Object],
set_global_limits: [Object],
set_producers: [Object],
setabi: [Object],
setcode: [Object],
setparams: [Object],
setpriv: [Object],
setram: [Object],
total_resources: [Object],
undelegatebw: [Object],
unlinkauth: [Object],
unregprod: [Object],
updateauth: [Object],
user_resources: [Object],
voteproducer: [Object],
voter_info: [Object],
wait_weight: [Object],
account: [Object],
create: [Object],
currency_stats: [Object],
issue: [Object],
transfer: [Object],
fields: [Object] },
types:
{ bytes: [Function],
string: [Function],
vector: [Function],
optional: [Function],
time: [Function],
map: [Function],
static_variant: [Function],
fixed_string16: [Function],
fixed_string32: [Function],
fixed_bytes16: [Function],
fixed_bytes20: [Function],
fixed_bytes28: [Function],
fixed_bytes32: [Function],
fixed_bytes33: [Function],
fixed_bytes64: [Function],
fixed_bytes65: [Function],
uint8: [Function],
uint16: [Function],
uint32: [Function],
uint64: [Function],
uint128: [Function],
uint224: [Function],
uint256: [Function],
uint512: [Function],
varuint32: [Function],
int8: [Function],
int16: [Function],
int32: [Function],
int64: [Function],
int128: [Function],
int224: [Function],
int256: [Function],
int512: [Function],
varint32: [Function],
float64: [Function],
name: [Function],
public_key: [Function],
symbol: [Function],
extended_symbol: [Function],
asset: [Function],
extended_asset: [Function],
signature: [Function],
config: [Object],
checksum160: [Function],
checksum256: [Function],
checksum512: [Function],
message_type: [Function],
symbol_code: [Function],
field_name: [Function],
account_name: [Function],
permission_name: [Function],
type_name: [Function],
token_name: [Function],
table_name: [Function],
scope_name: [Function],
action_name: [Function],
time_point: [Function],
time_point_sec: [Function],
timestamp: [Function],
block_timestamp_type: [Function],
block_id: [Function],
checksum_type: [Function],
checksum256_type: [Function],
checksum512_type: [Function],
checksum160_type: [Function],
sha256: [Function],
sha512: [Function],
sha160: [Function],
weight_type: [Function],
block_num_type: [Function],
share_type: [Function],
digest_type: [Function],
context_free_type: [Function],
unsigned_int: [Function],
bool: [Function],
transaction_id_type: [Function] },
fromBuffer: [Function],
toBuffer: [Function],
abiCache: { abiAsync: [Function: abiAsync], abi: [Function: abi] } },
modules:
{ format:
{ ULong: [Function: ULong],
isName: [Function: isName],
encodeName: [Function: encodeName],
decodeName: [Function: decodeName],
encodeNameHex: [Function: encodeNameHex],
decodeNameHex: [Function: decodeNameHex],
DecimalString: [Function: DecimalString],
DecimalPad: [Function: DecimalPad],
DecimalImply: [Function: DecimalImply],
DecimalUnimply: [Function: DecimalUnimply],
printAsset: [Function: printAsset],
parseAsset: [Function: parseAsset] } } }

实例:创建用户

通过以上列出的eos对象的提供的这些功能,我们可以满足大部分业务方的需求,这里展示一个创建用户的代码实例:

const nameRule = "12345abcdefghijklmnopqrstuvwxyz"
const config = {
trx_pool_size: 10,
optBCST: {expireInSeconds: 120, broadcast: true},
opts: {expireInSeconds: 60, broadcast: false},
ok: true,
no: false
}
function createAccount(account, publicKey, callback) {
eos.transaction(tr => {
tr.newaccount({
creator: 'eosio',
name: account,
owner: publicKey,
active: publicKey
}) tr.buyrambytes({
payer: 'eosio',
receiver: account,
bytes: 4096
}) tr.delegatebw({
from: 'eosio',
receiver: account,
stake_net_quantity: '0.0002 SYS',
stake_cpu_quantity: '0.0002 SYS',
transfer: 0
})
}).then(callback)
} function generateAccounts(nameroot) {
for (i = 0; i < 31; i++) {
let accountname = nameroot + nameRule.charAt(i)
console.log("create account: ", accountname)
createAccount(accountname, ecc.privateToPublic(keyProvider[1]), asset => {
eos.transfer("eosio", accountname, "40.0000 SYS", "initial distribution", config.optBCST)
})
}
}

实例:获取账户余额

function getAccountsBalance(nameroot) {
for (i = 0; i < 31; i++) {
let accountname = nameroot + nameRule.charAt(i)
eos.getCurrencyBalance("eosio.token", accountname, "SYS").then(tx => {
console.log(accountname + " balance: " + tx[0])
})
}
}

打包交易

打包交易接口目前我还未封装完毕,这篇文章更适合作为学习研究而不是代码段粘贴,因此对于打包交易的功能,研究好以上内容的朋友可以有自己的想法,这里我简单说一下我的实现思路:

每笔transaction是可以包含多个action的,在上面介绍过的插件的实现中,也是它的实现思路。另外push_transactions接口是链提供的http接口,我们打包多笔transaction成一个transactions对象请求这个接口,正如插件和EOSBenchTool的实现方式。然后中间要经过大量的优化,这其中较为重要的是我们的本地交易池,这个概念在EOSBenchTool中也研究过,那里的内存对象最多存活5分钟,而我们这里要如何设计呢?是否采用内存变量?还是引入队列?这都是架构师的工作,也是根据不同的业务场景大有所为的地方。

更新添加打包交易时序图:

【精解】EOS TPS 多维实测

更新打包交易源码: Templar

总结

本篇文章全面而详细地分析了EOS中关于tps的一切手段,包括了cleos,插件,EOSBenchTool,eosjs的方式,这其中,我们仔细研究了EOSBenchTool的源码,过程中也涉及到了qt的部分语法,对比了这几种方式的利弊,讨论了tps的计算方式,tps的现实意义,插件的“作弊”行为,EOSBenchTool的良好思路和贡献,eosjs的最终确型,以及针对transaction,action等内部元素的深入理解与研究。最后也思考了未来eos商业实现的架构设想:通过eosjs作为承上启下的sdk。

参考资料

  • EOS官方文档
  • EOSBenchTool源码
  • eosjs源码

更多文章请转到醒者呆的博客园