使用XLog、Spring-Boot、And-Design-Pro搭建日志系统

4,860 阅读15分钟

一、前言:移动端为什么要三方日志系统

日志系统用于记录用户行为和数据以及崩溃时的线程调用栈,以帮助程序员解决问题,优化用户体验。

iOS系统就有自带Crash收集应用程序“ReportCrash”来收集App Crash信息,我也深入了解过iOS收集Crash 信息的过程并记录在此 CPU发生异常到生成Crash Log的过程 , 但用户遇到的很多问题不仅仅是Crash,更何况有些情况仅靠Crash Log并不能定位Crash,而且ReportCrash 收集的Crash信息还需要用户同意才可以和开发者共享。为了说明用户日志的重要性,这里引入一个faceu 团队-轻颜相机-Tom哥的调侃

“譬如用户反馈,拍照偏黄,中间经过了十几个渲染管线,没有log真呵呵,你又不可能让用户再拍一次”

因此大多数App 都带有Crash收集框架和日志收集框架,而Crash信息也是日志信息的一种,为什么要分成两个框架去收集呢?因为信息采集方式不一样,Crash收集框架通过捕获系统发送来的 Mach 异常和 Unix 信号进行信息采集、而日志收集框架是程序员主动代码触发的信息采集,信息采集部分共用代码很少,所以分成两个框架也更易于维护。

这里介绍的日志系统是收集非Crash 信息的日志系统。该系统分为三部分:

  • 采集部分:使用微信开源的日志采集组件XLog,该组件具有安全性流畅性完整性容错性 的优点。
  • 传输部分:使用Spring Boot 开发Web服务器,Web服务器提供简单的文件上传和下载、文件上传白名单获取和设置的功能。
  • 管理部分:使用Ant Design Pro 框架来开发后台管理系统,在管理系统中提供页面进行设置日志上传白名单,及日志下载。

下面和大家聊一聊我技术选型的过程

二、技术选型:本日志系统的技术栈

都说技术服务业务,考虑技术选型当然离不开业务需求。

其实团队项目之前就有日志系统,没有使用三方框架,而是自己写了一些简单的策略,如按优先级写文件或上报服务器。但仍满足不了客户端开发的需求

  • 有个致命的缺点是:找日志看的话没有可视化平台,日志上报的接口对应的服务端同学早就离职了,而找别的服务端同学帮捞日志比较费时费力。
  • 其次,日志明文写文件保证不了安全性、闪退时无法保证Log的 完整性 、本地日志管理策略也没有对文件数量、单个文件大小做限制
  • 再者,日志只输出到console和文件,想要数据通过web socket实时输出到web页面展示的话,代码层面不易于扩展,这个功能可以帮助在真机调试push、测试同学实时查看埋点信息。

开发有时间限制,需求也就有优先级之分

2.1 最高优先级需求:稳定的日志传输服务和可视化的日志管理

服务端和前端同学人力紧张,而且这个不是挣钱的产品需求,不一定能争取到排期让服务端和前端同学支持“稳定的日志传输服务和可视化的日志管理”这个需求,而且我们的Crash 收集和查看工具都是用的腾讯的,不是同一个公司同一个部门,更别指望他们来支持了,只能网上寻找解决方案。

2.1.1 Seafile 个人网盘 + CocoaLumberjack 日志采集

本人服务端开发经验匮乏,只搞过vps搭建vpn和博客,并没有玩过web服务器,找到一个不需要搭建web服务器的方案。服务器搭建Seafile 个人网盘服务

Seafile,是一套中国国产的开源、专业、可靠的云存储项目管理软件,解决文件集中存储、共享和跨平台访问等问题。正式发布于2012年10月。除了一般网盘所提供的云存储以及共享功能外,Seafile还提供消息通信、群组讨论等辅助功能,帮助员工更好的围绕文件展开协同工作,已有10多万用户使用。

Seafile 是比较成熟的方案了,还提供接口来上传、下载文件,赶紧买个10块/月的腾讯云学生机搭个Seafile 个人网盘验证一下。

seafile

