原文連結:醒者呆的部落格園,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是引數的值,是一個陣列型別。如下圖:
我們接著來看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
相關文章和視訊推薦
圓方圓學院彙集大批區塊鏈名師,打造精品的區塊鏈技術課程。 在各大平臺都長期有優質免費公開課,歡迎報名收看。
公開課地址:ke.qq.com/course/3451…