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

原文鏈接: https://www.cnblogs.com/Evsward/p/eostps.html

本文主要研究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在安裝時要同時勾選安裝 QCreator 和 QT 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源碼
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章