半天业余时间就解决了“稳定的日志传输服务和可视化的日志管理“,接着又调研了下客户端日志采集框架,发现 CocoaLumberjack start数量很多,最近还有提交记录,而且框架设计得易于扩展,已经有基于CocoaLumberjack 去解决“有通过web socket实时输出log到web页面”问题的方案,另外,CocoaLumberjack 还支持对文件数量、单个文件大小的设置。CocoaLumberjack 这个方案看着挺好,马上又撸了个Demo去验证。

拿着Demo去和同事讨论,总结了几个问题:

  • Seafile,高定制化牺牲了可扩展性

    • 除了文件上传下载外,无法自定义接口来进行更多的C/S交互,比如客户端询问服务端日志上传类型,是上传用户日志还是上传数据库文件,是发送到微信好友还是上传到服务器。
    • 数据存储孤独,无法和其他数据进行联动展示,还是可扩展性不好。比如用户反馈的问题的日志文件应该和Crash收集文件放到同一个地方才合理,虽然Crash 收集用的是第三方框架,无法自己去改代码做扩展,万一以后不跟那个三方框架合作了呢,或者用户日志的数据需要和其他数据进行联动呢。
  • CocoaLumberjack 仍无法百分百保证Log的 完整性 ,在App Crash时无法保证Log 已经写到文件。因为CocoaLumberjack是通过 -[NSFileHandle writeData:] 来进行写文件的,此方式除了无法保证Log完整性外,相对mmap中使用内存映射文件来执行写操作的方案也较慢

2.2 方案优化

既然找不到服务端和前端同学来帮忙,那就硬着头皮自己上吧。分解任务,逐个击破!将日志系统分成相对独立的三部分 采集部分、传输部分、管理部分

  • 采集部分:使用微信开源的日志采集组件XLog,该组件具有安全性流畅性完整性容错性 的优点。
  • 传输部分:使用Spring Boot 开发Web服务器,Web服务器提供简单的文件上传和下载、文件上传白名单获取和设置的功能。
  • 管理部分:使用Ant Design Pro 框架来开发后台管理系统,在管理系统中提供页面进行设置日志上传白名单,及日志下载。

2.2.1 采集部分:高性能日志采集组件 XLog

流畅性是首要考虑

在采集部分,之前一直没有提到 流畅性 的重要程度,其实这个才是日志采集组件的最高优先级需求,不能因为日志频繁写文件导致应用程序卡顿或耗电量增加。因为项目中之前没有高频高密度地使用日志,没能及早意识到流畅性 的重要程度。频繁写文件为什么会卡顿?

当写文件的时候,并不是把数据直接写入了磁盘,而是先把数据写入到系统的缓存(dirty page)中,系统一般会在下面几种情况把 dirty page 写入到磁盘:

  • 通过页的 flag 标记为有改动,操作系统定时将这种 dirty page 写回到磁盘上,时机不可控。
  • 调用用户态的写接口->触发内核态的sys_write->文件系统将数据写回磁盘。而文件系统回写磁盘的时机也是不可控的,发现 dirty page 占用内存超过系统内存一定比例后回写。

而且数据从程序写入到磁盘的过程中,牵涉到两次数据拷贝:一次是用户空间内存拷贝到内核空间的缓存,一次是回写时内核空间的缓存到硬盘的拷贝。其中内核空间和用户空间频繁切换的话也带来性能损耗。

保证流畅性无法兼顾完整性

避免频繁写文件,先在内存中创建buffer,合适时在进行写文件。这个方式虽然保证了流畅性,缺无法保证完整性,而且集中压缩日志会导致 CPU 短时间飙高。程序发生Crash的话内存中的数据还没有持久化,实时写文件的话又无法保证流畅性,该如何是好?

mmap 保证流畅性和完整性

mmap 是使用逻辑内存对磁盘文件进行映射,中间只是进行映射没有任何拷贝操作,避免了写文件的数据拷贝。操作内存就相当于在操作文件,避免了内核空间和用户空间的频繁切换。

