前言
mars 是微信官方的终端基础组件,是一个使用 C++ 编写的业务性无关,平台性无关的基础组件。目前已接入微信 Android、iOS、Mac、Windows、WP 等客户端。现正在筹备开源中,它主要包括以下几个部分:
comm:可以独立使用的公共库,包括 socket、线程、消息队列等
xlog:可以独立使用的日志模块
sdt:可以独立使用的网络诊断模块
stn:可以独立使用的信令分发网路模块
本文章是 mars 系列的第一篇:高性能跨平台日志模块。
正文
对于移动开发者来说,最大的尴尬莫过于用户反馈程序出现问题,但因为不能重现且没有日志无法定位具体原因。这样看来客户端日志颇有点“养兵千日,用兵一时”的感觉,只有当出现问题且不容易重现时才能体现它的重要作用。为了保证关键时刻有日志可用,就需要保证程序整个生命周期内都要打日志,所以日志方案的选择至关重要。
常规方案
方案描述: 对每一行日志加密写文件
例如 Android 平台使用 java 实现日志模块,每有一句日志就加密写进文件。这样在使用过程中不仅存在大量的 GC,更致命的是因为有大量的 IO 需要写入,影响程序性能很容易导致程序卡顿。选择这种方案,在 release 版本只能选择把日志关掉。当有用户反馈时,就需要给用户重新编一个打开日志的安装包,用户重新安装重现后再通过日志来定位问题。不仅定位问题的效率低下,而且并不能保证每个需要定位的问题都能重现。这个方案可以说主要是为程序发布前服务的。
来看一下直接写文件为什么会导致程序卡顿
当写文件的时候,并不是把数据直接写入了磁盘,而是先把数据写入到系统的缓存(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 堆栈辅助定位问题,所以丢日志的问题这个时候显得尤为凸显。