【腾讯Bugly干货分享】微信终端跨平台组件 mars 系列(一) - 高性能日志模块xlog

本文来自于腾讯bugly开发者社区,非经做者赞成,请勿转载,原文地址:http://dev.qq.com/topic/57ff5932cde42f1f03de29b1java

本文来源: 微信客户端开发团队算法

前言

mars 是微信官方的终端基础组件,是一个使用 C++ 编写的业务性无关,平台性无关的基础组件。目前已接入微信 Android、iOS、Mac、Windows、WP 等客户端。现正在筹备开源中,它主要包括如下几个部分:缓存

  1. comm:能够独立使用的公共库,包括 socket、线程、消息队列等
  2. xlog:能够独立使用的日志模块
  3. sdt:能够独立使用的网络诊断模块
  4. stn:能够独立使用的信令分发网路模块

本文章是 mars 系列的第一篇:高性能跨平台日志模块。安全

正文

对于移动开发者来讲,最大的尴尬莫过于用户反馈程序出现问题,但由于不能重现且没有日志没法定位具体缘由。这样看来客户端日志很有点“养兵千日,用兵一时”的感受,只有当出现问题且不容易重现时才能体现它的重要做用。为了保证关键时刻有日志可用,就须要保证程序整个生命周期内都要打日志,因此日志方案的选择相当重要。微信

常规方案

方案描述: 对每一行日志加密写文件网络

例如 Android 平台使用 java 实现日志模块,每有一句日志就加密写进文件。这样在使用过程当中不只存在大量的 GC,更致命的是由于有大量的 IO 须要写入,影响程序性能很容易致使程序卡顿。选择这种方案,在 release 版本只能选择把日志关掉。当有用户反馈时,就须要给用户从新编一个打开日志的安装包,用户从新安装重现后再经过日志来定位问题。不只定位问题的效率低下,并且并不能保证每一个须要定位的问题都能重现。这个方案能够说主要是为程序发布前服务的。app

来看一下直接写文件为何会致使程序卡顿socket

当写文件的时候,并非把数据直接写入了磁盘,而是先把数据写入到系统的缓存(dirty page)中,系统通常会在下面几种状况把 dirty page 写入到磁盘:工具

  • 定时回写,相关变量在/proc/sys/vm/dirty_writeback_centisecs和/proc/sys/vm/dirty_expire_centisecs中定义。
  • 调用 write 的时候,发现 dirty page 占用内存超过系统内存必定比例,相关变量在/proc/sys/vm/dirty_background_ratio( 后台运行不阻塞 write)和/proc/sys/vm/dirty_ratio(阻塞 write)中定义。
  • 内存不足。

数据从程序写入到磁盘的过程当中,其实牵涉到两次数据拷贝:一次是用户空间内存拷贝到内核空间的缓存,一次是回写时内核空间的缓存到硬盘的拷贝。当发生回写时也涉及到了内核空间和用户空间频繁切换。 dirty page 回写的时机对应用层来讲又是不可控的,因此性能瓶颈就出现了。性能

这个方案存在的最主要的问题:由于性能影响了程序的流畅性。对于一个 App 来讲,流畅性尤其重要,由于流畅性直接影响用户体验,最基本的流畅性的保证是使用了日志不会致使卡顿,可是流畅性不只包括了系统没有卡顿,还要尽可能保证没有 CPU 峰值。因此一个优秀的日志模块必须保证流畅性

  • 不能影响程序的性能。最基本的保证是使用了日志不会致使程序卡顿

我以为绝大部分人不会选择这一个方案。

进一步思考

在上个方案中,由于要写入大量的 IO 致使程序卡顿,那是否能够先把日志缓存到内存中,当到必定大小时再加密写进文件,为了进一步减小须要加密和写入的数据,在加密以前能够先进行压缩。至于 Android 下存在频繁 GC 的问题,可使用 C++ 来实现进行避免,并且经过 C++ 能够实现一个平台性无关的日志模块。

方案描述:把日志写入到做为 log 中间 buffer 的内存中,达到必定条件后压缩加密写进文件。

这个方案的总体的流程图:

这个方案基本能够解决 release 版本由于流畅性不敢打日志的问题,而且对于流畅性解决了最主要的部分:因为写日志致使的程序卡顿的问题。可是由于压缩不是 realtime compress,因此仍然存在 CPU 峰值。但这个方案却存在一个致命的问题:丢日志。

理想中的状况:当程序 crash 时, crash 捕捉模块捕捉到 crash, 而后调用日志接口把内存中的日志刷到文件中。可是实际使用中会发现程序被系统杀死不会有事件通知,并且不少异常退出,crash 捕捉模块并不必定能捕捉到。而这两种状况偏偏是平时跟进的重点,由于没有 crash 堆栈辅助定位问题,因此丢日志的问题这个时候显得尤其凸显。

在实际实践中,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%。

既然 LZ77 编码已经完成了大部分压缩,那么是否能够弱化 huffman 压缩部分,好比使用静态 huffman 表,自定义字典等。因而咱们测试了四种方案:

