【劉文彬】【精解】EOS TPS 多維實測

原文連接:醒者呆的博客園,www.cnblogs.com/Evsward/p/e…html

本文主要研究EOS的tps表現,會從插件、cleos、EOSBenchTool以及eosjs四種方式進行分析研究。node

關鍵字:eos, tps, cleos, txn_test_gen_plugin, EOSBenchTool, qt, eosjs, C++源碼分析python

身心準備

  • 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的影響。ios

單節點環境

單節點的搭建這裏再也不贅述,直接使用腳本執行,git

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

注意參數的順序不能變。
複製代碼

執行成功之後,咱們將獲得一個擁有1000個stake帳戶(簡單理解爲已抵押完可直接投票的帳戶)的單節點eos環境,最後一個參數-X會讓當前環境不斷執行隨機轉帳操做(注意:每一筆轉帳都是一個action,一個action對應一個transaction)github

查看日誌shell

修改腳本的stepLog函數,改成:json

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

而後在終端執行:api

./bios-boot-tutorial.py -l

便可進入同步日誌輸出的界面。

1、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。

2、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
複製代碼

鏈上日誌結果:

經過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卻失敗的朋友可以在這裏找到答案。

準備

1、EOS主網環境

首先,要準備EOS主網環境,能夠經過腳本快速得到:python3 ./bios-boot-tutorial.py -k -w -b -s -c -t (不部署system合約,由於部署後沒法使用create account建立帳戶。)

2、獲取代碼,QT工具,編譯代碼

  • 源碼位置:EOSBenchTool

  • QT去官網下載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內容,以下:

關於幾個參數:

  • 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分鐘,而咱們這裏要如何設計呢?是否採用內存變量?仍是引入隊列?這都是架構師的工做,也是根據不一樣的業務場景大有所爲的地方。
複製代碼

更新添加打包交易時序圖:

更新打包交易源碼: Templar

總結

本篇文章全面而詳細地分析了EOS中關於tps的一切手段,包括了cleos,插件,EOSBenchTool,eosjs的方式,這其中,咱們仔細研究了EOSBenchTool的源碼,過程當中也涉及到了qt的部分語法,對比了這幾種方式的利弊,討論了tps的計算方式,tps的現實意義,插件的「做弊」行爲,EOSBenchTool的良好思路和貢獻,eosjs的最終確型,以及針對transaction,action等內部元素的深刻理解與研究。最後也思考了將來eos商業實現的架構設想:經過eosjs做爲承上啓下的sdk。

參考資料

  • EOS官方文檔
  • EOSBenchTool源碼
  • eosjs源碼

相關文章和視頻推薦

圓方圓學院聚集大批區塊鏈名師,打造精品的區塊鏈技術課程。 在各大平臺都長期有優質免費公開課,歡迎報名收看。

公開課地址:ke.qq.com/course/3451…

相關文章
相關標籤/搜索