货拉拉客户端通用日志组件 - Glog

Glog 是货拉拉移动端监控系统中的日志存储组件,Glog 意即 General log - 通用日志。为了满足我们对日志格式的多种需求,我们在存储方式、归档方式上做了一些探索和实践,使得 Glog 的通用性和性能优于常见的日志方案。Glog 已经在货拉拉全线 App 中稳定运行了 1 年多,现在将其开源,我们希望 Glog 的开源能够为移动开发者提供一种更加通用的日志方案,同时希望 Glog 能够从社区中汲取养分,不断得到优化和完善。GitHub 地址:https://github.com/huolalatech/hll-wp-glog

背景简介

移动端日志系统通常来说,主要的目标是辅助开发同学排查线上问题,这些问题包括但不限于

  1. 客诉渠道反馈的 App 使用问题;
  2. Crash 系统上报的崩溃问题;
  3. 其他线上冒烟问题。

为了能够尽快定位问题,我们希望能够快速、详细的还原问题现场,这就需要在代码中 App 运行的关键节点埋入日志,将出现问题时的运行状态快速上报。这对日志系统提出了两个关键的要求,信息完整性以及实时性。 在移动端,公司之前存在一套简单的日志系统,收集的日志信息比较有限,我们通过 App 的常见使用流程来看其覆盖的关键节点

old-log-cover

另外,之前的日志系统只能通过下发任务回捞,实时性较差,还存在 I/O 性能等问题。

为了解决这些问题,我们开发了新的移动端日志系统,覆盖了上面 App 使用流程的全节点信息

new-log-cover

另一方面,为了提升日志的实时性,我们增加了实时日志,通过短轮询来进行定时上报,此外还补充了监控日志来支撑 App 的横向数据对比、评估 App 的性能指标,最终的方案如下

log-system

系统主要组成包括

  • Android/iOS 上层日志采集 SDK
  • 跨平台的储存组件 Glog
  • 负责日志存储过滤的日志后端
  • 负责日志展示的日志前端

新的监控系统包括实时性要求较高的实时日志,信息较完整的离线日志以及为大盘数据服务的监控日志

  • 实时日志,快速上传,信息精简,能够接近实时的查看,快速定位、排查用户反馈的问题;
  • 离线日志,通过后台任务触发上传,按天归档,作为实时日志的兜底,要求信息完整详尽;
  • 监控日志,支持采样,作为监控大盘的信息源,实时性要求最高,日志只包括监控信息。

为了适配不同日志的存储格式,我们希望存储组件能够在格式上尽量通用,最好做到格式无关;另一方面我们也希望其性能、可靠和安全方面能够对齐一线水平,在调研了市面上流行的日志组件后,我们发现并没有现成方案满足我们的需求,因此我们自研了自己的日志存储组件 Glog。

glog-arch

方案概览

应用上层对不同类型的日志序列化(推荐 Protobuf)之后,将二进制数据存储到 Glog,对于上传频次较高的实时日志和监控日志,采用重命名缓存的方式快速归档;对于信息较全而上传频次不高的离线日志,我们采用 mmap 偏移映射的方式归档,相较标准 I/O 复制归档的方式,提升了性能。在可靠性和安全性方面我们也借鉴了当前的流行方案,例如 mmap I/O 提升性能和可靠性、流式的加密和压缩防止 CPU 突发峰值,另外我们在日志中加入了同步标记支持读取容错。

存储方式

为了适应不同的日志格式,Glog 存储二进制数据,上层依据自己的需要,将数据序列化后交给 Glog

glog-flow

具体的文件格式:使用 2 个字节描述每条日志长度,在每条日志末尾加入一个同步标志,用于文件损坏时的读取容错。

glog-format

归档方式

回顾一下常见的日志组件中 mmap 的使用方式,首先 mmap I/O 需要映射一片大小为 page size (通常为 4KB) 整数倍大小的缓存,随着数据的写入将这片空间耗尽后,我们无法持续扩展这片空间的大小(由于它占用的是 App 的运行内存空间),因此需要将其中的数据归档,常见的方式是将其中内容 flush 追加到另一个归档文件当中,之后再清空 mmap 缓存,这个 flush 的过程一般使用标准 I/O

normal-format

