最近微信开源了他们的跨平台组件mars,前段时间看到他们发的 微信终端跨平台组件 mars 系列(一) - 高性能日志模块xlog 就已经有点跃跃欲试了,我们打算学习一下并尝试迁移。
在上面提到的文章中他们提出了几种方案:
微信选择了最后一种方案,关于mmap具体为什么效率高,可以参考 认真分析mmap:是什么 为什么 怎么用 ,这里面说明了mmap之所以快的主要是在读写时能够让用户空间与内核空间更容易地交互。
关于XLOG的性能比对、benchmark可以见上面的文章,或 Mars-benchmark 。
我从源码角度简单描述一下它是怎么做的,每一条日志的处理流程大约是如下流程:
简单来说就是三步:
它使用mmap映射一块固定长度的文件,这样保证每条log第一时间都被写入磁盘,由于每次将log写入目标文件时都会清空高速缓冲区,所以高速缓冲区的内容可以认为没有被写入文件,每次启动时可以检查缓冲区,若有数据则将它先写入目标文件,达到 不丢日志 的效果。这块核心的内容是在 appender.cc/.h
中,大家有兴趣可以对照看看。
当我刚看完这个代码的时候,我心里是有点疑问的,主要在于:
为什么不直接对输出log的文件进行mmap?而是写一块固定区域然后由后台线程读这块缓存区输出目标文件?
首先,我这种说法是可行的,因为 mmap 是指定目标map文件的偏移量,就可以通过代码动态扩展map起点,达到始终map 定长区域 的效果,然后直接对这块map进行输出。
由于mmap是映射固定长度区域,为保证写入顺利,每次在拓展时我们就拓展固定宽度,并且调用 ftruncate 将其使用’/0’字符填满,然后每次从第一个非0字符开始写入。
具体操作如下:
注:
MAP_LENGTH
为map的固定长度区域。每次发现剩余可写空间小于 MAP_LENGH
/2时便拓展 MAP_LENGH
/2的长度,并把offset设置为(文件大小- MAP_LENGH
/2),这样每次map区域都能满足长度为 MAP_LENGTH
,并且动态拓展文件。
但是经过我的实践,这样的效果并不好,原因主要是以下两点:
0
,这样导致了log文件末尾会有多余的 0
。 让数据说话吧,在迁移xlog之后,我尝试了一下直接map目标文件,动态拓展map的策略,比对xlog方案、Java缓存方案,连续打1000条日志(大约620kb内容),平均5次,几个方案比对下来性能开销如下:
可以看出,确实直接输出目标文件的mmap效率 是不好的 。
要将xlog迁移过来有点麻烦,主要是它带有很多依赖是我不想要的:
其实它的核心代码不多,所以我决定放弃迁移,学习思路就好了。但是它里面有很多可以借鉴、甚至直接拷贝的好轮子,比如其中的buffer系列:
ptrbuffer.cc/.h
它用于写入、读取一块 固定长度 的内存区; autobuffer.cc/.h
它用于写入、读取一块 可变长度 的内存区,它的长度会适应刷入的数据,并且 没有尾部的额外空数据 ; 这两个buffer都是可通用的,它们将内存地址偏移量的各种用法封装地很棒。
在xlog中主要操作的是 log_buffer
,它的作用主要是封装统一接口。因为当mmap失败时,需要使用内存缓存来折中处理,它与mmap的操作相同,都是基于某块内存进行操作。这个 log_buffer
就将对内存的操作交由ptrbuffer,并可以将内容flush到一块autobuffer上。
在需要将内容写到目标文件时,它会通知后台线程将内容刷到autobuffer上,去除尾部的空数据,然后将其写至文件。
在 log_buffer
中写入数据时封装了加密的操作,具体加密实现由 log_crypt.cc/.h
完成,也是可以直接迁移的工具类。
这个思路比较清晰,所以迁移时只要将上面两个可以移植的buffer迁移过来,如果需要加密再移植一下 log_crypt.cc
,基本上很快就就可以写出一套像模像样的xlog出来。