微信终端跨平台组件 mars 系列(一):高性能日志模块 xlog

  在实际实践中,Android 可以使用共享内存做中间 buffer 防止丢日志,但其他平台并没有太好的办法,而且 Android 4.0 以后,大部分手机不再有权限使用共享内存,即使在 Android 4.0 之前,共享内存也不是一个公有接口,使用时只能通过系统调用的方式来使用。所以这个方案仍然存在不足:

  如果损坏一部分数据虽然不会累及整个日志文件但会影响整个压缩块

  个别情况下仍然会丢日志,而且集中压缩会导致 CPU 短时间飙高

  通过这个方案,可以看出日志不仅要保证程序的 流畅性 ,还要保证日志内容的 完整性 和 容错性 :

  不能因为程序被系统杀掉,或者发生了 crash, crash 捕捉模块没有捕捉到导致部分时间点没有日志, 要保证程序整个生命周期内都有日志。

  不能因为部分数据损坏就影响了整个日志文件,应该最小化数据损坏对日志文件的影响。

  mars 的日志模块xlog

  前面提到了使用内存做中间 buffer 做日志可能会丢日志,直接写文件虽然不会丢日志但又会影响性能。所以亟需一个既有直接写内存的性能,又有直接写文件的可靠性的方案,也就是 mars 在用的方案。

  mmap

  mmap 是使用逻辑内存对磁盘文件进行映射,中间只是进行映射没有任何拷贝操作,避免了写文件的数据拷贝。操作内存就相当于在操作文件,避免了内核空间和用户空间的频繁切换。

物联网

  为了验证 mmap 是否真的有直接写内存的效率,我们写了一个简单的测试用例:把512 Byte的数据分别写入150 kb大小的内存和 mmap,以及磁盘文件100w次并统计耗时

物联网

  从上图看出mmap几乎和直接写内存一样的性能,而且 mmap 既不会丢日志,回写时机对我们来说又基本可控。 mmap 的回写时机:

  内存不足

  进程 crash

  调用 msync 或者 munmap

  不设置 MAP_NOSYNC 情况下 30s-60s(仅限FreeBSD)

  如果可以通过引入 mmap 既能保证高性能又能保证高可靠性,那么还存在的其他问题呢?比如集中压缩导致 CPU 短时间飙高,这个问题从上个方案就一直存在。而且使用 mmap 后又引入了新的问题,可以看一下使用 mmap 之后的流程:

物联网

  前面已经介绍了,当程序被系统杀掉会把逻辑内存中的数据写入到 mmap 文件中,这时候数据是明文的,很容易被窥探,可能会有人觉得那在写进 mmap 之前先加密不就行了,但是这里又需要考虑,是压缩后再加密还是加密后再压缩的问题,很明显先压缩再加密效率比较高,这个顺序不能改变。而且在写入 mmap 之前先进行压缩,也会减少所占用的 mmap 的大小,进而减少 mmap 所占用内存的大小。所以最终只能考虑:是否能在写进逻辑内存之前就把日志先进行压缩,再进行加密,最后再写入到逻辑内存中。问题明确了:就是怎么对单行日志进行压缩,也就是其他模块每写一行日志日志模块就必须进行压缩。

  压缩

  比较通用的压缩方案是先进行短语式压缩, 短语式压缩过程中有两个滑动窗口,历史滑动窗口和前向缓存窗口,在前向缓存窗口中通过和历史滑动窗口中的内容进行匹配从而进行编码。

物联网

  比如这句绕口令:吃葡萄不吐葡萄皮,不吃葡萄倒吐葡萄皮。中间是有两块重复的内容“吃葡萄”和“吐葡萄皮”这两块。第二个“吃葡萄”的长度是 3 和上个“吃葡萄”的距离是 10 ,所以可以用 (10,3) 的值对来表示,同样的道理“吐葡萄皮”可以替换为 (10,4 )

物联网

  这些没压缩的字符通过 ascci 编码其实也是 0-255 的整数,所以通过短语式压缩得到的结果实质上是一堆整数。对整数的压缩最常见的就是 huffman 编码。通用的压缩方案也是这么做的,当然中间还掺杂了游程编码,code length 的转换。但其实这个不是关注的重点。我们只需要明白整个压缩过程中,短语式压缩也就是 LZ77 编码完成最大的压缩部分也是最重要的部分就行了,其他模块的压缩其实是对这个压缩结果的进一步压缩,进一步压缩的方式主要使用 huffman 压缩,所以这里就需要基于数字出现的频率进行统计编码,也就是说如果滑动窗口大小没上限的前提下,越多的数据集中压缩,压缩的效果就越好。日志模块使用这个方案时压缩效果可以达到 86.3%。