深入浅出 NXLog (一)
轉自 http://www.jianshu.com/p/ac9f5ae4beb9
1. NXLog 簡介
nxlog 是用 C 語言寫的一個開源日志收集處理軟件,它是一個模塊化、多線程、高性能的日志管理解決方案,支持多平臺。今天我主要分析一下 nxlog 的啟動流程,基于的 code 版本是 nxlog-ce-2.8.1248。
2. NXLog 啟動流程圖
上圖是 nxlog 啟動的一個大致流程圖,大家可以先看一眼,對整個流程有個大致認識,具體的解析下面奉上。
3. NXLog 啟動詳解
下面根據啟動流程的各個步驟分別進行分析。
3.1 NXLog Init
nxlog->ctx = nx_ctx_new();nx_ctx_register_builtins(nxlog->ctx);CHECKERR(apr_thread_mutex_create(&(nxlog->mutex), APR_THREAD_MUTEX_UNNESTED, nxlog->pool));CHECKERR(apr_thread_cond_create(&(nxlog->event_cond), nxlog->pool));CHECKERR(apr_thread_cond_create(&(nxlog->worker_cond), nxlog->pool));nxlog_init 主要干了三件事情。
1. 通過 nx_ctx_new 接口創建一個configuration context。ctx 很重要,主要用來存儲 nxlog 的配置,module,jobgroups 等信息。
2. 通過 nx_ctx_register_builtins 接口綁定 IN, OUT 以及 CORE 模塊的 callback。下面以 IN 模塊為例來進行說明。
Input module 提供三種 callback。根據數據源的不同分為 linebased,dgram,binary 三種方式。linebased 代表log一行一行切分的,適用于讀取日志文件。dgram 代表log是一個包一個包來切分的,適用于接收 UDP syslog 消息。binary 代表數據源為二進制流。nxlog 的配置文件里每個 module 下面有個 InputType 選項,我們可以把它分別配置成 LineBased,Dgram,Binary 來實現相應的功能。
3. 通過 apr_thread_mutex_create,apr_thread_cond_create 創建 互斥鎖(nxlog->mutex) 和條件變量(nxlog->event_cond 和 nxlog->worker_cond)。這里我們看到 nxlog 有使用 APR(Apache Portable Runtime)的接口,在 nxlog 里很多涉及到平臺相關的代碼都是調用 APR 的接口來實現的,以此來實現跨平臺。
互斥鎖,是一種信號量,常用來防止兩個進程或線程在同一時刻訪問共享資源。條件變量(Condtion Variable)是在多線程程序中用來實現“等待” -> “喚醒”邏輯的常用方法。
NXLog 使用的是生產者消費者模式,該模式需要有一個緩沖區處于生產者和消費者之間,生產者把數據放入緩沖區,而消費者從緩沖區取出數據。這個緩沖區在 NXLog 里叫做 jobqueue,jobqueue屬于共享資源,我們需要使用 nxlog->mutex 對多個線程進行同步,防止多個線程同時訪問 jobqueue 而導致數據出錯。
既然是生產者消費者模式,那就有可能出現生產者快于消費者或者消費者快于生產者的情況。當生產者快于消費者時,我們首先想到的是 jobqueque 會不會滿,事實上 jobqueue 是用一個雙向鏈表來實現的,它只是負責把各種 event 鏈在一起,本身并不維護一塊內存空間,真正消耗內存的是 event 本身,所以理論上只要 event 能創建就沒有問題。但是如果程序經常出現這種情況,那就代表處理能力不足,影響運行效率。為了不出現這種問題,消費者這一塊 nxlog 是用線程池來實現的(在后面 Create threads 我們再細說),這也就是它號稱多線程,高性能的原因。
下面再來看看當消費者快于生產者的情況,出現這種情況后,我們就發現 jobqueue 是空的,這時我們不希望消費者忙等而消耗資源,而是希望他們睡眠,當有新的 event 產生時再喚醒消費者。這就需要上面我們說到的條件變量,消費者調用 apr_thread_cond_wait 睡眠,當生產者有消息時調用 apr_thread_cond_signal 來喚醒消費者。
3.2 NXLog parse configuration
NXLog 的配置分為兩部分,一部分通過命令行參數的方式帶進來,不過這部分配置很少,大多數配置還是通過配置文件的方式下發。
1. Command line
命令行就沒有什么好說的了, 主要是用來指定運行方式以及配置文件路徑等。
配置文件會被解析成一個config tree,nxlog 通過遞歸調用 nx_cfg_parse 接口對配置文件進行逐行(如果在行尾有反斜杠 ‘\’, 會把多行并作一行)分析,最后返回一個 cfgtree,這個 tree 中的每個節點都保存了一行配置中的指令和參數,同時它還會保存父節點,子節點以及兄弟節點的地址。
這樣就把配置文件保存在數據結構 cfgtree 中,最后在每個 module 啟動之前會調用這個 module 的 config 接口,這個接口遍歷該模塊緩存在 cfgtree 中的配置,然后使能這些配置,這樣 config file 的使命就完成了。關于 config file 的寫法以及每個 module 的配置選項有很多,在此我就不贅述,不清楚的朋友可以參考 doc/reference-manual 目錄,在 config-examples 目錄下有一些配置模板,在 en 目錄下有參考手冊 nxlog-reference-manual.pdf,這里面關于 config file 講的很詳細。
3.3 Read config cache
Config cache 里保存了 nxlog 上次采集的位置,比如文件的話它會記錄文件的名字以及最后一次的 offset,這樣當 nxlog 重啟后它就知道上次采集到了那里,然后沿著上一次采集的位置繼續采集,這樣就避免了重復采集。針對日志采集,我們比較忌諱的有兩點,一是丟數據,二就是重復采集,這兩種情況都會導致采集上來的日志和原來的日志不一致。
Nxlog 在啟動的時候調用 nx_config_cache_read 接口,把 configcache.dat 文件里保存的信息讀出并保存在 config_cache 數據結構中,采集文件的模塊啟動時候會在 config_cache 里查找有沒有某個文件的數據,如果不存在就從文件的開頭或者末尾開始讀取,如果存在就調用 apr_file_seek 移動文件指針到上次讀取的位置,當 nxlog 要退出的時候將更新 config_cache 到文件最新的讀取位置,然后調用 nx_config_cache_write 將 config_cache 回寫到 configcache.dat 文件中。
3.4 Add & config modules
Nxlog 會首先遍歷 cfgtree, 針對所有的 module(共有4種, input, output, processor, extension) 分別調用 nx_module_add 接口。該接口會調用 nx_module_new 創建一個 module,然后再調用 nx_module_load_dso 綁定該 module 的方法,等一些必要的初始化都完成后會將該 module 放到 ctx->modules 鏈表中統一管理。
if ( strcasecmp(curr->directive, "input") == 0 ){if ( curr->first_child == NULL ){nx_conf_error(curr, "empty 'Input' block");}nx_module_add(ctx, curr->first_child, curr->args, NX_MODULE_TYPE_INPUT);}else if ( strcasecmp(curr->directive, "processor") == 0 ){if ( curr->first_child == NULL ){nx_conf_error(curr, "empty 'Processor' block");}nx_module_add(ctx, curr->first_child, curr->args, NX_MODULE_TYPE_PROCESSOR);}else if ( strcasecmp(curr->directive, "output") == 0 ){if ( curr->first_child == NULL ){nx_conf_error(curr, "empty 'Output' block");}nx_module_add(ctx, curr->first_child, curr->args, NX_MODULE_TYPE_OUTPUT);}else if ( strcasecmp(curr->directive, "extension") == 0 ){if ( curr->first_child == NULL ){nx_conf_error(curr, "empty 'Extension' block");}nx_module_add(ctx, curr->first_child, curr->args, NX_MODULE_TYPE_EXTENSION);}接著 NXLog 會遍歷 ctx->modules 鏈表,分別調用每個 module 的 config 方法對該 module 進行配置,然后會解析該 module 的 “Exec” 以及 “Schedule” 配置。Nxlog 確實比較強大,不僅能采集各種類型的日志,而且還能通過 “Exec” 執行一些任務,比如日志過濾等,還能通過 “Schedule” 做一些任務調度,比如定期進行日志歸檔等。
ASSERT(module->status == NX_MODULE_STATUS_UNINITIALIZED);if ( module->decl->config != NULL ){module->decl->config(module);}else{nx_module_empty_config_check(module);}module->exec = nx_module_parse_exec_block(module, module->pool, module->directives);nx_module_parse_schedule_blocks(module);3.5 Init modules
NXLog 會遍歷 ctx->modules 鏈表,得到每個 module,然后調用 nx_module_init 接口對該 module 進行初始化。nx_module_init 也沒干什么事情,只是調用該 module 的 init 方法。
for ( module = NX_DLIST_FIRST(ctx->modules);module != NULL;module = NX_DLIST_NEXT(module, link) ){try{nx_module_init(module);if ( module->type == NX_MODULE_TYPE_INPUT ){num_input++;}}3.6 Init routes and jobs
Route 顧名思義就是路由,它定義數據從哪個 INPUT 模塊采集,經過哪個 PROCESSOR(不是必須的) 模塊處理,送到哪個 OUTPUT 模塊。Route 包含兩個配置,一個是 “Path”,它定義了數據的流動方向。第二個是 “Priority”,它定義了該 route 的優先級,路由會將自己的優先級賦值給 path 中的各個 module。
while ( curr != NULL ){if ( strcasecmp(curr->directive, "route") == 0 ){if ( nx_add_route(ctx, curr, curr->args) == TRUE ){num_routes++;}}curr = curr->next;}Nxlog 維護一個 jobgroups 鏈表,該鏈表按照優先級順序存放著許多 jobgroup,相同優先級的 module 會把他們的 job 掛到同一個 jobgroup->jobs 上面。worker_thread 會優先從 priority 高的 jobgroup 里取 job,最終演變下來就是 priority 高的 module 的 event 會優先得到處理。
ASSERT(ctx != NULL);ctx->jobgroups = apr_palloc(ctx->pool, sizeof(nx_jobgroups_t));NX_DLIST_INIT(ctx->jobgroups, nx_jobgroup_t, link);for ( module = NX_DLIST_FIRST(ctx->modules);module != NULL;module = NX_DLIST_NEXT(module, link) ){jobgroup = nx_ctx_get_jobgroup(ctx, module->priority);job = apr_pcalloc(ctx->pool, sizeof(nx_job_t));NX_DLIST_INSERT_TAIL(&(jobgroup->jobs), job, link);module->job = job;}3.7 Create threads
NXLog 會創建一個 event_thread 和多個 worker_thread。究竟會創建幾個 worker_thread 由 num_worker_thread 變量來決定,下面來說說這個 num_worker_thread。
如果配置文件里配置了 "Threads",num_worker_thread 將會被設置成該值。如果沒有配置,nxlog 會根據配置文件里配置的 module 的個數以及所有 module 的 pollset 個數計算得到 num_worker_thread,具體算法可以參見 nxlog_create_threads 函數,在此我就不贅述。當配置文件里配了很多 module 的時候,這個數值有可能會大于 CPU core 的數量,這時可能會有人認為這是沒用的,因為這些 threads 不可能同時得到執行,而且會增加系統的工作量(內存,調度的開銷)。在我看來這要分情況,如果這些 worker_thread 從事的是 CPU 密集型的工作,我覺得這種觀點是正確的。但是如果這些 worker_thread 從事的是 IO 密集型的工作,這種觀點就值得推敲了,IO 密集型的工作一個顯著的特點是 Thread 在 IO 不 Ready 的情況下會睡眠,這樣即使你起的 thread 數量超過了 CPU core 的數量,但由于很多 thread 都處于睡眠狀態,真正執行的 thread 并不多。針對 IO 密集型的工作,增加 thread 數量是能顯著提升性能的,但也不是越多越好,當 thread 數量太多的時候反而會走向另一個極端。
event_thread 主要是處理延時的 event,它會從 ctx->events 中依次取出 event, 當該 event 的 time 已經超時了 event_thread 會把該 event 交給 worker_thread 來處理。如果沒有超時會得到離當前時間最近的一個 event 要超時的時間,然后調用 apr_thread_cond_timedwait sleep 這么長的時間,等醒來后再去處理這個 event,當然在 sleep 的這段時間內如有新的 event 產生會調用 apr_thread_cond_signal(nxlog->event_cond) 喚醒 event_thread。由于 event_thread 干的事情不多,因此只需要一個。
worker_thread 先通過 nx_ctx_next_job 接口獲取一個 event,如果沒有 event 可處理,worker_thread 會調用 apr_thread_cond_wait(nxlog->worker_cond, nxlog->mutex) 睡眠,等有新的 event 產生時會調用 apr_thread_cond_signal(nxlog->worker_cond) 把它喚醒。如果有 event 會調用 nx_event_process 來處理。nx_ctx_next_job 獲取 event 有一定的講究,他會優先獲取 priority 高的 module 的 event,它是通過優先遍歷 priority 高的 jobgroup 來實現的,前面創建 jobgroups 的時候我們就說過,nxlog 會把不同 priority 的 module 的 job 放到不同 priority 的 jobgroup 中。
3.8 Start modules
NXLog 會遍歷 ctx->modules 鏈表,得到每個 module,然后調用 nx_module_start 啟動該 module。nx_module_start 會發送 NX_EVENT_MODULE_START event 給 worker_thread,worker_thread 收到該 event 后會調用 nx_module_start_self,繞了一圈后真正干活的才出現,nx_module_start_self 會首先獲取該 module 的狀態,如果是 NX_MODULE_STATUS_STOPPED 會調用該 module 的 start 接口,然后把它的狀態置成 NX_MODULE_STATUS_RUNNING。
ASSERT(nx_module_get_status(module) == NX_MODULE_STATUS_STOPPED);if ( module->decl->start != NULL ){module->decl->start(module);}nx_module_add_scheduled_events(module);nx_module_set_status(module, NX_MODULE_STATUS_RUNNING);3.9 Main loop
nxlog_mainloop 沒干什么事情,一直調用 apr_sleep(NX_POLL_TIMEOUT),把 CPU 讓給 event_thread 和 worker_threads。
當 nxlog 收到 SIGTERM, SIGINT, SIGQUIT 這三個信號中的任何一個時,terminate_request 會被置成 TRUE,nxlog_mainloop 結束,Nxlog 調用 nxlog_exit 執行 stop/shutdown module, 結束 event_thread, worker_threads, 回寫 config cache,remove pidfile,回收資源等動作,最后 Nxlog 進程退出。
總結
以上是生活随笔為你收集整理的深入浅出 NXLog (一)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JavaWeb(二)框架搭建篇
- 下一篇: snmp - 简单网络管理协议