上文書說到區塊鏈的存儲方式,並結合了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的一切,可以總結為順序:
- set_program_option,設置配置參數
- plugin_initialize,初始化使plugin配置參數生效,准備mongo連接,queue機制,信號槽監聽chain出塊action。
- init,mongo庫表初始化,建立索引,定義了consume_thread線程用來消費queue區塊數據
initialize周期結束。 - 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