美文网首页DevSupport
会了JNI,我们来看看Logan库

会了JNI,我们来看看Logan库

作者: violet小咔咔 | 来源:发表于2018-11-07 21:09 被阅读0次

    前言

    随着业务的发展,日志是必不可少的环节之一,越来越多的日志库如雨后春笋般崭露头角。这里就有很多日志库从快、准、稳这三个维度的追求,将触手从java层蔓延到了Linux层。这里就市面上运用了mmap技术的开源日志库(腾讯Mars中的一环XLog\美团点评的logan)进行一些个人总结,如有不对的地方,还请指正。
    抱着从简到杂,从易到复的原则,这里首先简述美团点评的Logan库,希望对广大希望快速拥有日志能力的开发者们早日脱离日志坑。

    Logan在初始化如下: Logan初始化.png 接着以炒菜的形式来表述吧:
    1、切菜、备油(这里我们可以看到必须的几要素如下)
    (1)、mmap文件的缓存路径;(2)、每个文件的最大文件大小,默认10M;(3)、保存日志的缓存天数,默认是7天;(4)、ASE加密的key;(5)、ASE加密的IV;(6)、需要SD卡最小的额外存储,默认是50M。 1LoganConfig.png 1-1初始化参数.png
    2、准备工具
    LoganControlCenter使用一个基于链接节点的无界线程安全队列的ConcurrentLinkedQueue来处理消费事件。同样我们还是准备工具:
    重点如下:(1)、最大队列数,默认500个;(2)、自定义线程LogainThread;(3)、日期格式 队列处理中心参数.png 这个队列处理中心担负了两个主要责任:一个是分发处理日志事件、还有一个就是负责把之前参数全部缓存起来在合适的时机传给JNI层。
    这些都有了,我们思考,如果我们要写一个日志库,配置、参数都有了是不是就差写文件了,就好比菜和工具都齐了,我们现在是不要要有菜谱做细节啊,那好,我们接下来看美团点评的同学如何做菜下酒。

    3、JNI层交互方法汇总

    前言说到这是一个基于Linux层面的一种内存映射技术,这技术会在后面统一讲解,这里暂时先梳理Logan库的思路和逻辑。我已经迫不及待的在找菜谱了,直接搜关键字Native,因为上一章节提到JNI交互的语法格式需要一个native字段,这里我们直接抓住重点,看到clogan_init、clogan_open、clogan_debug、clogan_write、clogan_flush CLoganProtocol中维护的native方法.png 那既然我们找到菜谱了,我们看看究竟每一步是干嘛的,第一步clogan_init:简述就是初始化缓存路径、目录路径,秘钥,如果mmap文件缓存目录初始化失败了,这里会延用内存作为缓存的姿势继续保存文件。因为逻辑较多,删除了部分判断逻辑用省略号代替。
    /**
     * 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和底层有更高阶的认识。期望大家可以做出比我更美味的菜,如有错误请指出,谢谢食客们的光临。

    相关文章

      网友评论

        本文标题:会了JNI,我们来看看Logan库

        本文链接:https://www.haomeiwen.com/subject/rdebxqtx.html