既然 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 短时间飙高。