【劉文彬】 Debug EOS:nodeos + mongo_db_plugin

圓方圓區塊鏈發表於2018-12-07

原文連結:醒者呆的部落格園,www.cnblogs.com/Evsward/p/s…

上文書說到區塊鏈的儲存方式,並結合了EOSIO進行分析,其中也提到了使用CLion除錯EOS的方法。本文將繼續深入細緻地展開對載入了mongo_db_plugin的nodeos的除錯過程以及心得。

關鍵字:原始碼分析,Debug EOS,nodeos,mongo_db_plugin,CLion,C++,boost::asio::signal_set,queue

本文涉及的環境:clang-6.0, clang++-6.0, GDB Debugger, make 4.1, mongodb-linux-x86_64-3.6.3, boost 1.67.0

除錯EOS: nodeos

關於EOS的除錯環境的搭建這裡不再贅述了,下文開始針對nodeos程式進行除錯。

(一)CMakeList.txt

nodeos開始執行前,要先使用專案的總CmakeList.txt配置,這裡我配置了boost庫的位置,如果你配置了boost的環境變數可以跳過這裡。

 set( BOOST_ROOT "/home/evsward/opt/boost")
複製程式碼
  • 這個檔案中有很多的set語句,這些語句都是開關,或者路徑,或者全域性變數,是配置各個子CMakeList.txt而用的。
  • include 語句是為runtime引入相關依賴庫。
  • add_subdirectory語句設定了子目錄程式。
  • install語句是將相關命令安裝到指定位置以供runtime後續使用。

總的CMakeList檔案介紹完了,下面會執行到nodeos目錄下的CMakeList.txt檔案:

  • add_executable( nodeos main.cpp )語句設定了nodeos程式執行入口。
  • set, configure_file, include, install 等都是為runtime準備的環境相關的。
  • 重點語句target_link_libraries,這裡定義了鏈runtime環境需要啟動的plugin。(注意記住這個順序)

(二)static register plugin

我們開啟每一個plugin的cpp檔案,會發現有一個static的register方法的呼叫。這裡會首先執行按上面plugin定義的順序中第一個login_plugin,它的static語句如下:

static appbase::abstract_plugin& _login_plugin = app().register_plugin<login_plugin>();
複製程式碼

執行此語句時,會先執行app(),這是application單例方法。

(三)application

  • application是nodeos的執行者,上面呼叫的app函式:
application& application::instance() {
   static application _app;
   return _app;
}
application& app() { return application::instance(); }
複製程式碼
application與plugin擁有相同的實現函式,而由於它作為執行者、統籌者的存在,它會安排呼叫所有plugin,例如set_program_options。
複製程式碼
  • 執行app()以後獲取到了application的例項,然後呼叫了register_plugin函式,通過模板類(泛型類)攜帶了login_plugin的型別。register_plugin函式是模板函式,定義在application.hpp檔案中。

  • application.hpp 中定義了私有的記憶體變數

      map<string, std::unique_ptr<abstract_plugin>> plugins;
    複製程式碼
  • abstract_plugin是所有plugin的基類,它定義了虛擬函式,需要繼承它的子類去實現。他們與application的關係是:

      abstract_plugin=>plugin(對基類的虛擬函式進一步使用,由application定義管理)=>各個plugin
    複製程式碼
template<typename Plugin>
auto& register_plugin() {
    auto existing = find_plugin<Plugin>(); // 從plugins尋找該plugin是否已註冊。
    if(existing)
       return *existing; // 如果已註冊了直接返回plugin的例項

    auto plug = new Plugin(); // 建立該未註冊plugin的例項
    plugins[plug->name()].reset(plug); // 先插入到上面定義的記憶體變數plugins
    plug->register_dependencies();// 註冊該plugin的依賴plugins,每個plugin內部都會呼叫APPBASE_PLUGIN_REQUIRES((chain_plugin))來註冊自己依賴的別的plugin。
    return *plug; // 返回plugin的例項
 }
複製程式碼

(四)main.cpp->main()

在編譯runtime環境結束以後,進入入口函式main(),

int main(int argc, char** argv)
複製程式碼

main函式的引數就是呼叫命令nodeos的通過--加入的引數,我們可以通過nodeos的Edit Configuration來調整。其中argc是個數,argv是引數的值,是一個陣列型別。如下圖:

【劉文彬】 Debug EOS:nodeos + mongo_db_plugin

我們接著來看main函式,它的函式體是通過app()對application單例進行的設定,包括版本號、data路徑、config路徑,然後是對於application例項內部方法的呼叫:

  • initialize<chain_plugin, http_plugin, net_plugin, producer_plugin>
  • startup()
  • exec()

main函式執行了內部函式initialize_logging()還通過ilog列印了日誌,輸出了版本號以及eosio root路徑地址。

由於main函式是入口函式,上面也介紹了它主要是對application例項的使用,以及一些異常處理等,接下來會逐一進行debug跟蹤分析。

(五)initialize plugin

這個初始化函式是一個模板函式,模板類引數是plugin基類,在main函式呼叫該函式時傳入了基本的外掛依賴(這些是不需要我們在config中配置的,是鏈啟動的基礎外掛):chain_plugin, http_plugin, net_plugin, producer_plugin。下面來看initialize函式在application標頭檔案中的宣告:

/**
  * @brief 檢視 --plugin(存在於命令列或者config配置檔案中)呼叫這些plugin的 initialize方法。
  * 
  * @tparam Plugin plugin的列表用來初始化,即使在config中沒有配置的但被其他plugin所依賴的plugin,都可以統一使用該模板類沒有影響。
  * @return true:plugin初始化完成,false:出現異常。
*/
template<typename... Plugin>
bool initialize(int argc, char** argv) {
    return initialize_impl(argc, argv, {find_plugin<Plugin>()...}); // ...是可變引數的語法,上面通過main函式的呼叫,我們傳入了多個plugin。
}
複製程式碼

