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