前言
随着业务的发展,日志是必不可少的环节之一,越来越多的日志库如雨后春笋般崭露头角。这里就有很多日志库从快、准、稳这三个维度的追求,将触手从java层蔓延到了Linux层。这里就市面上运用了mmap技术的开源日志库(腾讯Mars中的一环XLog\美团点评的logan)进行一些个人总结,如有不对的地方,还请指正。
抱着从简到杂,从易到复的原则,这里首先简述美团点评的Logan库,希望对广大希望快速拥有日志能力的开发者们早日脱离日志坑。
1、切菜、备油(这里我们可以看到必须的几要素如下)
(1)、mmap文件的缓存路径;(2)、每个文件的最大文件大小,默认10M;(3)、保存日志的缓存天数,默认是7天;(4)、ASE加密的key;(5)、ASE加密的IV;(6)、需要SD卡最小的额外存储,默认是50M。
2、准备工具
LoganControlCenter使用一个基于链接节点的无界线程安全队列的ConcurrentLinkedQueue来处理消费事件。同样我们还是准备工具:
重点如下:(1)、最大队列数,默认500个;(2)、自定义线程LogainThread;(3)、日期格式
这些都有了,我们思考,如果我们要写一个日志库,配置、参数都有了是不是就差写文件了,就好比菜和工具都齐了,我们现在是不要要有菜谱做细节啊,那好,我们接下来看美团点评的同学如何做菜下酒。
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); //创建保存mmap文件的目录
strcat(cache_path, LOGAN_CACHE_FILE);
..........
char *dirs = (char *) malloc(total); //缓存文件目录
if (NULL != dirs) {
_dir_path = dirs; //日志写入的文件目录
} else {
......
}
......
if (isAddDivede)
strcat(dirs, LOGAN_DIVIDE_SYMBOL);
makedir_clogan(_dir_path); //创建缓存目录,如果初始化失败,注意释放_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中读取数据
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;
}
接下来是clogan_open
FILE *file_temp = fopen(logan_model->file_path, "ab+");
long longBytes = ftell(file_temp);
logan_model->file_len = longBytes;
// 在之前init的时候会判断mmap缓存目录是否有缓存,如果有,下面在open的时候会把缓存数据写入防止丢失,这里主要告诉我们:1、如果之前有剩下的菜没有烧的,这次也放进去,就是有缓存我们也写到日志中;2、初始化我们的logan_model结构体(后面会贴出解释,存放文件数据的精髓类,里面包含了文件大小,压缩等信息);3、对于是否是mmap,这里还做了目录创建等处理
int clogan_open(const char *pathname) {
.........
if (NULL != logan_model) { //回写到日志中
if (logan_model->total_len > LOGAN_WRITEPROTOCOL_HEAER_LENGTH) {
clogan_flush();
}
..........
} else {
logan_model = malloc(sizeof(cLogan_model));
// 初始化Logan_model结构体
............
}
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)); //创建文件路径
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,缓存文件目录和文件名称
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; //数据长度
char *file_path; //文件路径
int is_malloc_zlib;
z_stream *strm;
int zlib_type; //压缩类型
char remain_data[16]; //剩余空间
int remain_data_len; //剩余空间长度
int is_ready_gzip; //是否可以gzip
int file_stream_type; //文件流类型
FILE *file; //文件流
long file_len; //文件大小
unsigned char *buffer_point; //缓存的指针 (不变)
unsigned char *last_point; //最后写入位置的指针
unsigned char *total_point; //总数的指针 (可能变) , 给c看,低字节
unsigned char *content_lent_point;//协议内容长度指针 , 给java看,高字节
int content_len; //内容的大小
unsigned char aes_iv[16]; //aes_iv
int is_ok;
} cLogan_model;
接下来就是精髓了,就是菜谱告诉我们,第一步我们要放油初始化文件目录参数,第二步告诉我们调用open方法开火,第三步就是放菜啦clogan_write,这里都是写入缓存的,具体随我们来看美团点评的同学如何来玩。
/**
@brief 写入数据 按照顺序和类型传值(强调、强调、强调)
@param flag 日志类型 (int)
@param log 日志内容 (char*)
@param local_time 日志发生的本地时间,形如1502100065601 (long long)
@param thread_name 线程名称 (char*)
@param thread_id 线程id (long long) 为了兼容JAVA
@param ismain 是否为主线程,0为是主线程,1位非主线程 (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;
}
//如果文件大小超过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文件是否存在,如果被删除,用内存缓存
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); //关闭已开的流
}
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;
}
上面这块东西代码块不大我就不省略了,别看这里代码块不多,但却是重中之重,首先当头便是文件写入策略,1、如果当日只会有一个文件记录,且大于10M则拒绝写入,这里笔者认为应该是根据实际情况做了一层妥协,其实很多业务方的数据都会丢进来,那么美团在业务回捞的时候,用户肯定会很懵逼,怎么就反馈了一个页面打不开的问题,我就10M流量没了,这里我们是否可以实现多业务多文件或者多类别的区分,这样在回捞的时候也可以侧重点来进行回捞,也就同时也避免了单文件10M数据拒绝写入的尴尬境地。2、这里的写入在非特殊情况下只是写入缓存和内存,读者们要在这里注意下。3、clogan_write_section关注这个方法,非常重要,这里是在正常情况下,非接入方干预前提下的缓存写入用户真实文件的时机,具体逻辑如下:
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); //有数据操作,要更新数据长度到缓存中
int is_gzip_end = 0;
if (!logan_model->file_len ||
logan_model->content_len >= LOGAN_MAX_GZIP_UTIL) { //是否一个压缩单元结束
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) { //如果是个空文件、第一条日志写入
isWrite = 1;
printf_clogan("clogan_write2 > write type empty file \n");
} else if (buffer_type == LOGAN_MMAP_MEMORY && is_gzip_end) { //直接写入文件
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 且 文件长度已经超过三分之一
isWrite = 1;
printf_clogan("clogan_write2 > write type MMAP \n");
}
if (isWrite) { //写入
write_flush_clogan();
} else if (is_gzip_end) { //如果是mmap类型,不回写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);
}
}
}
总结一下:1、如果是空文件的头文件,直接写入文件;2、如果是内存缓存且压缩结束了,直接写入文件;3、如果是mmap缓存,且文件长度超过缓存的三分之一,直接写入文件;
这时候差不多菜谱就读的差不多了,那细心的读者就会有一个问题了,说我就喜欢吃8分熟的牛排,我写了30条日志,并不满足上述的3个条件,或者我就要在APP退出之前把所有缓存数据写入文件,我就喜欢这样不写进去我心里不踏实。那这里顺理成章,一个经过通用的SDK库肯定具备完善的API,菜谱肯定是能满足大多数人的需要的,那就是clogan_flush,接下来我们来梳理下:
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);
}
}
//文件写入磁盘、更新文件大小
void write_dest_clogan(void *point, size_t size, size_t length, cLogan_model *loganModel) {
if (!is_file_exist_clogan(loganModel->file_path)) { //如果文件被删除,再创建一个文件
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);//写入到文件中
fflush(logan_model->file);
loganModel->file_len += loganModel->total_len; //修改文件大小
}
那这里就是写入磁盘、更新文件数据model.好了,这盘菜出锅了,接下来会为大家简述下微信的Mars XLOG流程,那个比这个更全面些,但是魔改起来需要对C和底层有更高阶的认识。期望大家可以做出比我更美味的菜,如有错误请指出,谢谢食客们的光临。