微信跨平台组件mars-xlog架构分析及迁移思路

amrg4308 8年前
   <p>最近微信开源了他们的跨平台组件mars,前段时间看到他们发的 <a href="/misc/goto?guid=4959735686805581092" rel="nofollow,noindex">微信终端跨平台组件 mars 系列(一) - 高性能日志模块xlog</a> 就已经有点跃跃欲试了,我们打算学习一下并尝试迁移。</p>    <h2>谈谈xlog</h2>    <p>在上面提到的文章中他们提出了几种方案:</p>    <ul>     <li><strong>内存缓存,缓存到一定阈值写入文件</strong> 优点是效率很高,缺点是在异常情况下 <strong>丢日志</strong> ;</li>     <li><strong>直接写入文件(普通IO)</strong> 优点是不会丢日志,但是效率低下;</li>     <li><strong>直接写入文件(mmap)</strong> 效率较高,不会丢日志,但是编程要求较高;</li>    </ul>    <p>微信选择了最后一种方案,关于mmap具体为什么效率高,可以参考 <a href="/misc/goto?guid=4959719773712755784" rel="nofollow,noindex">认真分析mmap:是什么 为什么 怎么用</a> ,这里面说明了mmap之所以快的主要是在读写时能够让用户空间与内核空间更容易地交互。</p>    <p>关于XLOG的性能比对、benchmark可以见上面的文章,或 <a href="/misc/goto?guid=4959735686926879747" rel="nofollow,noindex">Mars-benchmark</a> 。</p>    <p>我从源码角度简单描述一下它是怎么做的,每一条日志的处理流程大约是如下流程:</p>    <p><img src="https://simg.open-open.com/show/6b0e950c788494fea397192aa4d4576c.jpg"></p>    <p>简单来说就是三步:</p>    <ol>     <li>将要打的日志附上各种信息(进程、线程、日期)并格式化。</li>     <li>将日志写入高速缓冲区。这块高速缓冲区是使用mmap映射出来的内存区,被映射的磁盘文件是它新建的一个缓存文件,.mmap2后缀(若mmap失败,则用内存缓存代替)。每次打log时首先将它写入高速缓存,这样当使用mmap时可以保证这条log快速地被写入磁盘。</li>     <li>当高速缓冲区内容写到一定阈值时(此处为1/3),通知后台线程将缓冲区的内容写入文件。</li>    </ol>    <p>它使用mmap映射一块固定长度的文件,这样保证每条log第一时间都被写入磁盘,由于每次将log写入目标文件时都会清空高速缓冲区,所以高速缓冲区的内容可以认为没有被写入文件,每次启动时可以检查缓冲区,若有数据则将它先写入目标文件,达到 <strong>不丢日志</strong> 的效果。这块核心的内容是在 appender.cc\.h 中,大家有兴趣可以对照看看。</p>    <h2>一点疑问</h2>    <p>当我刚看完这个代码的时候,我心里是有点疑问的,主要在于:</p>    <p>为什么不直接对输出log的文件进行mmap?而是写一块固定区域然后由后台线程读这块缓存区输出目标文件?</p>    <p>首先,我这种说法是可行的,因为 <a href="/misc/goto?guid=4959553685798487797" rel="nofollow,noindex">mmap</a> 是指定目标map文件的偏移量,就可以通过代码动态扩展map起点,达到始终map <strong>定长区域</strong> 的效果,然后直接对这块map进行输出。</p>    <p>由于mmap是映射固定长度区域,为保证写入顺利,每次在拓展时我们就拓展固定宽度,并且调用 <a href="/misc/goto?guid=4959735687037397205" rel="nofollow,noindex">ftruncate</a> 将其使用’\0’字符填满,然后每次从第一个非0字符开始写入。</p>    <p>具体操作如下:</p>    <p><img src="https://simg.open-open.com/show/dc75cb7ace9ec844ec11da7cd4cc9d3f.jpg"></p>    <p>注:</p>    <p>MAP_LENGTH 为map的固定长度区域。每次发现剩余可写空间小于 MAP_LENGH /2时便拓展 MAP_LENGH /2的长度,并把offset设置为(文件大小- MAP_LENGH /2),这样每次map区域都能满足长度为 MAP_LENGTH ,并且动态拓展文件。</p>    <p>但是经过我的实践,这样的效果并不好,原因主要是以下两点:</p>    <ol>     <li>由于目标文件需要动态扩展长度,在map之前需要调用 <a href="/misc/goto?guid=4959735687037397205" rel="nofollow,noindex">ftruncate</a> 将其适应到对应长度,这部分会占 <strong>一定的开销</strong> ;</li>     <li>动态扩展目标文件,每次也是扩展固定的长度,这部分内容会先被填上 0 ,这样导致了log文件末尾会有多余的 0 。</li>    </ol>    <p>让数据说话吧,在迁移xlog之后,我尝试了一下直接map目标文件,动态拓展map的策略,比对xlog方案、Java缓存方案,连续打1000条日志(大约620kb内容),平均5次,几个方案比对下来性能开销如下:</p>    <p><img src="https://simg.open-open.com/show/9d98d42934dba46857d1878e265f4957.png"></p>    <p>可以看出,确实直接输出目标文件的mmap效率 <strong>是不好的</strong> 。</p>    <h2>迁移思路</h2>    <p>要将xlog迁移过来有点麻烦,主要是它带有很多依赖是我不想要的:</p>    <ol>     <li>部分boost,用于filesystem、mmap方面 => 自己手写封装</li>     <li>部分common代码,用于线程、互斥锁 => 使用c++11的 <a href="/misc/goto?guid=4959735687129293014" rel="nofollow,noindex">thread库</a> 代替</li>     <li>里面为了适配跨平台带了很多宏,我们目前只在Android上用,暂时可以去除。</li>    </ol>    <p>其实它的核心代码不多,所以我决定放弃迁移,学习思路就好了。但是它里面有很多可以借鉴、甚至直接拷贝的好轮子,比如其中的buffer系列:</p>    <ul>     <li>ptrbuffer.cc/.h 它用于写入、读取一块 <strong>固定长度</strong> 的内存区;</li>     <li>autobuffer.cc/.h 它用于写入、读取一块 <strong>可变长度</strong> 的内存区,它的长度会适应刷入的数据,并且 <strong>没有尾部的额外空数据</strong> ;</li>    </ul>    <p>这两个buffer都是可通用的,它们将内存地址偏移量的各种用法封装地很棒。</p>    <p>在xlog中主要操作的是 log_buffer ,它的作用主要是封装统一接口。因为当mmap失败时,需要使用内存缓存来折中处理,它与mmap的操作相同,都是基于某块内存进行操作。这个 log_buffer 就将对内存的操作交由ptrbuffer,并可以将内容flush到一块autobuffer上。</p>    <p>在需要将内容写到目标文件时,它会通知后台线程将内容刷到autobuffer上,去除尾部的空数据,然后将其写至文件。</p>    <p>在 log_buffer 中写入数据时封装了加密的操作,具体加密实现由 log_crypt.cc\.h 完成,也是可以直接迁移的工具类。</p>    <p>这个思路比较清晰,所以迁移时只要将上面两个可以移植的buffer迁移过来,如果需要加密再移植一下 log_crypt.cc ,基本上很快就就可以写出一套像模像样的xlog出来。</p>    <p> </p>    <p>来自:http://blog.desmondyao.com/mars-xlog/</p>    <p> </p>