Aug 1

小记:移动端的Logger不止是Logger

Lrdcq , 2021/08/01 22:03 , 程序 , 閱讀(1045) , Via 本站原創
前几天Log4j的问题爆出,虽然和移动端无关,不少Android同学也顺带排查了下java项目中是否被误引入log4j(毕竟log4j还真有Android桥接版)。当然除了线下工具链里,线上代码是不存在log4j的,不过话说回来,同时也注意到各个项目中logger使用的混乱与标准不一致。从根因上讲,移动端的logger在解决团队问题的同时也有大量技术问题要解决,因此整理一下,一个优秀的移动端Logger,需要符合哪些期待并且目前最合理的技术方案有哪些?

系统logger的不合理之处

Android系统提供的Log与iOS提供的NSLog或者print,包括apple社区的swift-log(https://github.com/apple/swift-log),说到底定义上是往终端即标准输出写入message的过程,往终端写入东西可以帮助我们在开发测试自动化过程中解决问题,但是在移动端线上环境却无能为力。

1. 在设备标准输出的信息在用户手里,我们根本无法得到。
2. 同时在标准输出里写入过多信息反倒会让移动客户端暴露更多的业务信息与安全风险。

因此实际做法中,在线上环境实际是尽量直接屏蔽业务代码通过标准输出写入日志而是始终通过我们的移动端Logger来解决问题。对应的移动端logger业务上需要解决日志的基本功能即写入功能,同时也具备回收功能。同时技术上,由于是自行处理写入,IO性能,储存安全与回收流量将是logger实现最关心的三个部分。

需要解决的业务问题

1. 日志等级行为

不同于直接将log直接输出到标准输出,合理的情况下日志应该是分等级的,如Android中log分为Verbose冗余信息/Debug调试信息/Info普通信息/Warning警告/Error错误/WTF严重异常,swift-log分为trace追踪信息/debug调试信息/info普通信息/notice提示/warning警告/error错误/critical紧急。分级本身公说公有理婆说婆有理那么我们到底怎么来进行分级呢:根据行为倒退分级。

行为及是我们得到这个信息有需要进行的反应,日志在线上的核心作用就是反应客户端在用户侧的实际运行与异常情况从而展示客户端实际大盘可用性,同时也辅助进行个案问题排查。因此从大盘行为与个案行为两个角度区分行为:

a. 【大盘行为】不可能发生的异常,如果这样的信息在线上出现,需要立刻排查。比如某界面事件出现一个不可能为空的判空兜底逻辑,可能反应的是界面在用户交互时还没有渲染完成或者渲染失败,这样的和外界(IO网络等)大概率无关也按道理不可能发生的问题需要收到报警立刻排查。

b. 【大盘行为】可能出现的异常,但是如果出现的数量过多或者比例过高,也是需要立刻排查的。如业务登陆流程风控效验失败,一般在线上有5%的错误率是合理的,如果爆增或者爆减,显然会怀疑风控策略是否有什么异常可能导致业务损失或者用户体验爆炸。另外如技术上io错误导致kv储存失败,每天有一个万0.1的错误率还可以接受,但是如果出现增长就要考虑别的可能性了。

c. 【大盘行为】不是异常,而是某一种技术行为如用户大字体设置情况,通过技术日志的数据来描述每日日活中用户系统字体大小设置占比,最终得到一些技术方向上的结论如是否针对某个级别的大字体进行GUI优化。这种数据没必要实时上报,而是按小时甚至按日上班均可。T+1查阅结果。

d. 【个案行为】观察用户某段时间内的普通日志信息来判断用户行为是否正常。这种情况一般是casebycase的排查比如排查慢执行用户,选取TP99一用户观察其慢执行期间系统有无异常操作或者异常日志。个案排查需要大量日志,但是只有在需要的时候这些日志才需要上传。

e. 【个案行为】线下行为,比如开发测试beta版本使用的日志。这些日志除了收集系统集中起来,也可以一并输出到标准输出中,毕竟没什么安全或者法务问题用起来更方便一些。

因此,基本上实际logger的处理行为分为以上五种,因此日志等级也可以按行为划分为至少五级即:

- Fatal:致命问题,立刻上报,需立刻处理。
- Error:普通问题,立刻上报,视数据异常情况处理。
- Info:一般信息,可以延迟上报,T+1处理。
- Log:日常信息,按需上报,按需处理。
- Debug:调试信息,线上环境忽略。

实际设计logger时整个过程通过一个策略系统来分发,对不同环境不同等级的错误分发到不同的logger实现。
點擊在新視窗中瀏覽此圖片

2. 日志回收

上文中提到了日志回收有3种形式“立刻上报”,“可以延迟上报”,“按需上报”。这三种形式的日子回收也需要一定设计。核心是基于——如果每一个log就发送一个请求上报到服务端,这也太不合理了,因此事实上,实时日志回收应该是基于队列的批量上报,同时平衡数据实时性与资源占用。而离线日志回收应完全基于本地或者远端指令执行。

a. 实时日志队列

无论是“立刻上报”还是“可以延迟上报”,均压入实时日志队列,仅对数据等级做标记即可。考虑到进入这个队列的数据不应该非常多,当时如果出现严重错误,进入该队列的数据就确实可能非常多而且还必须保证上报成功(严重日志保证尽可能低的丢失率)。因此日志队列在储存的同时,更重要的是定义上报时机,必要的上报时机包括:

- 启动后在确认网络可用的情况下,30s轮询清空队列并上报,保证30s-60s左右的延迟,上报具有30s超时,失败后重试,同时如果10次失败后,丢弃该队列的日志(log级日志依然有储存,避免应该服务器问题或者网络特殊问题导致一致轮询请求占用流量资源)。

- 发送fatal级日志并且1s内无上报则立刻触发上报。fatal级日志基本意味着应用出现严重问题,虽然catch住了有可能随后会crash或者用户因为应用不可用会立刻杀死应用,1s的容错是为了避免过于集中的fatal请求上报导致直接拖垮app。

- 启动app后立刻上报队列。对于出现异常杀死app后未完成上报的日志,这个行为本质上是继续进行重试上报。

b. 日志回捞

对于其他更普通的日志,我们是直接进行了本地储存,需要遇到个案问题排查时主动上报,那么我们需要在logger中预留各种口子想方设法去回捞这些日志了。目前主流可以留口子的地方包括:

- 在设置界面-反馈问题/上报问题这类的界面中留主动触发上报的按钮。如果是主动客诉,客户可以直接引导上报并将用户信息反馈到技术侧,技术团队直接进行排查。

- 动态化引导页面,通过web容器或者移动端容器提供主动上报bridge,通过短信或者推送给用户下发信息上报的动态化页面,引导用户进入页面触发上报。

- 当然,如果推送已经可以触达主应用,或者应用本来预留了双工信道,通过信道推送上报日志的指令,能够做到技术侧发起主动回捞日志。这种方式对用户未发现或者应该无感知的异常排查更友好。
點擊在新視窗中瀏覽此圖片

需要解决的技术问题

上文与其说是描述解决的业务问题,不如说是把一个移动端logger需要完成哪些工作描述清楚了。接下来则是从技术的角度来详细描述一个合格的logger需要解决的3个关键技术问题。

1. 写入性能问题

无论是上传队列还是直接储存,我们日志都需要实时在磁盘上进行储存以保证意外情况不丢失。而我们知道移动端的系统io风险其实是相当相当高,稍有卡顿就会导致io锁死。有没有能快速进行写入同时系统能不进行io阻塞呢?有,就是mmap。

mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。mmap的核心优点是对文件的读取操作API上无视页缓存,减少了数据的拷贝次数,用内存读写取代IO读写,并且系统来处理实际写入过程。

事实上业界大量著名的log类和存储类开源框架就是基于mmap完成的,腾讯家的xlog(https://mp.weixin.qq.com/s/cnhuEodJGIbdodh0IxNeXQ),mmkv(https://github.com/Tencent/MMKV)均是基于mmap实现的,业界也在普遍使用,看起来确实是百利无一害的方案。唯一可能存在的风险点是:a. 映射储存用的文件大小不是任意大小而是和内存对齐的pagesize,同时也需要自行扩张,因此在大量的log流入时需要对应的增减维护策略。同时如果需要删除旧有数据还是存在大段内存拷贝,需要挑选运行时机否则还是又可能带来卡顿。b. 另外对于日志这种文件可能会积累较大的场景mmap也并不友好,数十MB应该就是极限了。因此基于这个考虑于下一条基于流量靠谱,储存空间的限制(多余的日志丢弃)也建议在10day+每day20MB-30MB最佳(对于长期执行的客户端,如配送骑手端来说,30MB可能还偏小,因此还是需要适当控制日志数量拒绝无脑打log)。

2. 回收流量问题

流量问题则是更头疼的问题。我们日志上报往往不止是日志本身,同时还包含大量的上下文信息,可以列举的基础信息包括:a. 设备相关信息如设备型号系统版本设备uid等;b. 应用状态相关信息如当前cityid,用户userid,当前应用前后台状态等;c. 日志本身的信息也是标准化的如日志等级,时间,线程等。这三类信息可以分为固有信息,这个设备传的所有日志都具有相同的信息,上下文信息,应该是一部分日志会有相同的业务上下文,还有专有信息。因此可以定义出时机传输的结构如下:
{
  "deviceName": "iPhone XR",
  "system": "iOS 15.0.1",
  "uuid": "da89ca_da3113_321a90_31839c",
  "infopacks": [{
    "userId": 0,
    "cityId": 330100,
    "foreground": true
  },{
    "userId": 1235353,
    "cityId": 330100,
    "foreground": true
  }],
  "logs": [{
    "time": 173131432,
    "level": 3,
    "category": "log info = %@ count = %ld",
    "message": "log info = Object<0x78a93c81> count = 122",
    "infopack": 0,
    "hash": "89dacnh9da8d8a9sdsd"
  },{
    "time": 173138901,
    "level": 1,
    "category": "此处不应该出现的错误 error=%@",
    "message": "此处不应该出现的错误 error=网络请求失败404",
    "infopack": 1,
    "hash": "cad7a8vda0990dsada"
  },{
    "time": 174893123,
    "level": 1,
    "category": "此处不应该出现的错误 error=%@",
    "message": "此处不应该出现的错误 error=无网络",
    "infopack": 1,
    "hash": "1ca89cna09d0asd9fas"
  }]
}

设计上,如果用json结构表示一次上报的数据,数据最外层来容器设备等基础数据;而环境相关数据反在了infopacks数组中,每一项表示一个状态的上下文;logs数组中则是实际的每一条log的罗列,但是补充了infopack字段去关联infopack数组。

另外日志这样的信息,如果用如上的json这样的结构来一个10mb,不说json大小,光是对内存数据做一个json序列化就很难受了。因此从更直接的数据传输(减小序列化成本)与缩减数据大小的角度,logger生成的高密度日志数据使用protobuf协议更合适。

实际上传数据的请求上,继续保持http请求问题不大,但是考虑到整个app会在启动期间一定程度的轮训上传,支持http2.0与避免冗余的业务拦截器对请求处理是必然的,因此logger的网络模块反而更建议独立编写一个最小运行成本的即可。

3. 数据安全问题

数据安全在移动端上本身是一个防君子不防小人的话题,毕竟root/越狱过手机啥都不安全,这个话题更多是让小白用户无法访问到数据并且至少符合系统规范的安全储存数据。同时储存的数据也尽量避免法务风险(比如被用户观测日志注意到应用程序对用户的追踪,或者是在用户非预期时段执行任务)与舆情风险。

- 数据储存安全

储存格式本身,和请求一样用json这样的明文数据显然不利于储存,也更容易被用户直接查阅(虽然是通过mmap写入文件中会有空白区域或者不符合编码结构的区域),但是整个数据通过protobuf对切并按数据全量写入显然是安全性与性能上更优只选。

储存文件目录位置,iOS位于应用沙箱中基本没有疑问Library目录最佳(用户无法访问),Android侧注意放置在/data/data/的程序目录中而不是sd卡随便放置就没什么问题了。

也有讨论是否需要对日记内容通过RSA加一次密这个问题,考虑到客户端不防小人的本质与快速log过程中rsa对性能消耗相当严重(即使是https也只是握手阶段用到非对称加密),因此可以考虑直接放弃。不过同时有必要的是在每一条储存的log字段上添加hash效验字段,上传时进行效验至少一定程度上可以防止小人数据污染与极其偶发的系统写入异常。

- 数据内容安全

显然,logger日志数据内容上不宜出现的内容及是有严重数据重要性,与用户隐私相关的数据,如“password”等个别关键字,未加密电话号码,未加密地址。因此在logger interface层辅助建设filter层,对一些特定的数据如形式电话号码的数字,“password{}.[=:]”相关的内容进行匹配过滤。

当然在logger之前,上线前的线下研发team代码review就应该发现这些风险了,logger filter仅作辅助与兜底作用。

- 数据传输安全

都有https与protobuf了还担心什么呢?(笑)

——————

将以上提到的总总功能与技术特性完成后,可以称为这是一个合格的移动端Logger了。继续按需求细化其中数据,收集方式与实效,比如日志业务分类,比例型日志,关联日志链,以上功能加起来,才真的有可能足以保障移动端业务可用性建设。和Log4j相比,移动端的logger确实远不止logger吧。
关键词:移动端 , logger , android , ios
logo