为了验证 mmap 是否真的有直接写内存的效率,微信团队写了一个简单的测试用例:把512 Byte的数据分别写入150 kb大小的内存和 mmap,以及磁盘文件100w次并统计耗时

mmap 效率

mmap 除了能保证 流畅性 ,还能兼顾日志的 完整性,下面这些情况回自动回写磁盘

  • 内存不足
  • 进程退出
  • 调用这两个函数
    • msync(mmap_ptr, mmap_size, MS_ASYNC) 同步,异步写回磁盘
    • munmap(mmap_ptr, mmap_size) 解除一个map,内容会写回磁盘
  • 不设置 MAP_NOSYNC 情况下 30s-60s(仅限FreeBSD)
xlog 还保证了 安全性容错性

通过压缩和加密可以保证日志信息非明文写入磁盘,同时减少所占用的 mmap 的大小。策略是

在写进逻辑内存之前就把日志先进行压缩,再进行加密,最后再写入到逻辑内存中

微信选择的具体压缩方案可以参考文章 cloud.tencent.com/developer/a… ,简单来说就是 能够保证日志容错性的流式压缩,

即使压缩单位中有部分数据损坏,因为是流式压缩,并不影响这个单位中损坏数据之前的日志的解压,只会影响这个单位中这个损坏数据之后的日志。

所以一句话总结xlog 为什么具有安全性流畅性完整性容错性 的优点

使用流式压缩方式对单行日志进行压缩,压缩加密后写进作为 log 中间 buffer的 mmap 中,当 mmap 中的数据到达一定大小后再写进磁盘文件中

另外mmap 相关的API 如下,参考开源的XLog,你也可以给团队定制基于mmap的日志采集组件


FILE *fp = fopen(file_path, "wb+");
file_num = fileno(fp);
ftruncate(file_num, size);      // 调整size
char *mmap_ptr = (char *)mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_FILE | MAP_SHARED, file_num, 0);

// 然后就可以对 mmap_ptr 进行读写了

munmap(mmap_ptr, mmap_size);    // 解除一个map,内容会写回磁盘
msync(mmap_ptr, mmap_size, MS_ASYNC);  // 同步,异步写回磁盘

扩容办法:先解除munmap,调大文件大小,重新调 mmap 映射即可

手淘的SatanWoo 五子棋大佬在解析xlog源码的时候给出了两点使用mmap时需要额外注意的点,文章在此

  • 注意点1: 如果我们尝试打开mmap成功了,但是mmap对应的数据地址是NULL,那我们必须停止映射。因为NULL所代表的地址处于内核态,一旦映射了,势必造成Crash。
  • 注意点2:使用mmap的情况下,如果上次应用断电了、Crash,日志的信息还是存在的,但是并不一定能及时的转换成我们想要的日志文件。因此我们首先检查下mmap文件里面有没有数据,有的话先把这部分转换成日志。

2.2.2 传输部分:使用Spring Boot 快速开发Web服务器

在网上有很多介绍Spring Boot的文章,蚂蚁金服的一位前辈写了篇 Spring Boot快速开始指南 我看还不错,介绍了Spring-Boot的知识点线路图和基本概念,还有如何快速创建一个Spring Boot 应用。

我在自己的学生机开发完,用postman 调试完接口再去找公司运维要机器资源,部署到了公司机器上。由于我也是照葫芦画瓢,只是使用Spring Boot提供了简单的文件上传和下载功能,暂时无法在这一块深入介绍自己的经验,不过日志系统对应的Spring Boot部分我已经放到了github,感兴趣可以clone 下来跑跑看 github.com/HonchWong/H…

项目名字是RDA,也就是研发助手的英文,意味着这个Spring-Boot应用不会止步于此。因为平时iOS业务开发中会经常遇到些阻碍效率的问题,会想出很多“牛逼”的技术方案去解决,但仅仅熟悉Cocoa 框架是实现不了的,少不了服务端的支持,比如最近业余在做的三个需求,【客户端可视化Mock数据:提供可视化的界面去设置Mock网络数据,无需硬编码Mock和减少编译时间】、【客户端可视化一键生成bug单:提供可视化的界面去输入bug描述,并生成bug单,提高研发效率】、【客户端埋点管理平台:提供平台去管理埋点需求、验证埋点、埋点信息自定义展示】,这也是立个flag,技术服务业务,业务必将反哺于技术,19年好好学习服务端知识,再来完善这篇文章 :)