而我们的实时、监控日志为了快速上传保证数据实时性,采用间隔较短的轮询来触发 flush 并上传,这将导致 flush 频率变得很高;而通常的 flush 方式采用标准 I/O 来复制数据,性能相对较低,后续的日志写入需要等待 flush 完成,这将影响我们的写入性能,因此我们考虑两种方案来提升 flush 速度以优化写入性能

  1. mmap 偏移映射,通过 mmap 映射归档文件的末尾,之后通过内存拷贝将 mmap 缓存追加到归档文件末尾。这种方式将文件复制变成内存复制,性能较好。
  2. 文件重命名,对于可以快速上传并删除的日志,我们可以在需要时将 mmap 缓存重命名成归档文件,之后重建缓存。这种方式直接去除了复制的环节,但是在日志量较大时,可能产生很多零碎的归档文件。
glog-archive

这两种方案可以在我们的不同日志场景应用,对于实时、监控日志来说,对性能要求最高,选用第 2 种方案,这个方案带来的零碎归档文件问题,由于上传和删除较快,在这里并不会堆积,另一方面,考虑到实时、监控日志上传周期较短,零碎的归档文件也便于控制上传的数据量;而离线日志选用第 1 种方案,可以将每天的日志归档在一个文件中,相对常见的标准 I/O 也有性能上的优势。

加密方式

Glog 使用了 ECDH + AES CFB-128,对每条日志进行单独加密。具体来说通过 ECDH 协商加密秘钥,之后 AES CFB-128 进行对称加密。

glog-encrypt

选择 CFB-128 是因为 AES 通用性和安全性较好,加解密只需执行相同块加密算法,对 IV 随机性要求低,ECC 相对 RSA 在加密强度相同的前提下,秘钥更短。

Security(In Bits) RSA Key Length Required(In Bits) ECC Key Length Required(In Bits)
80 1024 160-223
112 2048 224-255
128 3072 256-383
192 7680 384-511
256 15360 512+

压缩方式

考虑到解压缩的便捷性和通用性,Glog 使用了常见的 Deflate 无损压缩算法,对日志进行流式压缩,即以每条日志为压缩单元,在每次写入时进行同步压缩。这样避免了归档时对整个 mmap 缓存做压缩带来的 CPU 波峰,具体的原理下面做一些解释。

Deflate 算法是 LZ77 与哈夫曼编码的组合

LZ77

LZ77 将数据(短语)通过前向缓冲区,然后移动到滑动窗口中成为字典的一部分,之后从字典中搜索能与前向缓冲区匹配的最长短语,如果能够命中,则成为短语标记作为结果保存起来,不能命中则作为字符标记保存。 解压时,如果是字符标记则直接拷贝到滑动窗口中,如果是短语标记则在滑动窗口中查找相应的偏移量,之后将滑动窗口中相应长度的短语拷贝到结果中。

短语标记包括了

  1. 滑动窗口中的偏移量
  2. 匹配命中的字符长度
  3. 匹配结束后前向缓冲区的第一个字符

下面展示了对字符 LABLALALABLA 进行 LZ77 压缩和解压缩的过程,

glog-lz77

接下来霍夫曼编码对 LZ77 的处理结果(包括前面提到的偏移量、长度、字符),按照出现频率越高,占用空间越少的方式进行编码存储。 在简要说明原理之后,我们知道影响压缩率的几个因素:滑动窗口(字典)大小,输入的数据(短语)长度、以及短语中字符的重复率。字典越大、短语越长,越容易从字典中找到匹配短语进而变成短语标记,那么流式压缩以每条日志作为压缩单元,输入数据长度变短,我们如何保证压缩率呢? 这里我们能做的是尽量保证字典的大小,不频繁重置字典,具体做法是只在 mmap 缓存归档时重置字典,对于归档前 mmap 缓存的数据,复用字典来保证压缩率。

消息队列

mmap 相对标准 I/O 在性能上有较大优势,主要是由于其减少了内核空间与用户空间的拷贝、以及 write lseek 系统调用带来的上下文切换开销

glog-mmap

但在系统资源不足时 mmap 仍有可能出现性能问题,举个例子,我们知道 mmap 与标准 I/O 一样也需要通过 Page Cache 回写到磁盘

