原文鏈接:醒者呆的博客園店读,https://www.cnblogs.com/Evsward/p/eostps.html
本文主要研究EOS的tps表現(xiàn),會從插件、cleos、EOSBenchTool以及eosjs四種方式進行分析研究绞蹦。
關鍵字:eos, tps, cleos, txn_test_gen_plugin, EOSBenchTool, qt, eosjs, C++源碼分析
身心準備
- tps: Transaction per Second. 每秒事務處理量
- 鏈環(huán)境部署使用Python3腳本 bios-boot-tutorial,使用方法請參考boot-sequence腳本
- 測試機器的硬件配置:雙核cpu + 8G內(nèi)存
- 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是我們第一個能想到的手段溅呢。這一節(jié)我們將加深理解tps的意義澡屡,tps的計算方法,討論單節(jié)點與多節(jié)點環(huán)境對tps的影響藕届。
單節(jié)點環(huán)境
單節(jié)點的搭建這里不再贅述,直接使用腳本執(zhí)行亭饵,
./bios-boot-tutorial.py -k -w -b -s -c -t -S -T --user-limit 1000 -X
注意參數(shù)的順序不能變休偶。
執(zhí)行成功以后,我們將得到一個擁有1000個stake賬戶(簡單理解為已抵押完可直接投票的賬戶)的單節(jié)點eos環(huán)境辜羊,最后一個參數(shù)-X會讓當前環(huán)境不斷執(zhí)行隨機轉(zhuǎn)賬操作(注意:每一筆轉(zhuǎn)賬都是一個action踏兜,一個action對應一個transaction)
查看日志
修改腳本的stepLog函數(shù)词顾,改為:
def stepLog():
run('tail -f ' + args.nodes_dir + '00-eosio/stderr')
然后在終端執(zhí)行:
./bios-boot-tutorial.py -l
即可進入同步日志輸出的界面。
一碱妆、shell方式
環(huán)境準備完畢肉盹,我們來測試一下當前正在不斷進行轉(zhuǎn)賬的eos鏈上的tps表現(xiàn)。這里采用的tps計算方式為:
tps = BlockTxs*2
因為eos是半秒出塊疹尾,所以兩個塊的打包交易量之和就是tps上忍,為確保數(shù)值可靠性,每個塊的打包交易量我們要通過大量區(qū)塊取平均值的方式纳本。
基于以上思想窍蓝,可以總結出一個shell命令直接在終端執(zhí)行即可:
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}'
取出區(qū)塊號從200到1200的區(qū)塊,分別計算每個區(qū)塊的打包交易量(通過統(tǒng)計其包含的“executed”即可繁成,因為每個交易對應一個“executed”)吓笙,然后將這些區(qū)塊交易量進行累加除以數(shù)量得到平均值,再乘以2巾腕,輔以可視化備注輸出即可面睛。
最終結果不是很理想,至少距離官方聲稱的幾千tps有很大差距尊搬。
1001 blocks average tps = 39.2727
所以1000個塊統(tǒng)計tps為 39.2727叁鉴。
二、python腳本
由于tps的結果不理想毁嗦,我也有過很多思考亲茅,下面我們換一種計算方式來看:
tps = trxs/time
這里通過一種簡單的方式來計算tps:即統(tǒng)計共發(fā)出了trxs筆交易所耗費的時間,以秒為單位狗准,然后相除即可得到tps克锣。
基于以上思想,由于這部分代碼是無法通過一行shell解決的腔长,所以我通過修改bios腳本來解決袭祟,
- 增加內(nèi)容:
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函數(shù),增加參數(shù)t,用來決定循環(huán)次數(shù):
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"),
- 增加參數(shù):
parser.add_argument('--num-tps', metavar='', help="Number of tps test trx", type=int, default=1000)
-
執(zhí)行A:
注意己肮,在執(zhí)行前甚带,我們要先停掉單節(jié)點環(huán)境,將-X去掉胆绊,而采用我們的-A來執(zhí)行隨機轉(zhuǎn)賬。
./bios-boot-tutorial.py -k -w -b -s -c -t -S -T --user-limit 1000 -X
- 執(zhí)行B:
./bios-boot-tutorial.py -A --num-tps 2000
發(fā)起2000筆交易欧募,然后使用腳本函數(shù)stepTPS進行測試压状。
結果:
Time used: 26.172592401504517 s tps= 38.20790790072884
結果與shell方式差不多,都是不到40的tps表現(xiàn)。
多節(jié)點環(huán)境
tps的結果不盡人意种冬,我又轉(zhuǎn)念想到了是否因為單節(jié)點出塊的原因镣丑。因此我搭建了多節(jié)點出塊加全節(jié)點的環(huán)境,搭建環(huán)境的方法可以參考《【精解】EOS多節(jié)點組網(wǎng):商業(yè)場景分析以及節(jié)點啟動時序》
我仍舊通過以上兩種方式娱两,分別是shell方式和Python腳本的方式去測試莺匠,最后結果是并無改變,這也證實了eos不具備多線程處理事務的能力十兢。
插曲:我將python腳本的修改提交了EOSIO/eos的官方pr趣竣,結果被拒絕合并,原因是“unrelated change”纪挎,轉(zhuǎn)念一想期贫,如果合并至源碼,用戶可以通過這種方式直白地得到eos的tps就是幾十個的結論异袄,那絕對是很不好的通砍。
txn_test_gen_plugin插件測試
我對eos的高tps有了深深地懷疑,于是找來了官方的tps測試插件烤蜕,要親自感受一下tps的“洗禮”封孙。插件的使用方式很簡單,按照官方文檔的步驟執(zhí)行即可讽营,最后我調(diào)整參數(shù):
curl --data-binary '[""30, 50]' http:/ /localhost:8888/v1/txn_test_gen/start_generationn
鏈上日志結果:
通過trxs一列可以看出虎忌,每個區(qū)塊打包的交易量大大提升了,平均tps在2000左右橱鹏。
插件的測試方法也是bm所推崇的膜蠢,他說通過cleos無法發(fā)揮出真正的eos的性能。那么具體是為什么莉兰,我們通過插件的源碼txn_test_gen_plugin.cpp進行分析挑围,我將這一部分內(nèi)容單獨成文,請閱讀《【源碼解讀】EOS測試插件:txn_test_gen_plugin.cpp》
EOSBenchTool方式
EOSBenchTool來自于OracleChain的貢獻糖荒,雖然他們的節(jié)點oraclegogogo沒競選上bp杉辙,但我認為bp的競選更多是市場行為,不是技術實力的“成績單”捶朵,在所有bp中蜘矢,目前我也僅看到了OracleChain做出的技術方面的貢獻,包括對EOSIO/eos的pr综看,都是OracleChain自身技術氣質(zhì)的體現(xiàn)品腹。多余夸獎的話不多講了,下面來研究這套工具內(nèi)容红碑。
EOSBenchTool的思想與以上的cleos有很大不同舞吭,與插件的方式(打包交易)比較相似,但它的實現(xiàn)方式卻是獨具一格的,他并不是像插件那樣直接在“服務器端”自我模擬交易來測試tps镣典。他們敢于直接使用C++ 來編寫客戶端請求主網(wǎng)來打包、發(fā)起請求唾琼,最終測試得到一個非常不錯的結果兄春,大約可以到200到300,這個結果也是我在眾多壓測手段中得到的比較理想的結果锡溯,包括下面要介紹到的eosjs的方式赶舆,都不及EOSBenchTool的測試結果。
EOSBenchTool既能不犧牲在真實場景中的模擬祭饭,又能通過技術手段優(yōu)化交易通訊芜茵,可以說他的tps結果是比較具備真實性、業(yè)務可行性倡蝙,以及他的技術實現(xiàn)手段也是非常值得業(yè)務方來學習并嘗試使用的九串。
EOSBenchTool的使用
官方文檔的介紹比較技術范兒,就是不太親民寺鸥。這里我給他填點肉猪钮,希望層級嘗試使用EOSBenchTool卻失敗的朋友能夠在這里找到答案。
準備
一胆建、EOS主網(wǎng)環(huán)境
首先烤低,要準備EOS主網(wǎng)環(huán)境,可以通過腳本快速獲得:python3 ./bios-boot-tutorial.py -k -w -b -s -c -t (不部署system合約笆载,因為部署后無法使用create account創(chuàng)建賬戶扑馁。)
二、獲取代碼凉驻,QT工具腻要,編譯代碼
源碼位置:EOSBenchTool
QT去官網(wǎng)下載community版本即可,注意:QT在安裝時要同時勾選安裝 QCreator 和 QT source 以及 QT prebuild tool(這里我選擇的是mingw)
打開QCreator沿侈,一般情況下闯第,上面的步驟準備妥當以后,QCreator會自動檢測一套構建套件(Kit)缀拭,構建套件依賴于Qt Version咳短、編譯器、Debuggers蛛淋,Cmakes咙好,這些工具也都是可以自動檢測到的,如果無法檢測到褐荷,一定是某個工具未安裝勾效,請檢查相應的工具,并重新下載安裝(一般來講,所有這些工具在QT安裝包都會包含层宫,只需再次打開QT安裝包杨伙,選擇更新,重新勾選缺乏的工具安裝即可萌腿。)最終我的構建套件(Kit) 截圖如下:
- QCreator中限匣,Open Project 導入項目源碼中的文件 src/EOSBenchTool.pro,點擊左下角小鋤頭構建項目
啟動EOSBenchTool
以上工作都順利完成以后毁菱,在QCreator中米死,點擊左下角三角按鈕運行啟動EOSBenchTool工具。建議將UI最大化贮庞,可以更方便地查看日志峦筒。填寫好setting內(nèi)容,如下:
關于幾個參數(shù):
- Thread number:會創(chuàng)建對應的賬戶數(shù)量窗慎。
- Transaction pool size:總共發(fā)送的測試交易筆數(shù)
- Transaction batch size:打包時每個包內(nèi)包含的交易筆數(shù)
其他參數(shù)不多介紹物喷。設置好參數(shù)以后,點擊OK保存遮斥,然后切換到 Benchmark Testing 點擊Prepare:創(chuàng)建測試賬戶脯丝、給測試賬戶轉(zhuǎn)賬、每個測試賬戶發(fā)起測試交易并打包伏伐。
等待Prepare結束宠进,1萬筆測試交易大約兩到三分鐘,視客戶端機器本地性能藐翎。然后點擊Start材蹬,得到tps結果,這里由于界面都是可視化的吝镣,我不再贅述堤器。
源碼分析
這部分我們將一起通過源碼學習EOSBenchTool打包交易的原理。
- 整個EOSBenchTool工具末贾,我們從main.cpp入口闸溃,然后轉(zhuǎn)到主要文件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使用中介紹到的那樣,包括創(chuàng)建賬戶吸耿,轉(zhuǎn)賬,打包酷窥。
- 通過CreateAccount對象創(chuàng)建測試賬戶
- 通過PushManager來轉(zhuǎn)賬
- 通過Packer來打包交易
創(chuàng)建賬戶
下面先來看創(chuàng)建賬戶的源碼:
CreateAccount createAccount;
int count = createAccount.create(thread_num, [=](const QString& name, bool res) { // lambda格式的回調(diào)函數(shù):打印日志
commonOutput(QString("Create %1 %2.").arg(name).arg(res ? "succeed" : "failed"));
});
進入createaccount.cpp文件咽安,查看create函數(shù):
int CreateAccount::create(int threadNum, const create_account_callback& func)
{
if (threadNum <= 0) { // 根據(jù)threadNum個數(shù)創(chuàng)建對應數(shù)量的賬戶。
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回調(diào)函數(shù)蓬推,實際功能函數(shù):get_info_returned妆棒,由connect開啟socket訪問進去。
connect(httpc, &HttpClient::responseData, this, &CreateAccount::get_info_returned);
}
loop.exec();
// 返回執(zhí)行結果res沸伏,成功為true糕珊,失敗為false
res = !(AccountManager::instance().listKeys(newAccountName).first.empty());
// 執(zhí)行回調(diào)函數(shù):打印日志
func(newAccountName, res);
}
return AccountManager::instance().count() - 1; // 除了super account以外的集合中的賬戶個數(shù)
}
查看一下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的實現(xiàn)方法:
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的命名規(guī)則
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函數(shù)喇肋,
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格式作為入?yún)ⅰ? httpc->request(FunctionID::get_required_keys, param);
// get_required_keys的回調(diào)函數(shù)迹辐,通過socket建立通道去訪問get_required_keys_returned函數(shù)蝶防。
connect(httpc, &HttpClient::responseData, this, &CreateAccount::get_required_keys_returned);
}
}
轉(zhuǎn)到函數(shù)packGetRequiredKeysParam(),該函數(shù)是創(chuàng)建賬戶的實際生效函數(shù):
QByteArray CreateAccount::packGetRequiredKeysParam()
{
if (getInfoData.isEmpty()) {
return QByteArray();
}
// 組裝了newAccount的請求數(shù)據(jù)
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對象轉(zhuǎn)為十六進制
// 通過ChainManager創(chuàng)建事務明吩,是創(chuàng)建賬戶的事務间学。
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格式的創(chuàng)建賬戶的事務對象
}
進入get_required_keys_returned函數(shù),
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()函數(shù)組裝好的推送交易接口的入?yún)aram低葫,然后通過http發(fā)起請求。
httpc->request(FunctionID::push_transaction, param);
// 通過connect建立socket連接訪問push_transaction的回調(diào)函數(shù)push_transaction_returned仍律,繼續(xù)處理氮采。
connect(httpc, &HttpClient::responseData, this, &CreateAccount::push_transaction_returned);
}
}
packPushTransactionParam(),開始組裝push transaction的參數(shù)染苛,由于代碼中對于數(shù)據(jù)的處理較多鹊漠,這里只展示結果的部分:
// 給上面由函數(shù)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(); // 獲得簽名后的交易數(shù)據(jù)
push_transaction_returned,我們經(jīng)過大量的組合校驗躯概,與鏈上的信息進行同步組裝獲得了合法的簽名交易對象登钥,然后通過http接口請求了push_transaction接口將簽名交易對象推送到鏈上執(zhí)行,執(zhí)行結果通過回調(diào)函數(shù)處理娶靡,回調(diào)函數(shù)的主要作用是將處理結果 -> 成功創(chuàng)建了的這個賬戶牧牢,存入集合accounts中,由于accounts是私有屬性姿锭,所以通過方法AccountManager::instance().addAccounts執(zhí)行塔鳍。
客戶端本地保存了一個對象accounts用來同步自己創(chuàng)建過的賬戶。大部分代碼是對accounts的處理呻此。
賬戶轉(zhuǎn)賬
在上一個創(chuàng)建賬戶的部分轮纫,我們詳細解讀了通訊的過程,仍舊是通過http去發(fā)起請求焚鲜,通過每個請求的回調(diào)函數(shù)進行處理掌唾,組裝,維護了本地的集合accounts忿磅。由于篇幅過大糯彬,在之后的介紹中,不會再過多介紹葱她,而專注于實現(xiàn)方式的核心代碼撩扒。轉(zhuǎn)賬的核心代碼:
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); // 拼串,轉(zhuǎn)賬額度
QString to = QString::fromStdString(accounts.at(i)); // 遍歷接收轉(zhuǎn)賬的賬戶
commonOutput(QString("Transfering %1 to %2 ...").arg(quantity).arg(to)); // 日志
bool ret = push.transferToken(super_account, to, quantity); // 核心生效代碼吨些,是PushManager的transferToken函數(shù)却舀。
commonOutput(ret ? "Succeed." : "Failed.");
}
PushManager的transferToken函數(shù)是本地組裝了標準的轉(zhuǎn)賬請求參數(shù),json字符串格式的from, to, quality以及memo信息锤灿。然后跳轉(zhuǎn)到make_push函數(shù)挽拔。make_push函數(shù)需要通過http請求接口abi_json_to_bin,而針對該接口的入?yún)⒌#夹枰谶@個函數(shù)處理獲取到螃诅,入?yún)╝ction,code以及args状囱。code就是對應的合約的code术裸,例如我們使用賬戶eosio部署了合約eosio.system,那么eosio.system的code就可以通過get code eosio獲得亭枷。action就是轉(zhuǎn)賬:transfer袭艺。args就是上面PushManager的transferToken函數(shù)組裝的參數(shù)對象。http請求成功以后叨粘,通過回調(diào)函數(shù)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數(shù)據(jù)為二進制數(shù)據(jù)瘤睹。這個結果的數(shù)據(jù)通常用在push_transaction的data字段。
action.setData(hexData); // action的hexData字段就是以上接口**abi\_json\_to\_bin**獲得的結果答倡。
剩余部分與上面介紹“創(chuàng)建賬戶”相同轰传,get_info -> get_required_keys -> push_transaction 的流程。
總結一下瘪撇,轉(zhuǎn)賬由于涉及到合約获茬,所以多了一步abi_json_to_bin,而創(chuàng)建賬戶不需要這一步倔既,但創(chuàng)建賬戶需要本地的集合對象同步存儲恕曲。
打包交易
首先說明,打包的交易是測試交易渤涌,不是以上的創(chuàng)建賬戶和賬戶轉(zhuǎn)賬佩谣。先看源碼部分:
trxpool = new TransactionPool; // 創(chuàng)建交易池
trxpool->setTargetSize(trx_size); // 設置交易池的大小
// packedTrxTransferFinished,打包測試交易發(fā)送鏈全部結束
connect(trxpool, &TransactionPool::finished, this, &MainWindow::packedTrxTransferFinished);
// packedTrxReady歼捏,prepare階段完成,可以點擊start
connect(trxpool, &TransactionPool::packedTrxPoolFulfilled, this, &MainWindow::packedTrxReady);
enablePacker(true);// 核心打包內(nèi)容
enablePacker()笨篷,觸發(fā)打包流程
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(); // 執(zhí)行Packer
packers.push_back(p);
}
進入incomingPackedTrxs函數(shù)瞳秽,
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開始執(zhí)行练俐,
void Packer::run()
{
while(!needStop) {
PushManager push(false);
// 這是一個包含lambda為回調(diào)函數(shù)的connect語句
connect(&push, &PushManager::trxPacked, this, [&](const QByteArray& data){
emit newPackedTrx(data); // emit 發(fā)送signal給newPackedTrx B:稍后重點講
func(QString("PACKED: %1 to %2.").arg(accountName).arg(super_account));// 打印日志
});
// 以下部分與賬戶轉(zhuǎn)賬接口一致,后續(xù)內(nèi)容均同上冕臭。
push.transferToken(accountName, super_account, QString("0.0001 %1").arg(token_name));
}
}
當Packer開始run的時候腺晾,它是一個無線循環(huán),直到灌滿trxPool為止辜贵,而其中悯蝉,我們注意觀察,這一connect翻譯過來就是:我先注冊一個signals trxPacked在這托慨,等待某處代碼將該信號發(fā)射鼻由,會被這里捕捉到,將它傳入回調(diào)函數(shù)厚棵,就是這個lambda回調(diào)函數(shù)的參數(shù)data中蕉世,這個lambda回調(diào)函數(shù)我們先放一放,來講這個signals trxPacked:
signals 對應的觸發(fā)是 emit
trxPacked 作為一個signals 是在PushManager::get_required_keys_returned中被發(fā)射emit的(注意這個是與上面講到的CreateAccount::get_required_keys_returned是不同的婆硬。)
QByteArray param = packPushTransactionParam();
emit trxPacked(param);
...
httpc->request(FunctionID::push_transaction, param);
這個emit發(fā)送的param是僅在push_transaction發(fā)送之前的transaction狠轻,會將這個對象傳入回調(diào)函數(shù)。下面來看一下lambda回調(diào)函數(shù)的內(nèi)部彬犯,獲取到transaction數(shù)據(jù)對象以后向楼,會將該對象再次emit到一個signals newPackedTrx查吊,我們?nèi)フ乙幌逻@個signals的注冊位置:MainWindow::enablePacker,就是上面展示過的代碼蜜自,我注釋為“A:稍后重點講”菩貌,因此相同的原理,這個data又被傳入了incomingPackedTrxs函數(shù)重荠,最終被打包進packedTransactions集合中箭阶。
關于QT的signals emit slot connect 的具體語法介紹的內(nèi)容可以查看這篇文章我們沒有QT開發(fā)的需求,所以沒必要在此過多介紹語法內(nèi)容戈鲁,只需要捋清楚業(yè)務邏輯即可仇参。
packedTransactions的內(nèi)容是屬于TransactionPool的,它會在TransactionPool被啟動時(也就是start按鈕被按下時)使用婆殿,而這個對象是在prepare階段被儲存诈乒。(據(jù)說這個時間只有5分鐘,機器性能不太好的不要將trxPool設置地太高婆芦,否則執(zhí)行不完怕磨,打包好的packedTransactions并未做持久化,就會消失掉消约,最終導致測試結果失真)
on_pushButtonRun_clicked
這個按鈕點擊事件的內(nèi)容看上去比較簡單肠鲫,只有一個enableTrxpool(true)是生效代碼,其他都是一些日志或粮。下面直接進入enableTrxpool函數(shù)导饲,不張貼了,直接轉(zhuǎn)到核心代碼trxpool->start(); 那么我們進入到transactionpool.cpp氯材,start對應run函數(shù)渣锦,源碼如下:
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接口,將這些“小包”推送到鏈執(zhí)行冗尤。
總結
沒想到EOSBenchTool的源碼解讀一下子搞了這么長的篇幅娄猫,我沒控制住,讀者又要吃力了生闲。其實到這里我們來總結一下媳溺,EOSBenchTool主要是使用了QT的界面系統(tǒng),同時也用到了QT的signals碍讯,emit悬蔽,connect等專有語法,不懂qt的同學看起來有些吃力捉兴。然而蝎困,拋開這些語言或者類庫的語法來講录语,我們專注于代碼邏輯,EOSBenchTool的實現(xiàn)是容易被人理解的:
- 首先禾乘,可以確定他是一個客戶端澎埠,都是通過我們前面文章介紹過很多遍的最熟悉的那些http接口的請求來與鏈交互的。
- ++接著始藕,它采用了本地內(nèi)存對象的方式來存儲我們設定好的所有的交易量的集合對象蒲稳。這個部分是可以改善的,畢竟如果測試量過大就會丟失伍派。++
- 它設計了一個“小包”的概念江耀,相對應的,我們前面打包好的“大包”诉植,我們設置了一個小包的大小祥国,可以按照小包為單位對鏈發(fā)起批量交易的請求。
eosjs方式
上面我們介紹了:
Way | Business | TPS | memo |
---|---|---|---|
cleos | 可直接使用 | 70-80 | (單節(jié)點晾腔、多節(jié)點)shell方式舌稀,python腳本 |
txn_test_gen_plugin | 不可使用 | 1500-2000 | 官方用來測試的一種方式,這個插件純粹是為了測tps而設的 |
EOSBenchTool | 可修改使用 | 200-300 | C++門檻較高且無對外封裝接口 |
通過以上總結灼擂,我們可以推論出壁查,如果有一種方式,支持:
- 有對外接口可易于調(diào)用
- 開發(fā)語言門檻較低
- 客戶端行為
- 支持打包請求
- tps能達到200-300
那么它對于業(yè)務方來講缤至,是完全可以接受并享受基于eos的區(qū)塊鏈帶來的紅利的潮罪。
下面就到了引出eosjs的時刻了康谆,eosjs是官方EOSIO組織承認的客戶端調(diào)用技術领斥,它不僅僅是對rpc協(xié)議的封裝,更多的還有大量的eos本身的特性沃暗,這些特性都可以做到在客戶端本地實現(xiàn)月洛,例如本地簽名,本地生成交易id等等孽锥,這些技術可以讓我們在業(yè)務方的客戶端角度充分挖掘需求嚼黔,自定義接口,上乘業(yè)務方惜辑,下啟公有鏈eos環(huán)境唬涧,這種目前為止最為合適的承上啟下的技術就是eosjs。
源碼位置
準備環(huán)境
eos環(huán)境盛撑,可通過腳本快速搭建:
python3 ./bios-boot-tutorial.py -k -w -b -s -c -t
繼續(xù)調(diào)用
python3 ./bios-boot-tutorial.py -l
將終端界面的輸出內(nèi)容保持鏈日志的同步輸出碎节。
源碼架構
eosjs是使用JavaScript語言,nodejs框架構成抵卫。
nodejs框架天生可以讓我們便攜地封裝導出以及依賴導入某個“組件”狮荔,監(jiān)于這種特性胎撇,我們也可以為業(yè)務方開發(fā)自己的sdk。
常用組件
- src/index.js 中的 module.exports = EOS殖氏,這是主要組件晚树,通過該組件可創(chuàng)建相應對象
- eosjs-ecc,可獲得加密工具對象雅采,該對象能夠調(diào)用所有加密相關的動作爵憎,例如簽名,私鑰公鑰等总滩。
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:過期時間纲堵,該行為如在此過期時間內(nèi)仍未執(zhí)行成功,則會被判定過期而拋棄闰渔。
- broadcast:這是一個本地行為(false)還是要廣播到遠端鏈上(ture)席函。
- verbose:是否要打印所有發(fā)生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] } } }
實例:創(chuàng)建用戶
通過以上列出的eos對象的提供的這些功能冈涧,我們可以滿足大部分業(yè)務方的需求茂附,這里展示一個創(chuàng)建用戶的代碼實例:
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])
})
}
}
打包交易
打包交易接口目前我還未封裝完畢,這篇文章更適合作為學習研究而不是代碼段粘貼督弓,因此對于打包交易的功能营曼,研究好以上內(nèi)容的朋友可以有自己的想法,這里我簡單說一下我的實現(xiàn)思路:
每筆transaction是可以包含多個action的愚隧,在上面介紹過的插件的實現(xiàn)中蒂阱,也是它的實現(xiàn)思路。另外push_transactions接口是鏈提供的http接口狂塘,我們打包多筆transaction成一個transactions對象請求這個接口录煤,正如插件和EOSBenchTool的實現(xiàn)方式。然后中間要經(jīng)過大量的優(yōu)化荞胡,這其中較為重要的是我們的本地交易池妈踊,這個概念在EOSBenchTool中也研究過,那里的內(nèi)存對象最多存活5分鐘泪漂,而我們這里要如何設計呢廊营?是否采用內(nèi)存變量?還是引入隊列萝勤?這都是架構師的工作露筒,也是根據(jù)不同的業(yè)務場景大有所為的地方。
更新添加打包交易時序圖:
更新打包交易源碼: Templar
總結
本篇文章全面而詳細地分析了EOS中關于tps的一切手段敌卓,包括了cleos慎式,插件,EOSBenchTool,eosjs的方式瞬捕,這其中鞍历,我們仔細研究了EOSBenchTool的源碼,過程中也涉及到了qt的部分語法肪虎,對比了這幾種方式的利弊劣砍,討論了tps的計算方式,tps的現(xiàn)實意義扇救,插件的“作弊”行為刑枝,EOSBenchTool的良好思路和貢獻藐唠,eosjs的最終確型沐鼠,以及針對transaction拴曲,action等內(nèi)部元素的深入理解與研究薄腻。最后也思考了未來eos商業(yè)實現(xiàn)的架構設想:通過eosjs作為承上啟下的sdk。
參考資料
- EOS官方文檔
- EOSBenchTool源碼
- eosjs源碼
相關文章和視頻推薦
圓方圓學院匯集大批區(qū)塊鏈名師暗赶,打造精品的區(qū)塊鏈技術課程晌纫。 在各大平臺都長期有優(yōu)質(zhì)免費公開課剂癌,歡迎報名收看锌雀。