一个好的移动端基础日志框架是怎么炼成的

最近美团开源了日志框架 Logan,微信开源的 Mars 里也有个日志模块 xlog。这个功能用的很多,简单点说就是往文件里写日志,这并不是一个复杂的框架。但是要做的好,还是有几个点需要考虑的,比如使用方便,性能和安全。

微信mars 的高性能日志模块 xlog
美团点评移动端基础日志库——Logan
网上基本上只有两个框架的使用教程,并没有源码分析。这两篇都是作者写的文章,基本上把框架的精髓都讲出来了,看完应该就知道一个好的日志框架是怎么炼成的了。
其实这两个框架的思路基本上是一模一样的,很明显 Logan 借鉴了 xlog 的很多实现。不过 Logan 的代码质量还是很高的,xlog 则有点乱,看的头疼。所以还是推荐阅读 Logan 源码。

mmap

mmap原理已经讲的很多了,主要说下注意点:

  • 映射的文件必须存在,且大小必须大于0
  • mmap 并不能改变文件本身的大小
  • 写入的数据不能超过文件本身的大小
unsigned char *p_map = NULL;
    int size = MAXLENGTH;
    int fd = open(_filepath, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP);
    //文件必须存在
    if (fd != -1) {
        FILE *file = fopen(_filepath, "rb+");
        
        if (NULL != file) {
            fseek(file, 0, SEEK_END);
            long longBytes = ftell(file);
            if (longBytes <= size) {
                fseek(file, 0, SEEK_SET);
                char zero_data[size];
                memset(zero_data, 0, size);
                size_t _size = 0;
                //必须先设置大小
                _size = fwrite(zero_data, sizeof(char), size, file);
                fflush(file);
                if (_size == size) {
                    p_map = (unsigned char *)mmap(0, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
                    close(fd);
                }
            }
            fclose(file);
        }
    }

mmap 优点:

  • 几乎拥有和直接写内存一样的性能
  • 程序退出,关机等异常情况,系统会回写,可靠性有了保障

也正是这些原因,mmap 并不是用来直接映射日志文件的,而是映射一个 temp 文件做一个中转。

日志文件

如果直接把日志写内存,一定的条件下写回文件。这种做法会很容易,但是 crash,关机等异常情况会容易丢日志。
而 mmap 不会有这个问题,异常情况下,日志还是会写入 mmap 映射的中转文件中。所以现在要做的就是把 mmap 文件中的数据写回到日志文件中去。

logan 里写回的地方很多,主要有 3 处很重要:
监听 app 通知,比如回到后台的时候

- (void)appWillResignActive {
    [self flash];
}

- (void)appDidEnterBackground {
    [self flash];
}

- (void)appWillEnterForeground {
    [self flash];
}

- (void)appWillTerminate {
    [self flash];
}

每次日志系统启动的时候,这正是 mmap 的优点所在,不会丢日志

int clogan_init(const char *cache_dirs, const char *path_dirs, int max_file,                 const char *encrypt_key16,
                const char *encrypt_iv16) {
    ...
    if (flag == LOGAN_MMAP_MMAP) {
        read_mmap_data_clogan(_dir_path);
    }
    ...
}

文件长度超过三分之一或者第一条日志写入

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_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();
}

日志协议

如果只是简单地往文件里添加字符串,那么是不需要自建协议的。不过一个好的日志框架,需要对大日志分片,需要把 mmap 文件里的数据 flush 到日志文件里等,所以也是需要自定义协议的。而且我觉得一个自定义协议本身就具有了加密功能。

logan 日志协议:
logan_protocol-1

以上只是移动端存储的协议,大数据分片前,是会先构造出Construct_Data_cLogan

...
add_item_string_clogan(map, log_key, log);
add_item_number_clogan(map, flag_key, (double) flag);
add_item_number_clogan(map, localtime_key, (double) local_time);
add_item_string_clogan(map, threadname_key, thread_name);
add_item_number_clogan(map, threadid_key, (double) thread_id);
add_item_bool_clogan(map, ismain_key, is_main);
...

服务端解析源码还没看,猜测是先将分片的分隔符 \0\1 去掉合并成整个数据,而整个数据的分隔符是 \n,然后再解析。

压缩加密

这个 logan 和 xlog 都是用的同样的方式:流式压缩,先压缩后加密。

#define LOGAN_MAX_GZIP_UTIL 5 * 1024 //压缩单元的大小
void clogan_zlib_compress(cLogan_model *model, char *data, int data_len)
void clogan_zlib_end_compress(cLogan_model *model)

基于上面的 20KB 分片,会调用clogan_zlib_compressclogan_zlib_end_compress两个方法。 每片的压缩单元至少为 5KB,每个压缩单元进行压缩然后加密,代码简化为:

void clogan_write_section(char *data, int length) {
    int size = 20 * 1024;
    int times = length / size;
    int remain_len = length % size;
    char *temp = data;
    int i = 0;
    for (i = 0; i < times; i++) {
        clogan_write2(temp, size);
        temp += size;
    }
    if (remain_len) {
        clogan_write2(temp, remain_len);
    }
}

void clogan_write2(char *data, int length) {
    clogan_zlib_compress(logan_model, data, length);  //压缩方法里压缩完会加密
    if (logan_model->content_len >= 5 * 1024){ //是否一个压缩单元结束
        clogan_zlib_end_compress(logan_model);
    }
          
    clogan_zlib_end_compress(logan_model);
}

想法

看了很多框架源码,但是一直没有机会去实现一个真正能用的,好用的底层框架。曾经一度觉得这些框架都是你抄抄我,我抄抄你。不过仔细想来,从 0 做一个真正能用的,能解决业务痛点的框架,还真是一件有难度又有意义的事情。

作者:levi
comments powered by Disqus