有赞移动日志实践_Java作者:MC & 川普团队:零售移动


背景

日志系统,是移动端定位排查线上问题非常有效的一个工具。以往商家使用App出现问题,向客服咨询时,客服需要详细收集商家的问题信息、店铺信息(操作步骤、操作视频等),然后提交工单反馈给开发,开发再根据这些信息进行问题定位。这个过程中反复沟通的时间成本无法避免,商家与客服在沟通时也存在信息遗漏与缺失。随着业务的不断扩张,业务的复杂度不断加深,当用户达到一定的量级时,仅靠客服在商家和开发之间反复沟通,显然不能满足各个业务开发同学的需要,也无法快速定位问题。

挑战

我们亟需一个完善的日志平台来作为商家与开发间沟通的管道,期望在接入日志平台后,可以解决上述问题。一个完善的日志平台,需要:提供移动端便捷的接入方式、保证日志安全写入、保障日志拉取稳定性、定期进行缓存清除、提供账号|店铺|业务级别的日志查找及筛选功能。在经过多次的迭代、测试、优化后,有赞日志平台于2019年上线。

有赞日志平台


有赞移动日志实践_Java_02有赞日志平台架构图。底层依赖有赞的通用服务,比如监控告警平台,消息平台。后端服务包含了基本信息聚合、日志回捞管理以及商家上传后的日志管理。前端页面包含了商家设备查询、日志回捞入口以及日志筛选入口。移动端的日志库ZanLogger,依赖了有赞移动的长链接库、设备信息库、配置中心库、网络库以及安全库。

1.1 日志信息

日志信息包含基本信息和业务信息。基本信息包含商家的店铺基本信息、软件版本、系统版本以及网络状况等内容。ZanLogger帮业务方做了写入处理。基本信息只写入一次,当基本信息内容发生变动,才会再次写入日志文件,减少冗余日志内容。业务信息是各个业务方的开发写在代码中的埋点信息。

1.2 日志上报

主动上报

商家在App的设置页操作,主动上报日志。

回捞日志

各个业务开发的同学,收到客服反馈的问题,可以日志平台,通过手机号或者店铺查询到该用户使用的设备,拉取该设备的日志文件。

回捞方式

有赞移动日志实践_Java_03长链接通知App。当App在活跃状态时,收到日志平台长链接通知,会打包日志内容上传至日志平台。冷启动请求接口,判断是否需要上传日志。弥补收到长链接通知时,商家App不在线的情况。提高日志回捞的到达率。推送通知App。接入App的推送功能,开发回捞日志时,当长链接无法建立连接,增加推送通知的能力。

日志级别

ZanLogger日志库对日志进行了分级。分为log.d(debug日志)、log.i(info日志)、log.w(warning日志)、log.e(error日志)四个级别。日志平台接入了有赞的监控告警平台,error级别的日志会上传到告警平台,通过短信、电话、企业微信、邮件的方式通知开发的同学。

1.3 日志 SDK

性能卡顿

传统方式写入每一条日志,都直接写入到文件。I/O读写是一个耗时的过程,操作系统使用了页缓存机制,系统会在写入时伴随着 用户态-->内核态-->文件的过程,并且会进行两次的拷贝。由于日志需要非常频繁的写入,应用消耗大量I/O资源,导致GC频繁发生,即使在子线程工作,也会造成应用卡顿。ZanLogger将日志的写入下沉到Native-c++,使用 mmap 内存映射,写入时直接操作内存,不需要经过内核空间的数据缓存,只使用一次数据拷贝,减少了大量I/O损耗,并且也大大加快了写入速度。

性能对比

在Android性能测试中,以java写入和mmap写入进行对比,我们分别对一千组、一万组数据进行写入实验。测试结果中,java写入是mmap写入时间的3倍以上。内存状况如下:

有赞移动日志实践_Java_04

java写入GC非常频繁,存在非常严重的内存抖动:

有赞移动日志实践_Java_05

mmap则表现的非常平滑:

有赞移动日志实践_Java_06

日志完整性

在日志系统开发中,日志丢失问题也是需要重点关注和解决的,通常日志丢失有以下几种情况:

  1. 用户强制退出应用

  2. 应用意外退出,其中包括由于crash闪退、内存不足,系统回收等情况

  3. 由于日志系统自身缺陷导致无法写入或写入不全