實現類initialize_impl的內容較多,不貼上原始碼,直接總結一下:

(1)set_program_options()函式

application.cpp檔案中的set_program_options()函式是用來生成初始化的config.ini檔案內容以及nodeos命令列--help的輸出內容。
複製程式碼

該函式首先遍歷外掛列表,呼叫每個外掛都會實現的plugin基類的虛擬函式set_program_options(options_description& cli, options_description& cfg),例如下面就是mongo_db_plugin的實現:

void mongo_db_plugin::set_program_options(options_description& cli, options_description& cfg)
{
   cfg.add_options()
         ("mongodb-queue-size,q", bpo::value<uint32_t>()->default_value(256),
         "The target queue size between nodeos and MongoDB plugin thread.")
         ("mongodb-wipe", bpo::bool_switch()->default_value(false),
         "Required with --replay-blockchain, --hard-replay-blockchain, or --delete-all-blocks to wipe mongo db."
         "This option required to prevent accidental wipe of mongo db.")
         ("mongodb-block-start", bpo::value<uint32_t>()->default_value(0),
         "If specified then only abi data pushed to mongodb until specified block is reached.")
         ("mongodb-uri,m", bpo::value<std::string>(),
         "MongoDB URI connection string, see: https://docs.mongodb.com/master/reference/connection-string/."
               " If not specified then plugin is disabled. Default database 'EOS' is used if not specified in URI."
               " Example: mongodb://127.0.0.1:27017/EOS")
         ;
}
複製程式碼

通過呼叫mongo_db_plugin的這個方法,就可以拼湊到config.ini檔案中關於mongo_db_plugin的部分,因為這個外掛只有對於config.ini配置檔案的配置,沒有對於命令列的內容,我們可以去檢視chain_plugin的實現,它會同時有配置檔案和命令列兩個方面的內容設定,原始碼較長請自行檢視。

配置的物件options_description_easy_init是一個靈活的結構。可以表示:一個配置項,一個配置的值;一個配置項,一個配置的值,一個註釋或者描述;一個配置項,一個註釋或者描述。這些多種組合,我們也可以直接去檢視自己的config.ini的每一個配置項去對應。

那麼是如何拼湊所有的外掛配置內容呢?
複製程式碼

application.cpp檔案中的set_program_options()函式的函式體中使用了application的兩個類變數來儲存這些引數:

  • _app_options:用於接收來自於命令列和config.ini兩個引數來源的引數。
  • _cfg_options:僅儲存來自於config.ini配置檔案的引數。

外掛遍歷結束後,我們已經有了所有外掛的config.ini配置內容以及命令列提示配置內容,下面要從巨集觀角度去配置一些屬於application的配置項,config.ini中加入了plugins的配置,通過這個配置我們可以手動指定要啟動的外掛,例如mongo_db_plugin就是需要手動配置的。接著,要配置命令列的引數內容,包括help, version, print-default-config, data-dir, config-dir, config, logconf。將他們追加儲存到上面那兩個類變數中。

到這裡,application.cpp檔案中的set_program_options()函式的工作就完成了。

上面提到的_app_options和_cfg_options仍就是傻傻分不清楚,他們的用意到底是什麼呢?
複製程式碼

簡單來理解就是,命令列能夠做所有配置檔案config.ini中的配置的工作,同時命令列還有專屬於自己的help, version, print-default-config, data-dir, config-dir, config, logconf配置。這樣就好理解了,config.ini是命令列配置的子集,命令列配置是全集。

(2)app全域性引數的檢測與合併

我們回到initialize_impl,目前我們已經擁有了兩套預設配置引數,這裡直接使用全集_app_options配置,我們先接收來自於命令列的引數,將以它為優先順序高的方式與_app_options配置合併:

bpo::variables_map options;
bpo::store(bpo::parse_command_line(argc, argv, my->_app_options), options);
複製程式碼

(3)app全域性引數配置項生效與響應

拿到合併後的配置物件options,依次針對配置項的內容進行響應:

  • help:直接輸出_app_options配置項的全部內容。
  • version:輸出application例項的類成員_version的值。
  • print-default-config:與_app_options無關,重新去每個plugin找配置,然後會基於_cfg_options生成一份預設的config配置列印到終端介面。
  • data-dir:是設定data目錄的命令儲存至application的類成員_data_dir,沒有響應的輸出。
  • config-dir:設定config路徑,儲存在類成員_config_dir中。config和data的路徑結構如下:
evsward@evsward-TM1701:~/.local/share/eosio/nodeos$ tree
.
├── config
│   └── config.ini
└── data
    ├── blocks
    │   ├── blocks.index
    │   ├── blocks.log
    │   └── reversible
    │       ├── shared_memory.bin
    │       └── shared_memory.meta
    └── state
        ├── forkdb.dat
        ├── shared_memory.bin
        └── shared_memory.meta

5 directories, 8 files
複製程式碼
  • logconf:預設是logging.json,放置在config目錄下面,可自定義設定,儲存在類成員_logging_conf中。
  • config:指定配置檔案的名字,預設是config.ini。如果發現在config目錄下找到config.ini檔案,則按照該檔案的配置載入。
bpo::store(bpo::parse_config_file<char>(config_file_name.make_preferred().string().c_str(),
                                           my->_cfg_options, true), options);
複製程式碼

得到整合好本地config.ini文字配置的options物件。然後對其引數配置項進行設定。

  • plugin:讀取配置檔案中的plugin配置(多條),對於每一個plugin,要重新呼叫各自的initialize方法去按照新的配置初始化。
  • autostart_plugins:設定前面的初始化外掛chain_plugin, http_plugin, net_plugin, producer_plugin,同樣分別呼叫他們的初始化函式設定新的配置。

