前言
隨著業(yè)務(wù)的發(fā)展,日志是必不可少的環(huán)節(jié)之一,越來(lái)越多的日志庫(kù)如雨后春筍般嶄露頭角。這里就有很多日志庫(kù)從快、準(zhǔn)、穩(wěn)這三個(gè)維度的追求,將觸手從java層蔓延到了Linux層。這里就市面上運(yùn)用了mmap技術(shù)的開(kāi)源日志庫(kù)(騰訊Mars中的一環(huán)XLog\美團(tuán)點(diǎn)評(píng)的logan)進(jìn)行一些個(gè)人總結(jié),如有不對(duì)的地方,還請(qǐng)指正。
抱著從簡(jiǎn)到雜,從易到復(fù)的原則,這里首先簡(jiǎn)述美團(tuán)點(diǎn)評(píng)的Logan庫(kù),希望對(duì)廣大希望快速擁有日志能力的開(kāi)發(fā)者們?cè)缛彰撾x日志坑。

1、切菜、備油(這里我們可以看到必須的幾要素如下)
(1)、mmap文件的緩存路徑;(2)、每個(gè)文件的最大文件大小,默認(rèn)10M;(3)、保存日志的緩存天數(shù),默認(rèn)是7天;(4)、ASE加密的key;(5)、ASE加密的IV;(6)、需要SD卡最小的額外存儲(chǔ),默認(rèn)是50M。


2、準(zhǔn)備工具
LoganControlCenter使用一個(gè)基于鏈接節(jié)點(diǎn)的無(wú)界線(xiàn)程安全隊(duì)列的ConcurrentLinkedQueue來(lái)處理消費(fèi)事件。同樣我們還是準(zhǔn)備工具:
重點(diǎn)如下:(1)、最大隊(duì)列數(shù),默認(rèn)500個(gè);(2)、自定義線(xiàn)程LogainThread;(3)、日期格式

這些都有了,我們思考,如果我們要寫(xiě)一個(gè)日志庫(kù),配置、參數(shù)都有了是不是就差寫(xiě)文件了,就好比菜和工具都齊了,我們現(xiàn)在是不要要有菜譜做細(xì)節(jié)啊,那好,我們接下來(lái)看美團(tuán)點(diǎn)評(píng)的同學(xué)如何做菜下酒。
3、JNI層交互方法匯總