Page Cache 的生命周期: 当用户通过标准 I/O 从用户缓冲区向内核空间拷贝数据时,如果内核缓冲区中没有这个 Page,将发生缺页中断分配一个 Page,之后拷贝数据,结束后这个 Page Cache 变成一个脏页,然后该脏页同步到磁盘中,同步结束后,这个 Page Cache 变成 Clean Page 保存在系统中。

Android 中可以通过 showmap 命令观察 mmap 写入了 Page Cache

glog-pagecache

当系统内存不足时,系统将回收 Page Cache 来释放内存,引起频繁的磁盘回写,mmap 性能也会受到影响。 另一方面由于实时日志、监控日志需要高频归档,而归档会阻塞后续的写入。因此我们在 Glog 底层加入了消息队列来处理写入和归档等操作,进一步提升性能,避免卡顿。

glog-message-queue

性能对比

手机型号 日志 SDK 1w 条日志耗时 10w 条日志耗时
Samsung Galaxy S10+ Android 11 glog 21 ms 182 ms
glog+pb 54 ms 335 ms
xlog 207 ms 1961 ms
logan 250 ms 6469 ms
Huawei Honor Magic 2 Android 10 glog 38 ms 286 ms
glog+pb 84 ms 505 ms
xlog 263 ms 2165 ms
logan 242 ms 3643 ms
Xiaomi 10 Android 11 glog 27 ms 244 ms
xlog 198 ms 1863 ms
logan 210 ms 4238 ms
Huawei Mate 40 pro HarmonyOS 2.0.0 glog 30 ms 257 ms
xlog 275 ms 2484 ms
logan 260 ms 4020 ms
OPPO R11 Android 8.1.0 glog 63 ms 324 ms
glog+pb 234 ms 1611 ms
xlog 464 ms 3625 ms
logan 430 ms 5355 ms
iPhone 12 128G iOS 14.8 glog 7 ms 29 ms
xlog 152 ms 1079 ms
logan 162 ms 12821 ms
iPhone 8 64G iOS 13.7 glog 12 ms 50 ms
xlog 242 ms 2106 ms
logan 251 ms 38312 ms

Glog 使用异步模式、按天归档

通过对比数据来看,Glog 异步模式由于使用了消息队列,即使累加上 Protobuf 的序列化时间,写入性能相对来说依然有较大优势。

遇到的问题

  • 使用 mmap 偏移映射方式拷贝数据时,需要通过 mmap 映射文件末尾,其偏移量也需要是 page size 的整数倍,而归档文件和复制数据大小通常情况下都不是 page size 的整数倍,需要做额外的计算;
  • 如果只对归档文件总体积作为阈值来清理,在重命名归档这种情况下零碎文件较多,可能在收集文件列表的过程中导致 JNI 本地引用超限,需要限制文件总个数、及时回收 JNI 本地引用;
  • 在跨天写入日志的情况下,mmap 缓存中的数据可能无法及时归档,造成部分日志误写入次日的归档文件当中,需要在归档轮询中增加时间窗口的判定;
  • 为了便于上层上传日志,在底层需要添加日志解析模块。

总结

通过上面的介绍,可以看到 Glog 相较其他流行方案的主要区别是:

  • 存储的是格式无关的二进制数据,具有更好的定制性;
  • 底层实现的消息队列,性能更优使用也更方便;
  • 新的归档方式一方面提升性能,另一方面也便于高频读取。

当然这些手段也带来了一些妥协,比如由于存储的是二进制数据,使用 Glog 需要额外添加序列化代码;异步模式下,消息队列中的任务在 Crash 或断电时可能丢失,这些问题在我们的使用场景基本可以忽略。 为了实现货拉拉的业务需求,我们参考流行的日志方案,站在巨人的肩膀上,在移动端存储组件高性能、可靠、安全的基本要求之外,提供了更多的特性和额外的优化。在开源之后,也希望能够反哺社区,为移动开发者提供一种更为通用的日志方案。

以 Glog 为存储模块的日志系统,目前已经接入了公司的全线 app,实时日志的单日日志量达到数十亿条,稳定运行在百万级别的 App 上。为线上用户反馈问题解决、App 崩溃排查提供了有力的帮助,除此之外,还为风控系统、监控大盘提供了数据支撑。

glog-app

猜你喜欢

转载自juejin.im/post/7168662263337861133