(4)plugin initialize

承接上文,所有相關的plugin的執行各自的initialize。這個initialize函式是abstract_plugin的虛擬函式,而該虛擬函式被plugin類所複寫:

virtual void initialize(const variables_map& options) override {
    if(_state == registered) {//如果註冊過
       _state = initialized;
       static_cast<Impl*>(this)->plugin_requires([&](auto& plug){ plug.initialize(options); });// 先執行依賴plugin的initialize方法。
       static_cast<Impl*>(this)->plugin_initialize(options);// 呼叫自身的plugin_initialize方法實現。
       //ilog( "initializing plugin ${name}", ("name",name()) );
       app().plugin_initialized(*this);// 儲存到一個initialized_plugins類成員變數中,用來按順序記錄已經開始啟動執行的plugin。
    }
    assert(_state == initialized); /// 如果外掛未註冊,則不能執行initialize方法。
}
複製程式碼

所以在plugin呼叫initialize函式的時候,是先執行的以上覆寫的plugin的虛擬函式。我們這裡先設定幾個要跟蹤的plugin為目標吧,否則plugin太多,望山止步。

目標:主要研究mongo_db_plugin,以及基礎plugin(chain_plugin, http_plugin, net_plugin, producer_plugin),路線是研究主plugin,若有額外的依賴plugin,看情況控制研究的深淺程度。
複製程式碼

(5)eosio::mongo_db_plugin::plugin_initialize

前面寫set_program_options()提到了mongo_db_plugin,這裡研究它的plugin_initialize方法。傳入的引數是結合了命令列以及本地config檔案的合併配置項,按照此配置環境。

void mongo_db_plugin::plugin_initialize(const variables_map& options)
{
   try {
      if( options.count( "mongodb-uri" )) {//查mongodb-uri的配置,config.ini中有對應的。
         ilog( "initializing mongo_db_plugin" );
         my->configured = true;//設定標誌位:已配置

         if( options.at( "replay-blockchain" ).as<bool>() || options.at( "hard-replay-blockchain" ).as<bool>() || options.at( "delete-all-blocks" ).as<bool>() ) {//捕捉是否有replay-blockchain、hard-replay-blockchain、delete-all-blocks三個動作,有的話要判斷是否要擦出mongo歷史資料。
            if( options.at( "mongodb-wipe" ).as<bool>()) {//檢查擦除項mongodb-wipe的配置
               ilog( "Wiping mongo database on startup" );
               my->wipe_database_on_startup = true;//如果設定擦除,這裡設定本地標誌位wipe_database_on_startup
            } else if( options.count( "mongodb-block-start" ) == 0 ) {//如果設定是從0開始,檢查是否要全部擦除歷史資料。
               EOS_ASSERT( false, chain::plugin_config_exception, "--mongodb-wipe required with --replay-blockchain, --hard-replay-blockchain, or --delete-all-blocks"
                                 " --mongodb-wipe will remove all EOS collections from mongodb." );
            }
         }

         if( options.count( "abi-serializer-max-time-ms") == 0 ) {//eosio::chain_plugin的配置
            EOS_ASSERT(false, chain::plugin_config_exception, "--abi-serializer-max-time-ms required as default value not appropriate for parsing full blocks");
         }
         my->abi_serializer_max_time = app().get_plugin<chain_plugin>().get_abi_serializer_max_time();

         if( options.count( "mongodb-queue-size" )) {// queue大小
            my->queue_size = options.at( "mongodb-queue-size" ).as<uint32_t>();
         }
         if( options.count( "mongodb-block-start" )) {// mongo對應的開始區塊號
            my->start_block_num = options.at( "mongodb-block-start" ).as<uint32_t>();
         }
         if( my->start_block_num == 0 ) {
            my->start_block_reached = true;
         }

         std::string uri_str = options.at( "mongodb-uri" ).as<std::string>();
         ilog( "connecting to ${u}", ("u", uri_str));
         mongocxx::uri uri = mongocxx::uri{uri_str};
         my->db_name = uri.database();
         if( my->db_name.empty())
            my->db_name = "EOS";// 預設起的庫名字為EOS,如果在mongodb-uri有配置的話就使用配置的名字。
         my->mongo_conn = mongocxx::client{uri};// 客戶端連線到mongod

         // controller中拉取得訊號,在init函式中註冊訊號機制,始終監聽鏈上訊號,作出反應。
         chain_plugin* chain_plug = app().find_plugin<chain_plugin>();//檢查chain_plugin是否載入,chain_plugin是必要依賴,下面我們要使用chain的資料。
         EOS_ASSERT( chain_plug, chain::missing_chain_plugin_exception, ""  );
         auto& chain = chain_plug->chain();// 獲得chain例項
         my->chain_id.emplace( chain.get_chain_id());

         // accepted_block_connection對應了chain的signal,是boost提供的一種訊號槽機制,這種connection物件有四個,見當前原始碼的下方展示。
         my->accepted_block_connection.emplace( chain.accepted_block.connect( [&]( const chain::block_state_ptr& bs ) {// 建立connect,每當chain有accepted_block訊號(這些訊號是定義在controller.hpp中,稍後會介紹),即呼叫下面的函式。
            my->accepted_block( bs );// accepted_block認同block資訊
         } ));
         my->irreversible_block_connection.emplace(//含義同上
               chain.irreversible_block.connect( [&]( const chain::block_state_ptr& bs ) {
                  my->applied_irreversible_block( bs );// applied_irreversible_block,應用不可逆區塊
               } ));
         my->accepted_transaction_connection.emplace(//含義同上
               chain.accepted_transaction.connect( [&]( const chain::transaction_metadata_ptr& t ) {
                  my->accepted_transaction( t );// accepted_transaction認同交易
               } ));
         my->applied_transaction_connection.emplace(//含義同上
               chain.applied_transaction.connect( [&]( const chain::transaction_trace_ptr& t ) {
                  my->applied_transaction( t );// applied_transaction,應用交易
               } ));

         if( my->wipe_database_on_startup ) {
            my->wipe_database();// 擦除mongo歷史資料
         }
         my->init();//初始化函式
      } else {
         wlog( "eosio::mongo_db_plugin configured, but no --mongodb-uri specified." );
         wlog( "mongo_db_plugin disabled." );
      }
   } FC_LOG_AND_RETHROW()
}
複製程式碼