这里能够看出来后两种方案明显优于前两种,压缩率均可以达到 83.7%。第三种是把整个 app 生命周期做为一个压缩单位进行压缩,若是这个压缩单位中有数据损坏,那么后面的日志也都解压不出来。但其实在短语式压缩过程当中,滑动窗口并非无限大的,通常是 32kb ,因此只须要把必定大小做为一个压缩单位就能够了。这也就是第四个方案, 这样的话即便压缩单位中有部分数据损坏,由于是流式压缩,并不影响这个单位中损坏数据以前的日志的解压,只会影响这个单位中这个损坏数据以后的日志。

对于使用流式压缩后,咱们采用了三台安卓手机进行了耗时统计,和以前使用通用压缩的的日志方案进行了对比(耗时为单行日志的平均耗时):

经过横向对比,能够看出虽然使用流式压缩的耗时是使用多条日志同时压缩的 2.5 倍左右,可是这个耗时自己就很小,是微秒级别的,几乎不会对性能形成影响。最关键的,多条日志同时压缩会致使 CPU 曲线短期内极速升高,进而可能会致使程序卡顿,而流式压缩是把时间分散在整个生命周期内,CPU 的曲线更平滑,至关于把压缩过程当中使用的资源均分在整个 app 生命周期内。

xlog 方案总结

该方案的简单描述:

使用流式方式对单行日志进行压缩,压缩加密后写进做为 log 中间 buffer的 mmap 中

虽然使用流式压缩并无达到最理想的压缩率,但和 mmap 一块儿使用能兼顾流畅性 完整性 容错性 的前提下,83.7%的压缩率也是能接受的。使用这个方案,除非 IO 损坏或者磁盘没有可用空间,基本能够保证不会丢失任何一行日志。

在实现过程当中,各个平台上也踩了很多坑,好比:

  • iOS 锁屏后,由于文件保护属性的问题致使文件不可写,须要把文件属性改成 NSFileProtectionNone。

  • boost 使用 ftruncate 建立的 mmap 是稀疏文件,当设备上无可用存储时,使用 mmap 过程当中可能会抛出 SIGBUS 信号。经过对新建的 mmap 文件的内容全写'0'来解决。

  • ……

日志模块还存在一些其余策略:

  • 每次启动的时候会清理日志,防止占用太多用户磁盘空间
  • 为了防止 sdcard 被拔掉致使写不了日志,支持设置缓存目录,当 sdcard 插上时会把缓存目录里的日志写入到 sdcard 上
  • ……

在使用的接口方面支持多种匹配方式:

  • 类型安全检测方式:%s %d 。例如:xinfo(“%s %d”, “test”, 1)
  • 序号匹配的方式:%0 %1 。例如:xinfo(TSF”%0 %1 %0”, “test”, 1)
  • 智能匹配的懒人模式:%_ 。例如:xinfo(TSF”%_ %_”, “test”, 1)

总结

对于终端设备来讲,打日志并不仅是把日志信息写到文件里这么简单。除了前文提到的流畅性 完整性 容错性,还有一个最重要的是安全性。基于不怕被破解,但也不能任何人都能破解的原则,对日志的规范比加密算法的选择更为重要,因此本文并无讨论这一点。

从前面的几个方案中能够看出,一个优秀的日志模块必须作到:

  • 不能把用户的隐私信息打印到日志文件里,不能把日志明文打到日志文件里。
  • 不能影响程序的性能。最基本的保证是使用了日志不会致使程序卡顿。
  • 不能由于程序被系统杀掉,或者发生了 crash,crash 捕捉模块没有捕捉到致使部分时间点没有日志, 要保证程序整个生命周期内都有日志。
  • 不能由于部分数据损坏就影响了整个日志文件,应该最小化数据损坏对日志文件的影响。

上面这几点也即安全性 流畅性 完整性 容错性, 它们之间存在着矛盾关系:

  • 若是直接写文件会卡顿,但若是使用内存作中间 buffer 又可能丢日志
  • 若是不对日志内容进行压缩会致使 IO 卡顿影响性能,但若是压缩,部分损坏可能会影响整个压缩块,并且为了增大压缩率集中压缩又可能致使 CPU 短期飙高。

mars 的日志模块 xlog 就是在兼顾这四点的前提下作到:高性能高压缩率、不丢失任何一行日志、避免系统卡顿和 CPU 波峰。


更多精彩内容欢迎关注bugly的微信公众帐号:

腾讯 Bugly是一款专为移动开发者打造的质量监控工具,帮助开发者快速,便捷的定位线上应用崩溃的状况以及解决方案。智能合并功能帮助开发同窗把天天上报的数千条 Crash 根据根因合并分类,每日日报会列出影响用户数最多的崩溃,精准定位功能帮助开发同窗定位到出问题的代码行,实时上报能够在发布后快速的了解应用的质量状况,适配最新的 iOS, Android 官方操做系统,鹅厂的工程师都在使用,快来加入咱们吧!

相关文章
相关标签/搜索