微信 xlog 源码分析
初始化
程序启动初始化xlog
1 |
|
一堆的初始化设置
- 设置 log level
- 设置是否开启 控制台 日志输出
- 设置写日志的模式为异步
- 暂存到 mmap buffer 中,满足条件刷入文件中
- 避免每写一条log调用一次 IO
- 设置log日志的存储路径
- 设置log日志的文件名前缀
- 设置加密的公钥,不加密传空字符串
- 设置压缩模式,zlib 或者 Zstd
- 日志会先压缩咋存储,如果加密,先压缩再加密
- 设置 mmap 临时文件路径,为空,使用 logdir_
- 设置缓存天数,为 0
写日志接口
1 |
|
format 后写入文件的日志格式
规则
1 |
|
示例
1 |
|
appender_open && xlogger_Write
mars/xlog/appender.h
定义了一系列与 appender 操作的 c 接口,内部的 XloggerAppender 是真正实现逻辑的 c++ 类。
1 |
|
mars/xlog/src/appender.cc
1 |
|
关键的代码
1 |
|
xlogger_SetAppender 定义在 mars/comm/xlogger/xloggerbase.h
xloggerbase 导出了一套 c 接口用于写日志的调用。
mars/comm/xlogger/xloggerbase.h
1 |
|
关键的是这几个
1 |
|
调用栈
1 |
|
gs_appender 外部调用 xlogger_SetAppender 传入的。
mars/xlog/src/appender.cc
1 |
|
然后就是分析 XloggerAppender 类的细节了。
XloggerAppender
1 |
|
- 初始化赋值,创建了一个线程 XloggerAppender::__AsyncLogThread
- 执行了 open 方法
XloggerAppender::Open
1 |
|
主要做了什么
- 判断是否使用了 cachedir_ ,
- 使用了 cachedir_,创建对应的目录,
- 清理过期的数据,文件是否过期是通过 max_alive_time_ 来判断的
- 迁移 cachedir_ 中的文件到 logdir_ 中
- 创建logdir_, 清理logdir_中过期文件, 注意 Apple 平台,将logdir_文件属性设置为NSFileProtectionNone
- 打开 mmap 文件映射
- 使用了 cachedir_, mmap 映射的文件就放在 cachedir_ 中,否则在 logdir_ 中
- mmap 文件大小默认是 kBufferBlockLength = 150 * 1024, 150 KB
- mmap 映射失败,回退, 使用内存缓存
- 根据选定的压缩方式,决定 log_buff_
- kZlib 使用 LogZlibBuffer
- kZstd 使用 LogZstdBuffer
- 将 log_buff_ 中的内容刷入日志文件
- 使用 mmap 文件映射,崩溃后, 下次进入,将 mmap 文件中的内容读出,刷到文件中
- 通过这种方式达到崩溃了也不丢日志
- 开启 thread_async_ 执行 XloggerAppender::__AsyncLogThread
- 往 log_buff_ 中写入各种帮助信息
- “get mmap time: %”
- MARS_URL
- MARS_PATH
- ….
XloggerAppender::Write
关键的写日志接口
1 |
|
主要逻辑:
- 安全检测, 如果已经调用了 close 方法,直接 return
- 如果打开了控制台输出,调用输出
- 判断是否有递归调用,出现递归调用层数大于 等于 2, 输出错误信息到控制台,大于 10, 直接 return
- 没有递归调用
- 如果是同步写, 调用 __WriteSync
- 如果是异步写, 调用 __WriteAsync
__WriteAsync
1 |
|
主要逻辑:
- 根据 XLoggerInfo 将 _log 格式化, 形式是
level + timestamp + [pid, threadId(*is main thread)] + tag + [fileName, line, funcName] + message
- 当 log_buff_ 的长度 大于等于 4/5 时, 该条日志不会写入, 被替换成一条 fatal 的信息日志
- 将格式化的日志写入log_buff_中
- 如果当前log_buff_日志长度大于 1/3,或者当前是一条等级为 kLevelFatal 的日志。条件变量通知
XloggerAppender::__AsyncLogThread
, 将 log_buff_ 的日志刷到文件里。
XloggerAppender::__AsyncLogThread
1 |
|
线程启动时机 open -> SetMode async -> thread start
主要逻辑:
- white 循环死循环,等待条件变量
- 进入循环
- 加锁
- log_buff_ 内容 flush 到 tmp 中
- 调用 __Log2File 将 tmp 写入文件
- 如果 已经 close, 跳出循环,结束线程
- 没有 close, 继续等待下一次唤醒
__WriteSync
1 |
|
- 格式化
- 写入 log_buff_
- log_buff_ 写入文件
选择异步还是同步
文件写入模式,分异步和同步,变量定义见 appender.h 里 kAppednerXX, Release版本一定要用 kAppednerAsync, Debug 版本两个都可以,但是使用 kAppednerSync 可能会有卡顿。
当日志写入模式为异步时,调用接口会把内存中的日志写入到文件。appender_flush_sync 为同步 flush,flush 结束后才会返回。 appender_flush 为异步 flush,不等待 flush 结束就返回。
__Log2File
1 |
|
主要的就是
- 打开文件,准备写入
__OpenLogFile
- 写入文件
__WriteFile
- 写入文件完成,关闭文件
__CloseLogFile
__OpenLogFile
1 |
|
主要逻辑
1 |
|
- 获取 log 文件路径
- __MakeLogFileName 内部处理了文件切换的逻辑,判断需要切换文件,就生成新的文件名
- 打开文件
__MakeLogFileName
1 |
|
主要逻辑:
log 文件名生
- 如果 max_file_size_ > 0, log 会分开写入多个文件,每个文件限制 max_file_size_ 大小
- 文件命名会包含 index, index 递增。当前文件大小达到 max_file_size_, index ++
__GetNextFileIndex
1 |
|
主要逻辑:
- 将 cache 和 log 目录下, 对应的 log 文件递减排序, 栈顶部是最新写入的 log 文件
- 判断是否需要切分到下个文件,如果需要切分,index+1, 否则返回当前 index
__WriteFile
1 |
|
主要就是调用 fwrite 写文件。
__CloseLogFile
1 |
|
调用 fclose 关闭文件。
1 |
|
上面看到异步写入,写入之后关闭了文件。
压缩和加密是怎么做的
我们选定 zlib 压缩模式, 关联的 logbuffer 就是 LogZlibBuffer
1 |
|
LogZlibBuffer 继承自 LogZlibBuffer, 重写了 compress 和 flush 方法 。
因此 LogZlibBuffer 调用 write 方法写入的时候做了压缩和加密,看代码
1 |
|
写入的时候,先压缩再加密, 保存到内部的 buff_ 中。
看一下 log_crypt_->UpdateLogLen
1 |
|
上面说到 buffer 头部格式
1 |
|
崩溃了, mmap 写入文件,下次重新进入,重新 mmap 也能拿到之前数据长度。
看一下 flush
1 |
|
主要逻辑:
将 logBuffer 中存的 header + 压缩加密的数据, 读取到 AutoBuffer 中。
读取完毕,清理 logBuffer。
总结
- xlog 使用了 mmap 技术来解决崩溃丢日志的问题
- 支持压缩,加密
- 支持单文件和根据大小切分多文件
- 支持同步写异步写,release 模式要开启异步模式