四個connection物件的宣告如下:

fc::optional<boost::signals2::scoped_connection> accepted_block_connection;
fc::optional<boost::signals2::scoped_connection> irreversible_block_connection;
fc::optional<boost::signals2::scoped_connection> accepted_transaction_connection;
fc::optional<boost::signals2::scoped_connection> applied_transaction_connection;
複製程式碼

queue

這段程式碼中涉及到四個函式分別是accepted_block,applied_irreversible_block,accepted_transaction,applied_transaction,他們都對應著對queue的操作,mongo_db_plugin_impl類成員定義了一下幾種queue:

std::deque<chain::transaction_metadata_ptr> transaction_metadata_queue;
std::deque<chain::transaction_metadata_ptr> transaction_metadata_process_queue;
std::deque<chain::transaction_trace_ptr> transaction_trace_queue;
std::deque<chain::transaction_trace_ptr> transaction_trace_process_queue;
std::deque<chain::block_state_ptr> block_state_queue;
std::deque<chain::block_state_ptr> block_state_process_queue;
std::deque<chain::block_state_ptr> irreversible_block_state_queue;
std::deque<chain::block_state_ptr> irreversible_block_state_process_queue;
複製程式碼

queue是mongo_db_plugin自己定義的:

/**
 * 模板類Queue,可以匹配以上我們定義的多個queue型別。
 * 模板類Entry,可以匹配block_state_ptr以及transaction_trace_ptr作為被儲存實體型別。
 */
template<typename Queue, typename Entry>
void queue(boost::mutex& mtx, boost::condition_variable& condition, Queue& queue, const Entry& e, size_t queue_size) {
   int sleep_time = 100;//預設執行緒睡眠時間
   size_t last_queue_size = 0;
   boost::mutex::scoped_lock lock(mtx);//mutex鎖機制
   if (queue.size() > queue_size) {//如果超過了我們設定的queue大小,則採取如下措施。
      lock.unlock();//先解鎖
      condition.notify_one();// 見下文對condition的介紹
      if (last_queue_size < queue.size()) {//說明queue的增加速度大於我們程式消費處理的速度
         sleep_time += 100;//增加睡眠時間
      } else {
         sleep_time -= 100;//說明queue的增加速度小於我們消費的速度,就要減少睡眠時間,儘快更新last_queue_size的值。
         if (sleep_time < 0) sleep_time = 100;
      }
      last_queue_size = queue.size();
      boost::this_thread::sleep_for(boost::chrono::milliseconds(sleep_time));//執行緒睡眠,睡眠的時間按照上面的機制定奪。
      lock.lock();//上鎖
   }
   queue.emplace_back(e);//生效部分:插入到佇列中去。
   lock.unlock();//解鎖
   condition.notify_one();
}
複製程式碼

mongo_db_plugin_impl::wipe_database()

真正執行擦除mongo歷史資料的函式,這個動作是由我們配置mongodb-wipe引數來指定。擦除的函式體如下:

void mongo_db_plugin_impl::wipe_database() {
   ilog("mongo db wipe_database");

   // 定義的六張mongo的表型別,通過客戶端連線獲取到六張表的許可權。
   auto block_states = mongo_conn[db_name][block_states_col];
   auto blocks = mongo_conn[db_name][blocks_col];
   auto trans = mongo_conn[db_name][trans_col];
   auto trans_traces = mongo_conn[db_name][trans_traces_col];
   auto actions = mongo_conn[db_name][actions_col];
   accounts = mongo_conn[db_name][accounts_col];

   // 分別刪除,執行drop動作。
   block_states.drop();
   blocks.drop();
   trans.drop();
   trans_traces.drop();
   actions.drop();
   accounts.drop();
}
複製程式碼

mongo_db_plugin_impl::init()

原始碼較多不貼上,上面wipe_database函式,我們刪除了六張表,在init函式中,我們要對應的建立這六張表。表名初始化:

const std::string mongo_db_plugin_impl::block_states_col = "block_states";
const std::string mongo_db_plugin_impl::blocks_col = "blocks";
const std::string mongo_db_plugin_impl::trans_col = "transactions";
const std::string mongo_db_plugin_impl::trans_traces_col = "transaction_traces";
const std::string mongo_db_plugin_impl::actions_col = "actions";
const std::string mongo_db_plugin_impl::accounts_col = "accounts";
複製程式碼

這就是劉張表對應的名字。這六張表在初始化建立時是一個整體操作,也就是說互為依賴關係,accounts表先建立,通過

accounts = mongo_conn[db_name][accounts_col];
複製程式碼

即可建立成功accounts表,其他表亦然,後面不贅述。表資料是由make_document進行組裝的。首先我們向accounts表插入一條資料,結構是name為eosio,createAt是當前時間。

  • chain::config::system_account_name ).to_string()
  • std::chrono::duration_caststd::chrono::milliseconds(std::chrono::microseconds{fc::time_point::now().time_since_epoch().count()});

