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