如果这是你的眼----移动端Monitor重构复盘
一 背景和需求
背景1: 过去的一个月内,我司的五位工程师 一起重构了我司移动端的数据采集sdk。 平台包括android和ios。
像我这么厉害的,还有四个。背景2:这是本次sdk重构的一号任务是,减少数据漏报率。 之前的sdk,某采样场景下被人实锤 漏报率达10%。这也是本次sdk重构的直接原因。
背景3:目前我司的业务需求方对数据的需求基本是通过手动打点来实现的。近期随着业务方数据需求的扩大,每次业务方需求变更,移动端都要手动侵入式打点。 本次sdk 重构,希望在重构sdk,提供稳健的基础功能平台之后,有计划的实现采集规则实时下发。
稳健基础平台,良好的可扩展性。 是我们的重构的第二号任务。
二 整体结构
2.1 整体结构简介
整体结构图其实 摸着良心 说,我司之前的sdk也是不错的,在开发时间紧任务重的情况下,实现了一个采集模块应有的功能。
(局限性在于,之前是以一个 功能而不是项目的心态去做的monitor。 )
所以本次重构还是在之前的基础上实现。 在后面的描述中,所有的更改项都有阐述原因和理由。
良心还是要摸的2.2 整体结构优化---事件对象化
重构后
所有的事件消息,例如点击事件,启动事件,异常事件,以AppMessage为抽象顶级父类。便于管理和扩展。
AppMessage之前
之前的所有的事件,没有抽象出类角色,所有的事件都是以 字符串形式存在。相对来说扩展不是很方便。
2.3 整体结构优化---功能模块化
重构后
重构后的项目以 gradle 依赖包的形式输出。 虽然目前仅服务于 用户端app。
但是如果其他app想要接入使用,接入成本极低,或者不仅想采集业务数据,如果想要实现移动app性能检控等其他功能,也支持很方便的横向扩展。
事件的流程被划分为 采集--处理---储存--上报。 四个功能模块。 每个功能模块在编码上以 接口和调用的形式组合在一起。
方便拆卸和定位问题,在开发阶段也可以最大程度上并向开发。
(比如现在我们的sdk是以数据库的形式存储的,如果稍后我们想换成文件储存,切换成本也是最小化的)
之前
之前的数据采集虽然样式上也是以一个 gradle依赖包的样式输出,但是本质上只是一个实现了信息采集和上报的功能模块而已。
尤其是eventbus 这个结构毒瘤的引入,导致整个事件采集sdk 之间业务流程过于飘逸。之前查看业务流程的时候,基本只能依靠硬编码查找。。
那种初恋般的感觉:
22223 事件采集模块
重构后,所有的事件来源都由MessageSource 消息源统一管理。对应到结构图中是这里。
消息采集示意图消息采集模块的类比模型,可以参考一句古诗 百川东到海 何时复西归。
MessageSource内的消息有两种事件来源,
一种是MessageReporter 抽象类,他是一个主动的消息上报器。
开启monitor每个MessageReporter对应一种消息采集方式而不是一类消息。
这个之前在设计的时候,我们有过讨论,有没有必要专门设计这个MessageReport角色来 负责消息采集,是否有过度设计的嫌疑。但是最后经过我们真(ji)诚(lie)讨(bo)论(dou) 之后, 最终还是确定了,加上这个角色。
因为消息采集的业务逻辑在我们的sdk中是客观存在的,与其放任不管,那里用到写哪里,还不如给予其一种角色。就像在一家公司里面每个员工都要一个职位与之对应。
当然有些时候,为了灵活或者其他原因公司会请一些游离组织之外的外包人员实现一些功能,但是一定不能过多。
一家全是外包人员的公司是不稳定的。
一个没有角色抽象的sdk也一定是。
silicon valley目前项目中 消息上报器实例有三个,分别是
1 手动打点上报的DefaultReporter,他的消息采集方式是开发人员手动编码打点。
2 使用aspectJ 无痕埋点上报的TraceLessReporter,采集方式是aop无痕埋点。目前大部分的事件都是由此report采集的
3 采集崩溃异常信息的CrashReporter,他的消息采集方式是通过Java自带的UncaughtExceptionHandler接口实现功能采集。
类比成模型,就是三条大河奔腾不息将各自采集到的事件流输送到 MessageSource的大海中。
主动上报而IMessageObserver 消息观察员接口 与MessageReporter相反, 是被动的消息事件源。
类似于SessionId的信息,当前设备的运营商等信息。都需要一定量的代码逻辑操作才能获取。而这些代码都是当我们的sdk需要时才会去从这些事件源里取数据。
如果把MessageReporter 比作奔腾的河流,源源不断将用户产生的点击事件输入到MessageSource的大海里话。
那IMessageObserver就是一个个的静态的湖泊。 会把类似用户信息,session信息,等数据存在到一个个固定的地点,当我们的系统需要这些数据的时候,才会主动去获取。
可观察者示例至此消息采集的部分完毕。接下来,messageSource 会把message对象扔给MessageHandler类,进行处理。
4 数据处理模块
消息处理部分,分为三个步骤,分别是处理,储存和上传。
首当其冲的正是 数据处理链
数据处理链如果把消息采集的最终结果,看作是一片汪洋大海的话,那么数据处理链。应该那条巴拿马运河了。
所有的消息流都会以单个的形式经过 处理链,目前这里只有一个 IMessageProcessor实现,那么就是公共参数封装的处理器。
公共参数处理器实现这里支持面向切面的横向拓展。后续有需要的新增切面的逻辑,可以在这条巴拿马运河上注册新的关卡(Processer)。
就像这样:
滦河城的弗雷5 数据储存模块
消息处理的第二个环节,是数据储存模块。这个模块也是本次优化改动最大的环节。
Android之前的消息储存方法是文件储存,ios的稍微好点 采用的内置的xml储存。
本次重构,两端统一修改为数据库储存。 这样做的好处?
终于找到一个数据库样子的图片了太多了。SQLITE数据库底层也是采用的文件储存方式,但是作为一个合格的中(接)间(盘)件(侠)。他努力的做了很多的工作,进而成为了比文件储存更先进的生产力工具。 比如:
数据库的事务机制,保证了 消息储存的过程中是一个比Java io流更可靠的储存行为。
数据库的数据文件管理机制,保证了比手动管理文件有更好的压缩性能,安全性能。
等等,更具体的内容,可以参考各类书籍:
从删库到跑路对了,这里还有两处小优化。
1 之前的sdk 存在用户拒绝权限则无法储存的问题。 做了顺手处理。
2 之前的io操作是定时操作,每200ms进行一次消息写入。 现在修改成定量,内存中有10个容量的内存缓存。这样做的好处是降低了CPU的消耗,反应到移动设备上是省电。
6 网络上报模块
这里没有做太多的改动,上传采用的是netty长连接上传文件。
这里唯一的改进就是之前的长连接是使用文件直接传输,本次使用数据库灵活调整上传测试,并且使用了服务器返回的回执消息作为判断。
重构后的消息会在数据写入完成后,等待30s的消息回执。这样的结果就是 绝不会丢包,但是如果因为服务器的原因延迟事件内没有回执,就会造成数据的重复上报。 这个问题在我们的测试过程中,一般只会发生在服务器异常的情况下。
说起来,这个解决方案也不太厚道,不过这个是我同桌,尼古拉斯王尧教给我的: 死道友莫死贫道。
尼古拉斯另外,为了保证数据上报率的基准化,在测试环境下,我们准备了一套测试环境下的测试方案。
http://wiki.luckincoffee.com/pages/viewpage.action?pageId=3377976
测试环境下下,所有上传的事件都会在本地储存一份。每天测试完成后都会拿这两份数据进行比对,确定漏报情况。
目前来看,数据漏报基本可靠。已知的会丢失数据的极端场景,也在wiki中有标注。
测试环境下的检测结果7 取得的结果
重构后的数据sdk已经随着客户端1.9.0上线了。 毕竟线上环境总是比测试环境更加复杂的。特别是对于这种大数据采集项目来,需要让时间来考验它。
对于我们最初的两个目标。
第一稳定的可扩展性,目前已基本达成。 app上线后尚未有崩溃异常反馈。
第二 数据漏报率,数据还在统计中。 如果一旦马有失蹄,希望大家高呼 理解万岁。
理解万岁8 下期的规划
如果还有第二期的开发规划。我觉得还有这些地方可以优化。
1 打点事件动态配置
换句话来形容,就是要通过服务器动态下发规则的方式消灭掉,我们目前写的30多处手动打点。
wiki地址:http://wiki.luckincoffee.com/pages/viewpage.action?pageId=3377442
而这30多处代码非写不可的原因分为三类:
第一类: 类似轮播图展示,菜单点击添加之类的 业务采集事件。
其实目前来说,我们几乎是将所有的用户点击事件全量上传的。
针对这类情况来说,之所以我们频频修改手动打点代码,不是因为业务方需求的事件我们拿不到,而是因为业务需求的事件提供的数据我们拿不到。
eg : 用户点击添加购物车事件,我们一直都有,但是用户点击购物车上新增了哪件商品,我们需要手动打点。
这类功能,我们其实是可以解决的。
只需要将购物车数据,或者banner数据等常用业务数据 设置为一个 可观察对象 IMsgOberver。
服务器动态下发采集规则,key: code, value: 待采集的IMsgOberver。
客户端monitor在处理对应事件时,抓取上传即可。
类似这样:
信息源动态配置类比到我们的模型中,就是这些事件本来就是在我们的消息池中的。只是没有对应的数据而已。我们要做的就是在这些消息经过巴拿马运河的时候,添加一道关夹。 这道关夹的规则完全由服务器下发控制。
第二类:业务达成不以点击事件为标志的事件
比如 我们我们采集到的 H5加载成功事件,订单支付查成功事件。
目前我尚未想到很好的解决方案。因为这些事件其实本质上是因为我们的aop 并不是真正的运行时动态aop,而是编译时的代码生成。
也就是说这些消息是因为本来不就在我们的消息池采集范围内。可以考虑的笨办法就是加大采集范围。
例如和使用aspectj采集所有的H5事件,类似订单支付成功之类事件采集,可以通过手动添加注解方式实现。
事件动态配置2 数据采集平台,业务压缩算法实现
目前我们的只有netty上传自带 通用的gzip压缩算法,没有业务压缩算法。
以Android为例,每次采样一次上传事件16条消息, 数据为 13KB,gzip压缩后为2.4KB.
但是观察数据报文其实很多是又可以做优化的,且不谈json格式的转义字符问题。 只是其中大量出现的热词,例如“Activity”完全可以用"-a5"来替代。或者包名也完全可以用更小的代码来替代。
热词替换示例 热词替换结果示例3 fragment界面获取。
目前因为技术问题 android的用户界面只是以activity为最小单元的。这样很多时候用户的行为无法真实体现。
从数据记录上看用户甚至可以在MainAcitivty内部待一万年。 需要继续调查fragment界面抓取的可行性。
这个技术尚未调研。但是有实现的必要性。
一万年9.最后我想说的一点
丶
thx