通過insert_one方法將該條資料插入accounts表中。

接下來通過create_index方法對五張表建立索引,注意transaction_traces是沒有索引的,init操作時不涉及transaction_traces表。

auto blocks = mongo_conn[db_name][blocks_col]; // Blocks
blocks.create_index( bsoncxx::from_json( R"xxx({ "block_num" : 1 })xxx" ));
blocks.create_index( bsoncxx::from_json( R"xxx({ "block_id" : 1 })xxx" ));// block建立了兩個索引

auto block_stats = mongo_conn[db_name][block_states_col];
block_stats.create_index( bsoncxx::from_json( R"xxx({ "block_num" : 1 })xxx" ));
block_stats.create_index( bsoncxx::from_json( R"xxx({ "block_id" : 1 })xxx" ));// block_stats建立了兩個索引

// accounts indexes
accounts.create_index( bsoncxx::from_json( R"xxx({ "name" : 1 })xxx" ));

// transactions indexes
auto trans = mongo_conn[db_name][trans_col]; // Transactions
trans.create_index( bsoncxx::from_json( R"xxx({ "trx_id" : 1 })xxx" ));

auto actions = mongo_conn[db_name][actions_col];
actions.create_index( bsoncxx::from_json( R"xxx({ "trx_id" : 1 })xxx" ));
複製程式碼

初始化準備就完成了,接下來要建立執行緒監聽出塊訊息,同步到mongo資料庫中來。

ilog("starting db plugin thread");

consume_thread = boost::thread([this] { consume_blocks(); });

startup = false;// 結束,呼叫解構函式,關閉mongo_db_plugin:設定標誌位done = true;
複製程式碼

(6)mongo_db_plugin_impl::consume_blocks()

上面init函式執行到最後時,開啟了一個執行緒,執行的是consume_blocks()函式,如字面含義這是消費區塊的函式。這個函式是一個無限迴圈,保持執行緒的存活,監聽queue的資料隨時消費同步到mongo資料庫中去,而queue的資料的是由上面plugin_initialize函式中的connect訊號槽機制連線chain的出塊訊號往queue裡面插入/同步鏈上資料。

condition

無線迴圈第一部分就是對condition.wait(lock)的操作,condition在上面queue的原始碼中有一個notify_one()的呼叫,實際上就是與wait相互應的操作。

boost::mutex::scoped_lock lock(mtx);
while ( transaction_metadata_queue.empty() &&
         transaction_trace_queue.empty() &&
         block_state_queue.empty() &&
         irreversible_block_state_queue.empty() &&
         !done ) {
    condition.wait(lock);
}
複製程式碼
消費區塊佔用了一個執行緒,這個執行緒在上面四個queue是空的時候並且done也沒有完成是flase的時候,該執行緒會通過condition來阻塞執行緒,引數是mutex的一個鎖。
複製程式碼

condition.notify_one()會重新喚起這個阻塞的執行緒,而在mongo_db_plugin中,condition.notify_one()出現了3次:

  • queue模板型別,有了新的資料插入的時候。
  • 當queue模板型別的佇列超過設定值的時候,要主動喚起consume_block開啟消費執行緒加速消費(上面介紹queue的時候也談到了佇列大小超限時會增加queue插入的睡眠等待時間,這兩方面相當於針對中間佇列對兩邊進行開源節流,從而控制了佇列的大小)
  • ~mongo_db_plugin_impl()解構函式中
mongo_db_plugin_impl::~mongo_db_plugin_impl() {
   if (!startup) {//標誌位,上面init函式結尾有這個值的賦值。
      try {
         ilog( "mongo_db_plugin shutdown in process please be patient this can take a few minutes" );
         done = true;//設定標誌位done,consume_block()會使用到。
         condition.notify_one();// 喚醒consume_thread執行緒繼續消費掉queue中殘餘資料。

         consume_thread.join();// 掛起主執行緒,等待consume_thread執行緒執行完畢再喚起主執行緒。
      } catch( std::exception& e ) {
         elog( "Exception on mongo_db_plugin shutdown of consume thread: ${e}", ("e", e.what()));
      }
   }
}
複製程式碼

process_queue準備

我們要將鏈上的資料同步至mongo,是通過上面判斷是否為空的那四個queue來做,為了增加消費效率,進入consume_block函式以後,要先將資料move匯入到一個process_queue中來慢慢處理,相當於一箇中轉站。

size_t transaction_metadata_size = transaction_metadata_queue.size();
if (transaction_metadata_size > 0) {
    transaction_metadata_process_queue = move(transaction_metadata_queue);
    transaction_metadata_queue.clear();
}
size_t transaction_trace_size = transaction_trace_queue.size();
if (transaction_trace_size > 0) {
    transaction_trace_process_queue = move(transaction_trace_queue);
    transaction_trace_queue.clear();
}
size_t block_state_size = block_state_queue.size();
if (block_state_size > 0) {
    block_state_process_queue = move(block_state_queue);
    block_state_queue.clear();
}
size_t irreversible_block_size = irreversible_block_state_queue.size();
if (irreversible_block_size > 0) {
    irreversible_block_state_process_queue = move(irreversible_block_state_queue);
    irreversible_block_state_queue.clear();
}
複製程式碼

佇列大小報警器

接下來是一個針對四個源佇列的大小進行一個監控,當任意超過限額的75%時,會觸發報警,列印到控制檯。

分發到具體執行函式消費佇列

接下來,就是將上面的四個中轉的process_queue的資料分別分發到不同的函式(對應下面四個_process函式)中去消費處理。最後每個中轉佇列處理一條,就pop出去一條,都處理結束以後,會再次判斷四個源佇列的大小是否為空,都消費完了,同時也得有解構函式的done標誌位為true,才會中斷consume_thread執行緒的consume_block()的無線迴圈。