/**
* Logan初始化
* @param cachedirs 緩存路徑
* @param pathdirs 目錄路徑
* @param max_file 日志文件最大值
*/
int
clogan_init(const char *cache_dirs, const char *path_dirs, int max_file, const char *encrypt_key16,
const char *encrypt_iv16) {
.........
if (max_file > 0) {
max_file_len = max_file;
} else {
max_file_len = LOGAN_LOGFILE_MAXLENGTH;
}
...........
//初始化秘鑰的KEY和IV
aes_init_key_iv(encrypt_key16, encrypt_iv16);
makedir_clogan(cache_path); //創(chuàng)建保存mmap文件的目錄
strcat(cache_path, LOGAN_CACHE_FILE);
..........
char *dirs = (char *) malloc(total); //緩存文件目錄
if (NULL != dirs) {
_dir_path = dirs; //日志寫(xiě)入的文件目錄
} else {
......
}
......
if (isAddDivede)
strcat(dirs, LOGAN_DIVIDE_SYMBOL);
makedir_clogan(_dir_path); //創(chuàng)建緩存目錄,如果初始化失敗,注意釋放_(tái)dir_path
int flag = LOGAN_MMAP_FAIL;
if (NULL == _logan_buffer) {
if (NULL == _cache_buffer_buffer) {
flag = open_mmap_file_clogan(cache_path, &_logan_buffer, &_cache_buffer_buffer);
} else {
flag = LOGAN_MMAP_MEMORY;
}
} else {
flag = LOGAN_MMAP_MMAP;
}
.............
if (is_init_ok) {
if (NULL == logan_model) {
logan_model = malloc(sizeof(cLogan_model));
if (NULL != logan_model) { //堆非空判斷 , 如果為null , 就失敗
memset(logan_model, 0, sizeof(cLogan_model));
} else {
is_init_ok = 0;
printf_clogan("clogan_init > malloc memory fail for logan_model\n");
back = CLOGAN_INIT_FAIL_NOMALLOC;
return back;
}
}
if (flag == LOGAN_MMAP_MMAP) //MMAP的緩存模式,從緩存的MMAP中讀取數(shù)據(jù)
read_mmap_data_clogan(_dir_path);
printf_clogan("clogan_init > logan init success\n");
} else {
printf_clogan("clogan_open > logan init fail\n");
// 初始化失敗,刪除所有路徑
.........
return back;
}
接下來(lái)是clogan_open
FILE *file_temp = fopen(logan_model->file_path, "ab+");
long longBytes = ftell(file_temp);
logan_model->file_len = longBytes;
// 在之前init的時(shí)候會(huì)判斷mmap緩存目錄是否有緩存,如果有,下面在open的時(shí)候會(huì)把緩存數(shù)據(jù)寫(xiě)入防止丟失,這里主要告訴我們:1、如果之前有剩下的菜沒(méi)有燒的,這次也放進(jìn)去,就是有緩存我們也寫(xiě)到日志中;2、初始化我們的logan_model結(jié)構(gòu)體(后面會(huì)貼出解釋?zhuān)娣盼募?shù)據(jù)的精髓類(lèi),里面包含了文件大小,壓縮等信息);3、對(duì)于是否是mmap,這里還做了目錄創(chuàng)建等處理
int clogan_open(const char *pathname) {
.........
if (NULL != logan_model) { //回寫(xiě)到日志中
if (logan_model->total_len > LOGAN_WRITEPROTOCOL_HEAER_LENGTH) {
clogan_flush();
}
..........
} else {
logan_model = malloc(sizeof(cLogan_model));
// 初始化Logan_model結(jié)構(gòu)體
............
}
char *temp = NULL;
size_t file_path_len = strlen(_dir_path) + strlen(pathname) + 1;
char *temp_file = malloc(file_path_len); // 日志文件路徑
if (NULL != temp_file) {
memset(temp_file, 0, file_path_len);
temp = temp_file;
memcpy(temp, _dir_path, strlen(_dir_path));
temp += strlen(_dir_path);
memcpy(temp, pathname, strlen(pathname)); //創(chuàng)建文件路徑
logan_model->file_path = temp_file;
if (!init_file_clogan(logan_model)) { //初始化文件IO和文件大小
is_open_ok = 0;
back = CLOGAN_OPEN_FAIL_IO;
return back;
}
if (init_zlib_clogan(logan_model) != Z_OK) { //初始化zlib壓縮
is_open_ok = 0;
back = CLOGAN_OPEN_FAIL_ZLIB;
return back;
}
logan_model->buffer_point = _logan_buffer;
if (buffer_type == LOGAN_MMAP_MMAP) { //如果是MMAP,緩存文件目錄和文件名稱(chēng)
cJSON *root = NULL;
Json_map_logan *map = NULL;
root = cJSON_CreateObject();
map = create_json_map_logan();
char *back_data = NULL;
if (NULL != root) {
if (NULL != map) {
add_item_number_clogan(map, LOGAN_VERSION_KEY, CLOGAN_VERSION_NUMBER);
add_item_string_clogan(map, LOGAN_PATH_KEY, pathname);
inflate_json_by_map_clogan(root, map);
back_data = cJSON_PrintUnformatted(root);
}
cJSON_Delete(root);
if (NULL != back_data) {
add_mmap_header_clogan(back_data, logan_model);
free(back_data);
} else {
logan_model->total_point = _logan_buffer;
logan_model->total_len = 0;
}
} else {
logan_model->total_point = _logan_buffer;
logan_model->total_len = 0;
}
logan_model->last_point = logan_model->total_point + LOGAN_MMAP_TOTALLEN;
if (NULL != map) {
delete_json_map_clogan(map);
}
} else {
logan_model->total_point = _logan_buffer;
logan_model->total_len = 0;
logan_model->last_point = logan_model->total_point + LOGAN_MMAP_TOTALLEN;
}
restore_last_position_clogan(logan_model);
init_encrypt_key_clogan(logan_model);
logan_model->is_ok = 1;
is_open_ok = 1;
} else {
is_open_ok = 0;
back = CLOGAN_OPEN_FAIL_MALLOC;
printf_clogan("clogan_open > malloc memory fail\n");
}
..........
return back;
}
typedef struct logan_model_struct {
int total_len; //數(shù)據(jù)長(zhǎng)度
char *file_path; //文件路徑
int is_malloc_zlib;
z_stream *strm;
int zlib_type; //壓縮類(lèi)型
char remain_data[16]; //剩余空間
int remain_data_len; //剩余空間長(zhǎng)度
int is_ready_gzip; //是否可以gzip
int file_stream_type; //文件流類(lèi)型
FILE *file; //文件流
long file_len; //文件大小
unsigned char *buffer_point; //緩存的指針 (不變)
unsigned char *last_point; //最后寫(xiě)入位置的指針
unsigned char *total_point; //總數(shù)的指針 (可能變) , 給c看,低字節(jié)
unsigned char *content_lent_point;//協(xié)議內(nèi)容長(zhǎng)度指針 , 給java看,高字節(jié)
int content_len; //內(nèi)容的大小
unsigned char aes_iv[16]; //aes_iv
int is_ok;
} cLogan_model;
接下來(lái)就是精髓了,就是菜譜告訴我們,第一步我們要放油初始化文件目錄參數(shù),第二步告訴我們調(diào)用open方法開(kāi)火,第三步就是放菜啦clogan_write,這里都是寫(xiě)入緩存的,具體隨我們來(lái)看美團(tuán)點(diǎn)評(píng)的同學(xué)如何來(lái)玩。
/**
@brief 寫(xiě)入數(shù)據(jù) 按照順序和類(lèi)型傳值(強(qiáng)調(diào)、強(qiáng)調(diào)、強(qiáng)調(diào))
@param flag 日志類(lèi)型 (int)
@param log 日志內(nèi)容 (char*)
@param local_time 日志發(fā)生的本地時(shí)間,形如1502100065601 (long long)
@param thread_name 線(xiàn)程名稱(chēng) (char*)
@param thread_id 線(xiàn)程id (long long) 為了兼容JAVA
@param ismain 是否為主線(xiàn)程,0為是主線(xiàn)程,1位非主線(xiàn)程 (int)
*/
int
clogan_write(int flag, char *log, long long local_time, char *thread_name, long long thread_id,
int is_main) {
int back = CLOGAN_WRITE_FAIL_HEADER;
if (!is_init_ok || NULL == logan_model || !is_open_ok) {
back = CLOGAN_WRITE_FAIL_HEADER;
return back;
}
//如果文件大小超過(guò)10M
if (is_file_exist_clogan(logan_model->file_path)) {
if (logan_model->file_len > max_file_len) {
printf_clogan("clogan_write > beyond max file , can't write log\n");
back = CLOAGN_WRITE_FAIL_MAXFILE;
return back;
}
} else {
if (logan_model->file_stream_type == LOGAN_FILE_OPEN) {
fclose(logan_model->file);
logan_model->file_stream_type = LOGAN_FILE_CLOSE;
}
if (NULL != _dir_path) {
if (!is_file_exist_clogan(_dir_path)) {
makedir_clogan(_dir_path);
}
init_file_clogan(logan_model);
printf_clogan("clogan_write > create log file , restore open file stream \n");
}
}
//判斷MMAP文件是否存在,如果被刪除,用內(nèi)存緩存
if (buffer_type == LOGAN_MMAP_MMAP && !is_file_exist_clogan(_mmap_file_path)) {
if (NULL != _cache_buffer_buffer) {
buffer_type = LOGAN_MMAP_MEMORY;
buffer_length = LOGAN_MEMORY_LENGTH;
printf_clogan("clogan_write > change to memory buffer");
_logan_buffer = _cache_buffer_buffer;
logan_model->total_point = _logan_buffer;
logan_model->total_len = 0;
logan_model->content_len = 0;
logan_model->remain_data_len = 0;
if (logan_model->zlib_type == LOGAN_ZLIB_INIT) {
clogan_zlib_delete_stream(logan_model); //關(guān)閉已開(kāi)的流
}
logan_model->last_point = logan_model->total_point + LOGAN_MMAP_TOTALLEN;
restore_last_position_clogan(logan_model);
init_zlib_clogan(logan_model);
init_encrypt_key_clogan(logan_model);
logan_model->is_ok = 1;
} else {
buffer_type = LOGAN_MMAP_FAIL;
is_init_ok = 0;
is_open_ok = 0;
_logan_buffer = NULL;
}
}
Construct_Data_cLogan *data = construct_json_data_clogan(log, flag, local_time, thread_name,
thread_id, is_main);
if (NULL != data) {
clogan_write_section(data->data, data->data_len);
construct_data_delete_clogan(data);
back = CLOGAN_WRITE_SUCCESS;
} else {
back = CLOGAN_WRITE_FAIL_MALLOC;
}
return back;
}
上面這塊東西代碼塊不大我就不省略了,別看這里代碼塊不多,但卻是重中之重,首先當(dāng)頭便是文件寫(xiě)入策略,1、如果當(dāng)日只會(huì)有一個(gè)文件記錄,且大于10M則拒絕寫(xiě)入,這里筆者認(rèn)為應(yīng)該是根據(jù)實(shí)際情況做了一層妥協(xié),其實(shí)很多業(yè)務(wù)方的數(shù)據(jù)都會(huì)丟進(jìn)來(lái),那么美團(tuán)在業(yè)務(wù)回?fù)频臅r(shí)候,用戶(hù)肯定會(huì)很懵逼,怎么就反饋了一個(gè)頁(yè)面打不開(kāi)的問(wèn)題,我就10M流量沒(méi)了,這里我們是否可以實(shí)現(xiàn)多業(yè)務(wù)多文件或者多類(lèi)別的區(qū)分,這樣在回?fù)频臅r(shí)候也可以側(cè)重點(diǎn)來(lái)進(jìn)行回?fù)疲簿屯瑫r(shí)也避免了單文件10M數(shù)據(jù)拒絕寫(xiě)入的尷尬境地。2、這里的寫(xiě)入在非特殊情況下只是寫(xiě)入緩存和內(nèi)存,讀者們要在這里注意下。3、clogan_write_section關(guān)注這個(gè)方法,非常重要,這里是在正常情況下,非接入方干預(yù)前提下的緩存寫(xiě)入用戶(hù)真實(shí)文件的時(shí)機(jī),具體邏輯如下:
void clogan_write2(char *data, int length) {
if (NULL != logan_model && logan_model->is_ok) {
clogan_zlib_compress(logan_model, data, length);
update_length_clogan(logan_model); //有數(shù)據(jù)操作,要更新數(shù)據(jù)長(zhǎng)度到緩存中
int is_gzip_end = 0;
if (!logan_model->file_len ||
logan_model->content_len >= LOGAN_MAX_GZIP_UTIL) { //是否一個(gè)壓縮單元結(jié)束
clogan_zlib_end_compress(logan_model);
is_gzip_end = 1;
update_length_clogan(logan_model);
}
int isWrite = 0;
if (!logan_model->file_len && is_gzip_end) { //如果是個(gè)空文件、第一條日志寫(xiě)入
isWrite = 1;
printf_clogan("clogan_write2 > write type empty file \n");
} else if (buffer_type == LOGAN_MMAP_MEMORY && is_gzip_end) { //直接寫(xiě)入文件
isWrite = 1;
printf_clogan("clogan_write2 > write type memory \n");
} else if (buffer_type == LOGAN_MMAP_MMAP &&
logan_model->total_len >=
buffer_length / LOGAN_WRITEPROTOCOL_DEVIDE_VALUE) { //如果是MMAP 且 文件長(zhǎng)度已經(jīng)超過(guò)三分之一
isWrite = 1;
printf_clogan("clogan_write2 > write type MMAP \n");
}
if (isWrite) { //寫(xiě)入
write_flush_clogan();
} else if (is_gzip_end) { //如果是mmap類(lèi)型,不回寫(xiě)IO,初始化下一步
logan_model->content_len = 0;
logan_model->remain_data_len = 0;
init_zlib_clogan(logan_model);
restore_last_position_clogan(logan_model);
init_encrypt_key_clogan(logan_model);
}
}
}
總結(jié)一下:1、如果是空文件的頭文件,直接寫(xiě)入文件;2、如果是內(nèi)存緩存且壓縮結(jié)束了,直接寫(xiě)入文件;3、如果是mmap緩存,且文件長(zhǎng)度超過(guò)緩存的三分之一,直接寫(xiě)入文件;
這時(shí)候差不多菜譜就讀的差不多了,那細(xì)心的讀者就會(huì)有一個(gè)問(wèn)題了,說(shuō)我就喜歡吃8分熟的牛排,我寫(xiě)了30條日志,并不滿(mǎn)足上述的3個(gè)條件,或者我就要在APP退出之前把所有緩存數(shù)據(jù)寫(xiě)入文件,我就喜歡這樣不寫(xiě)進(jìn)去我心里不踏實(shí)。那這里順理成章,一個(gè)經(jīng)過(guò)通用的SDK庫(kù)肯定具備完善的API,菜譜肯定是能滿(mǎn)足大多數(shù)人的需要的,那就是clogan_flush,接下來(lái)我們來(lái)梳理下:
int clogan_flush(void) {
int back = CLOGAN_FLUSH_FAIL_INIT;
if (!is_init_ok || NULL == logan_model) {
return back;
}
write_flush_clogan();
back = CLOGAN_FLUSH_SUCCESS;
printf_clogan(" clogan_flush > write flush\n");
return back;
}
void write_flush_clogan() {
if (logan_model->zlib_type == LOGAN_ZLIB_ING) {
clogan_zlib_end_compress(logan_model);
update_length_clogan(logan_model);
}
if (logan_model->total_len > LOGAN_WRITEPROTOCOL_HEAER_LENGTH) {
unsigned char *point = logan_model->total_point;
point += LOGAN_MMAP_TOTALLEN;
write_dest_clogan(point, sizeof(char), logan_model->total_len, logan_model);
printf_clogan("write_flush_clogan > logan total len : %d \n", logan_model->total_len);
clear_clogan(logan_model);
}
}
//文件寫(xiě)入磁盤(pán)、更新文件大小
void write_dest_clogan(void *point, size_t size, size_t length, cLogan_model *loganModel) {
if (!is_file_exist_clogan(loganModel->file_path)) { //如果文件被刪除,再創(chuàng)建一個(gè)文件
if (logan_model->file_stream_type == LOGAN_FILE_OPEN) {
fclose(logan_model->file);
logan_model->file_stream_type = LOGAN_FILE_CLOSE;
}
if (NULL != _dir_path) {
if (!is_file_exist_clogan(_dir_path)) {
makedir_clogan(_dir_path);
}
init_file_clogan(logan_model);
printf_clogan("clogan_write > create log file , restore open file stream \n");
}
}
if (CLOGAN_EMPTY_FILE == loganModel->file_len) { //如果是空文件插入一行CLogan的頭文件
insert_header_file_clogan(loganModel);
}
fwrite(point, sizeof(char), logan_model->total_len, logan_model->file);//寫(xiě)入到文件中
fflush(logan_model->file);
loganModel->file_len += loganModel->total_len; //修改文件大小
}
那這里就是寫(xiě)入磁盤(pán)、更新文件數(shù)據(jù)model.好了,這盤(pán)菜出鍋了,接下來(lái)會(huì)為大家簡(jiǎn)述下微信的Mars XLOG流程,那個(gè)比這個(gè)更全面些,但是魔改起來(lái)需要對(duì)C和底層有更高階的認(rèn)識(shí)。期望大家可以做出比我更美味的菜,如有錯(cuò)誤請(qǐng)指出,謝謝食客們的光臨。