2.2.3 管理部分:直接fork "Ant Design Pro"进行改装

Ant Design Pro 是蚂蚁金服团队在 Ant Design 的设计规范与组件库基础上推出的一套 React 实现的企业级中后台前端/设计解决方案。基于这个框架可以快速开发后台管理系统,如果你有React Native的开发经验,那么对基于React开发的Ant Design Pro 肯定也是上手很快。本日志系统使用到的 Ant Design Pro 放到了github github.com/HonchWong/H…

三、本日志系统如何使用

除了服务端和前端的Demo,iOS端的Demo我已经提交到github。需要从github下载两个工程 web服务器工程 github.com/HonchWong/H… 、 iOS端工程github.com/HonchWong/H…

  • 本地运行web服务器
    • 环境搭建,安装JDK、Maven
    • HCRDA-SpringBoot/src/main/resources/application.properties 修改配置文件中的用户日志文件夹位置,比如,这个是我本地存放的位置userlog.dir.path=/Users/huanghongchang/Desktop/userLog
    • 在HCRDA-SpringBoot 目录下执行 mvn spring-boot:run
    • 访问 http://localhost:9080
  • 运行iOS工程
    • pod install 安装依赖
    • 修改 HCLogFileUploadManager.m 中的 static NSString *_hostURL 为 localhost的ip

3.1 XLog使用

  • 初始化xlog。 涉及四个API,具体调用可以参考Demo工程中的 XLogHelper.m +setupXlog

    • setxattr([[self xlogFileDirPath] UTF8String], attrName, &attrValue, sizeof(attrValue), 0, 0); 该API是系统库提供,为了防止该路径下的日志文件被 iCloud 同步。
    • xlogger_SetLevel(kLevelDebug); 设置log级别
    • appender_set_console_log(true); debug模式下控制台是否打印log
    • appender_open(kAppednerAsync, [logPath UTF8String], "Test"); 打开log目录下的日志文件,进行一些初始化操作。
  • 打log,xlogger_Write(&info, message.UTF8String),Demo用宏定义封装了该API

  • 需要在APP终止方法applicationWillTerminate中反初始化 appender_close()

以上只是Demo中的使用方式,更多详细API 可以参考xlog的wiki Mars iOS/OS X 接口详细说明,Demo中对log日志进行加密,appender_open 提供了参数传入公钥,xlog的加密使用可以参考文档 Xlog 加密使用指引

https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/2/13/168e5e26f60446be~tplv-t2oaga2asx-image.image

3.2 日志上传和下载

本日志系统日志上传的策略采用白名单的方式,在后台管理系统设置白名单,白名单包含用户唯一标识符uin及对应的上传的日志类型(如数据库or日志log),用户输入白名单中的uin,获取需要上传的日志类型进行日志上传。

在HCRDA-SpringBoot 目录下执行 mvn spring-boot:run 后,在浏览器中访问 http://localhost:9080, 输入账户admin 密码 123,在用户日志-指定用户中可以看到有白名单中已经有八个,目前Demo 中只实现了【选择日志上传单个文件】和【上传全部日志】

白名单

设置白名单后便可在Demo 中走上传流程,操作步骤如下

日志上传

在后台管理系统中可以查看和下载上传的日志

日志查看和下载

3.3 查看日志

我这里并没有对xlog文件进行加密,而是对zip文件进行了加密,而密码用了xor的方式对字符串进行混淆避免用hopper等反汇编工具直接查看,当然这个还不是绝对安全,还是可以通过逆向App,动态调试,获取zip文件的密码,只不过也加大了破解的难度。虽然没有加密,但单行log还是进行了流压缩,需要用这个脚本进行解压decode_mars_nocrypt_log_file

执行脚本 python decode_mars_nocrypt_log_file.py xxx.xlog

查看日志

四、参考文章