1. mongo_db_plugin_impl::_process_accepted_transaction() 執行接收交易, 需要start_block_reached標識位為true。原始碼較長不貼上,語言介紹一下,該函式的主要工作是獲得mongo的連線以及庫表物件,同時解析傳入的const chain::transaction_metadata_ptr& t 物件,該物件的路線是:

chain=>signal=>mongo_db_plugin connect signal=>queue=>process_queue=>遍歷出一條資料即是t
複製程式碼

獲得這個物件以後,也準備好了mongo資料庫的連線庫表物件,剩下的工作就是從t解析匯入mongo庫表了。

mongo作為列儲存的nosql檔案資料庫,這裡只接收document型別
複製程式碼

這裡建立了一個它的物件act_doc,解析過程:

  • 鏈資料物件的解析
const auto trx_id = t->id;
const auto trx_id_str = trx_id.str();
const auto& trx = t->trx;
const chain::transaction_header& trx_header = trx;
複製程式碼
  • mongo資料庫儲存結構的定義,值資料的傳入,通過process_action函式進行處理,
act_doc.append( kvp( "action_num", b_int32{act_num} ), kvp( "trx_id", trx_id_str ));
act_doc.append( kvp( "cfa", b_bool{cfa} ));
act_doc.append( kvp( "account", act.account.to_string()));
act_doc.append( kvp( "name", act.name.to_string()));
act_doc.append( kvp( "authorization", [&act]( bsoncxx::builder::basic::sub_array subarr ) {
    for( const auto& auth : act.authorization ) {
        subarr.append( [&auth]( bsoncxx::builder::basic::sub_document subdoc ) {
          subdoc.append( kvp( "actor", auth.actor.to_string()),
                 kvp( "permission", auth.permission.to_string()));
        } );
    }
} ));
複製程式碼

process_action函式處理的是action資料的匹配,而如果action涉及到新賬戶的建立,這部分要在process_action函式中繼續通過update_account函式進行處理。update_account函式只會過濾由system合約執行的newaccount動作,system合約預設是由chain::config::system_account_name(就是eosio)來建立的。所以過濾後的action的結構如下:

field value
account eosio
name newaccount

然後會同步在mongo的accounts表中新增一條記錄,要有當時的新增時間createdAt。新增之前,要根據這個使用者名稱去mongo中查詢,通過函式find_account,如果查詢到了則update,未查到就insert。

auto find_account(mongocxx::collection& accounts, const account_name& name) {
    using bsoncxx::builder::basic::make_document;
    using bsoncxx::builder::basic::kvp;
    return accounts.find_one( make_document( kvp( "name", name.to_string())));
}
複製程式碼

接著,是transaction表的資料插入,這個工作是對trans_doc文字型別變數的設定:

  • trx_id設定
  • irreversible設定
  • transaction_header設定
  • signing_keys設定
  • actions設定:遍歷源trx的actions,每一項去呼叫上面定義的process_action函式執行action資料的處理髮到action_array變數中,賦給actions。
  • context_free_actions,與action的處理過程差不多。
  • transaction_extensions設定
  • signatures
  • context_free_data
  • createdAt

整合完畢,將trans_doc插入到transaction表中去。整個_process_accepted_transaction執行完畢,其中涉及到了transaction, action, accounts三張表的內容的增加與修改。

2. mongo_db_plugin_impl::_process_applied_transaction 執行應用交易,需要start_block_reached標識位為true。這個函式是對mongo中transaction_traces表的操作。同樣的,是通過一個文字型別變數trans_traces_doc操作。這個函式的引數傳入是transaction_trace_ptr型別的物件t(對應的上面_process_accepted_transaction接收的是transaction_metadata_ptr型別的)

abi_serializer::to_variant, 轉化成abi格式的json資料。
abi_serializer::from_variant, 通過abi格式的json資料轉換出來對應的物件資料。
複製程式碼

3. mongo_db_plugin_impl::_process_accepted_block

這裡先要從process_accepted_block函式進入,上面的下劃線_開頭的函式都是又沒有下劃線的相同名字的函式呼叫的,只是他們除了呼叫以外都是一些異常的處理,日誌的輸出工作。而process_accepted_block函式有了簡單的邏輯,就是根據標誌位start_block_reached作出了處理。前面我們介紹plugin_initialize函式的時候,通過配置檔案的配置項"mongodb-block-start",我們設定了全域性變數start_block_num作為標誌位。這裡面就是對於這個引數值的一個判斷,如果達到了這個設定的起始區塊,則設定全域性變數標誌位start_block_reached為true,那麼就可以進入到_process_accepted_block函式進行處理了。
複製程式碼