ZanLogger引用了mmap来防止日志丢失。mmap是linux提供的函数,将一个文件或者其它对象映射进内存,并且在应用退出、内存不足时触发缺页中断,将缓存回写到硬盘。利用这个特性解决应用退出时的日志完整性。即使使用mmap也可能日志写入不全。如果发生这种情况,可能会导致日志解密失败。我们采用了日志分片和自定的二进制格式来解决,将长的日志分割成短的切片,并且将切片按照二进制格式来存储,二进制主要记录切片信息和校验。在解密时对日志进行拼接和校验,如果校验通过,则对该数据解密,校验不通过,则跳过这一条。

文件二进制格式

前面有提到文件的二进制格式,日志以二进制的协议进行存储。每个日志文件内容分为文件头和内容区两个部分,文件头用于存放日志日期、版本号、加密方式等信息,内容区用于存放加密后的日志。在解密时,先获取文件头的内容,解析出加密方式、秘钥等参数,再使用这些参数解密内容区的内容,从而解密出原始的日志。

安全性

日志会记录用户的一些重要信息,安全性必须得到保障,所以不能以明文存储的方式。目前主流的日志库大多都使用对称加密的形式,在java层初始化时会将密钥以明文的形式传递,反编后可以非常容易的获得秘钥,其加密方式也比较单一。ZanLogger抛弃传统的加密方式,使用了一种更加安全的方案。

对称秘钥生成

出于对性能的考虑,我们在对日志加密时依然使用对称加密的方式。不过我们并没有使用固定的秘钥,而是使用动态生成的。我们对生成秘钥的要求是不同设备不同时间的秘钥都不一致,日志以天为单位记录,所以需要保证每一天的秘钥都不一样。在生成秘钥时,需要将设备标志、时间和一些混淆参数,通过一系列计算生成的。考虑到Android和iOS获取设备信息的方式不同,在实现中,我们仅提供一个钩子函数。由两端的bridge模块自行提供算法。

//初始化密钥生成句柄
char *secretKeyGenerator(int version){}
static void initSecretKeyGenerator(char *(*secretKeyGenerator)(int));

在获取到bridge生成的 char* 值后,会根据具体的对称算法,对值进行再次处理以达到算法要求的秘钥格式。

对称秘钥存储

在完成秘钥生成后,也需要将秘钥存放到日志文件中,否则无法解密。首先考虑到将秘钥加密后存放到文件头中,如果使用对称加密,则需要一个固定的秘钥来加密。但这相比于传统方式,只是多了一次加密,没有什么意义,同样也不安全。所以我们使用非对称加密的形式。通过后端下发公钥和对应的公钥ID,在通过提供的公钥对秘钥进行加密,将加密后的数据和公钥ID共同存入文件头中,在上传后,后端取出公钥ID和加密数据,通过公钥ID拿到其私钥,再通过私钥解密出原始秘钥,最后使用该秘钥解密出原始日志。

1.4 特色功能

支持自定义沙盒文件和配置文件的上传 比如交易链路很长,当定位线上问题时,单纯的靠写入的日志内容,已经不能满足排查问题的需求。所以上传用户的数据库文件,结合日志文件,才能更好的定位线上问题。(沙盒文件上传的同时会和日志文件一起打包上传。) 用户可以手动勾选需要上传的数据库文件。最终数据库文件和日志文件会一起压缩上传到后台。如需上传其他文件可由开发者提供具体的json字符串,用户复制到“自定义”输入框内,即可完成其他沙盒文件的打包上传。数据库文件可使用简写。相关文件具体路径会自动匹配(包括“.sqlite-wal”和“.sqlite-shm”)。日志回捞,也支持这一功能。在日志平台填写文件的名称即可。

1.5 线上效果

作为移动端基础日志库。现在,ZanLogger已经接入有赞微商城、有赞零售、有赞精选、有赞美业、有赞会议等App,日志种类也更加丰富。客服提交工单,开发根据工单上的用户id拉取日志,分析解决问题,大大降低了沟通的成本。


未来展望

日志分析

针对平台的日志内容,进行初步的分析与处理,给出分析结果,辅助开发者定位分析业务问题。

日志多维度存储

目前日志时按照时间顺序储存,每天app产生的日志信息非常多,难以定位到需要的位置,为了更方便的分析日志。将会采取按日志类型归类存储,比如将error,warning,debug日志分文件进行存储。