這個函式是接收區塊處理。傳入的引數為block_state_ptr型別的物件bs。它的路線與上面介紹過的其他函式的引數t是相同的,只是類結構不同,存的資料不同。該函式涉及到mongo的兩張表,一個是block_states,另一個是blocks。我們分別來研究。

  • block_state_doc
 mongocxx::options::update update_opts{};
   update_opts.upsert( true );// upsert模式為true,代表update操作如果未找到物件則新增一條資料。
   
   auto block_states = mongo_conn[db_name][block_states_col];
   auto block_state_doc = bsoncxx::builder::basic::document{};
   // 資料結構對映
   block_state_doc.append(kvp( "block_num", b_int32{static_cast<int32_t>(block_num)} ),
                          kvp( "block_id", block_id_str ),
                          kvp( "validated", b_bool{bs->validated} ),
                          kvp( "in_current_chain", b_bool{bs->in_current_chain} ));

   auto json = fc::json::to_string( bhs );
   try {
      const auto& value = bsoncxx::from_json( json );
      block_state_doc.append( kvp( "block_header_state", value ));// 追加block_header_state的值
   } catch( bsoncxx::exception& ) {
      try {
         json = fc::prune_invalid_utf8(json);
         const auto& value = bsoncxx::from_json( json );
         block_state_doc.append( kvp( "block_header_state", value ));
         block_state_doc.append( kvp( "non-utf8-purged", b_bool{true}));
      } catch( bsoncxx::exception& e ) {
         elog( "Unable to convert block_header_state JSON to MongoDB JSON: ${e}", ("e", e.what()));
         elog( "  JSON: ${j}", ("j", json));
      }
   }
   block_state_doc.append(kvp( "createdAt", b_date{now} ));// 追加createdAt的值

   try {
      // update_one, 沒有查詢到相關資料則直接新增一條
      if( !block_states.update_one( make_document( kvp( "block_id", block_id_str )),
                                    make_document( kvp( "$set", block_state_doc.view())), update_opts )) {
         EOS_ASSERT( false, chain::mongo_db_insert_fail, "Failed to insert block_state ${bid}", ("bid", block_id));
      }
   } catch(...) {
      handle_mongo_exception("block_states insert: " + json, __LINE__);
   }
複製程式碼
  • block_doc
 auto blocks = mongo_conn[db_name][blocks_col];
   auto block_doc = bsoncxx::builder::basic::document{};
   // 資料結構對映
   block_doc.append(kvp( "block_num", b_int32{static_cast<int32_t>(block_num)} ),
                    kvp( "block_id", block_id_str ),
                    kvp( "irreversible", b_bool{false} ));

   auto v = to_variant_with_abi( *bs->block, accounts, abi_serializer_max_time );// 轉化為abi格式的資料儲存。
   json = fc::json::to_string( v );
   try {
      const auto& value = bsoncxx::from_json( json );
      block_doc.append( kvp( "block", value ));// 追加block的值,為json
   } catch( bsoncxx::exception& ) {
      try {
         json = fc::prune_invalid_utf8(json);
         const auto& value = bsoncxx::from_json( json );
         block_doc.append( kvp( "block", value ));
         block_doc.append( kvp( "non-utf8-purged", b_bool{true}));
      } catch( bsoncxx::exception& e ) {
         elog( "Unable to convert block JSON to MongoDB JSON: ${e}", ("e", e.what()));
         elog( "  JSON: ${j}", ("j", json));
      }
   }
   block_doc.append(kvp( "createdAt", b_date{now} ));// 追加createdAt的值

   try {
      // update_one, 沒有查詢到相關資料則直接新增一條
      if( !blocks.update_one( make_document( kvp( "block_id", block_id_str )),
                              make_document( kvp( "$set", block_doc.view())), update_opts )) {
         EOS_ASSERT( false, chain::mongo_db_insert_fail, "Failed to insert block ${bid}", ("bid", block_id));
      }
   } catch(...) {
      handle_mongo_exception("blocks insert: " + json, __LINE__);
   }
複製程式碼

4. mongo_db_plugin_impl::_process_irreversible_block 執行不可逆區塊,,需要start_block_reached標識位為true。涉及mongo的兩張表:blocks表和transactions表。

// 創世塊區塊號為1,沒有訊號到accepted_block處理。
if (block_num < 2) return;
複製程式碼

傳入的引數,思想與上面的幾個函式設計是相同的,它的型別與上面的_process_accepted_block函式相同,是block_state_ptr型別的物件bs。從bs中獲取到區塊,首先會通過find_block去mongo中查詢,如果有的話就不再處理。

  • blocks 資料對映更新插入。由於它是在_process_accepted_block函式的後面執行,所以語句update_opts.upsert( true );在這裡的update_one也是有效的。

bulk: 是一系列操作的集合。

mongocxx::options::bulk_write bulk_opts;
bulk_opts.ordered(false);// false說明可以並行,所有操作互不影響。若為true,則順序執行,一旦遇錯直接中止,後面的操作不會被執行到。
auto bulk = trans.create_bulk_write(bulk_opts);//所有的操作針對的是trans物件,對應的mongo表為transactions。
複製程式碼
auto update_doc = make_document( kvp( "$set", make_document( kvp( "irreversible", b_bool{true} ),
                                                                kvp( "validated", b_bool{bs->validated} ),
                                                                kvp( "in_current_chain", b_bool{bs->in_current_chain} ),
                                                                kvp( "updatedAt", b_date{now}))));

blocks.update_one( make_document( kvp( "_id", ir_block->view()["_id"].get_oid())), update_doc.view());
複製程式碼
  • transactions transactons是一個陣列,一個block可以包含很多條transaction,因此這裡要有個迴圈來處理。對於transaction在mongo中的儲存歷史,也有對應的find_transaction去mongo中查詢,如果有的話就不再處理。
auto update_doc = make_document( kvp( "$set", make_document( kvp( "irreversible", b_bool{true} ),
                                                                      kvp( "block_id", block_id_str),
                                                                      kvp( "block_num", b_int32{static_cast<int32_t>(block_num)} ),
                                                                      kvp( "updatedAt", b_date{now}))));

 mongocxx::model::update_one update_op{ make_document(kvp("_id", ir_trans->view()["_id"].get_oid())), update_doc.view()};
複製程式碼

最後通過在transaction迴圈中設定一個標誌位transactions_in_block來確定transaction遍歷結束。

mongo_db_plugin總結

我們是通過nodeos命令的initialize函式跟蹤到mongo_db_plugin的,關於mongo_db_plugin的一切,可以總結為順序:

1. set_program_option,設定配置引數
2. plugin_initialize,初始化使plugin配置引數生效,準備mongo連線,queue機制,訊號槽監聽chain出塊action。
3. init,mongo庫表初始化,建立索引,定義了consume_thread執行緒用來消費queue區塊資料。initialize週期結束。
4. consume_block,執行緒觸發與等待策略,process_queue消費中轉策略,根據四種資料結構(即上文反覆提到的那四個結構)分發消費函式。
複製程式碼
table function insert function update
transactions accepted_trx irreversible_block(bulk)
actions accepted_trx(bulk)
block_states accepted_block
blocks accepted_block irreversible_block
transaction_traces applied_trx
accounts accepted_trx
比較特殊的一個表是accounts,它可以過濾actions內容,找到newaccount的action並儲存賬戶到表裡。這給我們以啟發,我們可以自己定義新的表來過濾自己需要的action,例如我們自己寫的智慧合約。
複製程式碼

(六)initialize_logging()

日誌系統初始化。

void initialize_logging()
{
   auto config_path = app().get_logging_conf();
   if(fc::exists(config_path))
     fc::configure_logging(config_path); //故意不去捕捉異常
   for(auto iter : fc::get_appender_map())
     iter.second->initialize(app().get_io_service());
   // 重複以上程式碼邏輯,利用boost::asio::signal\_set機制,async\_wait。
   logging_conf_loop();
}
複製程式碼

(七)startup()

void application::startup() {
   try {
      for (auto plugin : initialized_plugins)//遍歷所有已初始化的外掛,執行他們的startup函式。
         plugin->startup();
   } catch(...) {
      shutdown();//如有異常,則呼叫shutdown函式,清空容器,釋放資源。
      throw;
   }
}
複製程式碼

這裡仍舊以mongo_db_plugin為例,它的startup()是空。而對於其他plugin而言,startup都有很多工作要做,例如producer_plugin和chain_plugin都非常重要,此外涉及到重要的控制器部分controller也需要仔細研究。由於本文篇幅過長,我們重點仍舊圍繞mongo_db_plugin來介紹整個nodeos啟動的生命週期。

(八)exec()

main入口函式執行到最後一個步驟:exec函式。

void application::exec() {
   std::shared_ptr<boost::asio::signal_set> sigint_set(new boost::asio::signal_set(*io_serv, SIGINT));
   sigint_set->async_wait([sigint_set,this](const boost::system::error_code& err, int num) {
     quit();
     sigint_set->cancel();
   });

   std::shared_ptr<boost::asio::signal_set> sigterm_set(new boost::asio::signal_set(*io_serv, SIGTERM));
   sigterm_set->async_wait([sigterm_set,this](const boost::system::error_code& err, int num) {
     quit();
     sigterm_set->cancel();
   });

   std::shared_ptr<boost::asio::signal_set> sigpipe_set(new boost::asio::signal_set(*io_serv, SIGPIPE));
   sigpipe_set->async_wait([sigpipe_set,this](const boost::system::error_code& err, int num) {
     quit();
     sigpipe_set->cancel();
   });

   io_serv->run();// 與上面initialize_logging的get_io_service()獲取到的io\_serv是同一個物件

   shutdown(); /// 同步推出
}
複製程式碼

這個函式與initialize_logging的迴圈中涉及到相同的訊號機制boost::asio::signal_set。

boost::asio::signal_set

boost庫的訊號量技術。它要使用到boost::asio::io_service,這也是上面提到多次的。訊號量物件的初始化可參照前文一段程式碼,如下:

std::shared_ptr<boost::asio::signal_set> sigint_set(new boost::asio::signal_set(*io_serv, SIGINT));
複製程式碼

共享指標這裡不談了,感興趣的同學請轉到這裡。它的建構函式是傳入了一個boost::asio::io_service以及一個訊號number SIGINT。這個SIGINT的宣告為:

#define SIGINT      2   /* Interrupt (ANSI).  */
複製程式碼

這個建構函式實現了向訊號量集合中新增了一個訊號2。

接著,我要通過async_wait來使用訊號量。可以貼上上面initialize_logging函式的logging_conf_loop函式。

void logging_conf_loop()
{
   std::shared_ptr<boost::asio::signal_set> sighup_set(new boost::asio::signal_set(app().get_io_service(), SIGHUP));
   sighup_set->async_wait([sighup_set](const boost::system::error_code& err, int /*num*/) {
      if(!err)
      {
         ilog("Received HUP.  Reloading logging configuration.");
         auto config_path = app().get_logging_conf();
         if(fc::exists(config_path))
            ::detail::configure_logging(config_path);
         for(auto iter : fc::get_appender_map())
            iter.second->initialize(app().get_io_service());
         logging_conf_loop();
      }
   });
}
複製程式碼

可以直接通過sighup_set->async_wait的方式來使用。它的宣告定義是:

void (boost::system::error_code, int)) 
複製程式碼

會在所監聽的訊號觸發時呼叫函式體。當發生錯誤的時候,退出logging_conf_loop函式的遞迴呼叫。

總結

寫到這裡,我們的nodeos的命令就啟動成功了,由於篇幅限制,我們沒有仔細去研究所有依賴的plugin,以及controller的邏輯。本文重點研究了mongo_db_plugin的原始碼實現,通過該外掛,我們全面分析了nodeos命令啟動的所有流程。而對於mongo_db_plugin外掛本身的學習,我們也明白了鏈資料是如何同步到mongo裡面的。接下來,我會繼續深入分析其他相關外掛的初始化流程以及啟動流程,還有controller的邏輯細節,以及出塊邏輯等等。

參考資料

EOSIO/eos


相關文章和視訊推薦

【許曉笛】EOS 資料庫與持久化 API —— 實戰

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

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

相關文章