来新公司20天,完成了第一个任务,安卓端日志收集流程的开发,在这里总结一下。
1.场景介绍
公司有多个产业,各产业产生若干app,现在需要收集app的日志信息,并做相关计算,例如流量统计、用户画像等。
用户的数量级目前不易确定,因为有大半app还在开发中,并没有发布。因为我们是新成立的数据组,没有人熟悉安卓相关的东西,所以老板让我研究一下把这条线打通。
2.技术方案
Android SDK(日志产生) -> Flume (日志收集) -> kafka(消息缓存) -> storm(日志解析) -> hbase (落地)
每一个模块我之前都没有接触过,所以都要从头了解,好在flume之后的这些服务已经搭好,我只要摆正姿势使用就可以。
3.技术细节
3.1 Android SDK
安卓日志的统计对象是用户的行为,即点击、滑动、翻页、跳转等事件;统计内容主要包括设备信息、用户信息、事件信息等。
3.1.1 接口设计
(1) luanchApp() // app启动时调用,目的是初始化sdk
(2) onEvent() // 各类按钮相应事件中调用,用来统计普通事件
(3) onPageStart() // 页面/activity的开始事件中调用,用来统计页面访问事件
(4) onPageEnd() // 页面/activity的结束事件中调用,用来统计页面访问事件
3.1.2 Android Activity 生命周期
页面事件是比较重要的一部分,开始之前,对安卓activity的生命周期做了一下了解。下面是几个相关的方法,
(1) onCreate(), onDestroy() // activity对象的创建和销毁
(2) onStart(), onStop() //activity的开始和停止事件
(3) onResume(), onPause() // activity的继续和暂停事件
通俗来讲,当activity显示出来,就会调用一次onStart,变得看不见,就会调用一次 onStop;当可以在activity上进行操作时,则会调用一次onResume,变得不能操作时,就会调用一次onPause。onResume和onStart的区别的一个例子:当一个activity A的上面,出现一个透明activity B将A覆盖,那么会调用A的 onPause,而不会调用 onStop。因为A还看得见,但是不能操作了。
3.1.3 日志项
名称 | 例子 | 类型 | 说明 |
userid | abc | 字符串 | 用户id(由app提供) |
appid | 123 | 字符串 | 应用id(由app提供) |
guid | 787f7300-37e2-34d8-b101-c8ef415385ae | 字符串 | 设备唯一id |
imei | 867831028457919 | 字符串 | 国际移动设备标识 |
ln | zh | 字符串 | 语言 |
density | 3.0 | 浮点型 | 屏幕密度 |
tel | 130128361936 | 11位整数 | 电话号码 |
mac | f4:8b:32:af:22:e9 | 字符串 | 设备mac地址 |
iscrack | 1 | 0或1 | 是否root |
timezone | Asia/Shanghai | 字符串 | 时区 |
nettype | lte | 字符串 | 网络类型 |
longitude | 39.001 | 浮点型 | 经度 |
os | 4.4.4 | 字符串 | os版本 |
platform | android | 字符串 | os |
module | MI 4LTE | 字符串 | 手机型号 |
sr | 1080*1920 | 字符串 | 屏幕分辨率 |
sdkver | 1.1.5 | 字符串 | sdk版本 |
isp | 46002 | 整型 | 运营商代号 |
appver | 1 | 字符串 | app版本 |
ip | 10.0.2.15 | 字符串 | ip地址 |
ismobile | 1 | 0或1 | 是否为手机 |
requesttime | 1451272912 | 整型 | 请求产生事件 |
netstatus | 1 | 0或1 | 网络状态 |
sim | 898600310115f0024716 | 字符串 | sim卡id |
channel | 12 | 字符串 | app渠道 |
latitude | 120.123 | 浮点型 | 纬度 |
event.sessionid | 112312341341341341 | 字符串 | 事件所属的sessionid |
event.eventtime | 1451028244 | 整型 | 事件产生时间 |
event.duration | 12 | 整型 | 事件持续事件 |
event.pagedur | 12 | 整型 | 页面停留时间 |
event.definedid | abc | 字符串 | 自定义事件id |
event.prepageid | red | 字符串 | 前一个页面id |
event.currevent | page | 字符串 | 事件类型 |
event.pageid | blue | 字符串 | 当前页面id |
这里面有几个项比较纠结,不易获取:
(1) latitude 和 longitude,参考一篇帖子,http://stackoverflow.com/questions/20438627/getlastknownlocation-returns-null
(2) IP,按照找到的方法总是取不到安卓的真实ip,索性不取了,在flume中的http请求头中得到
3.1.4 日志产生流程
为了让后续实时分析避免对历史数据的关联,发送的日志数据,每条记录都带上全部字段信息。这样就导致每条记录至少在1K,如果实时发送,吃不消。因此,使用定时发送策略,暂定每60s发送一次,每次发送的数据中,设备相关的信息只保留一份,事件以数组的形式附在其后。这样,经过压缩之后,基本可以保证每分钟日志产生流程在 2K 以内。另外,页面事件的产生,是在 onPageEnd 中,也就是说,当离开这个页面的时候,才产生这个页面的对应事件,这样做的目的是为了统计页面停留时间。
上图是日志产生的流程图。时钟响应每1分钟触发一次,期间产生的事件,放入sqlite数据库中。有一点需要注意:
app如果退出,则会导致缓存的事件不能及时发出,因为我们平时从后台退出app的方法是会直接杀死进程的。为了避免这种情况,当app发生进入后台、屏幕锁定这两种动作时,不管时钟相应是否触发,直接发送一次缓存事件,因为这两种动作之后,app进程很有可能被杀掉。另外,如果因为各种原因,app退出后还是留下没有及时发出的事件,那么下次打开app时,第一件事就是把上次缓存的事件发送出去。
3.1.5 日志格式
{
"client": {
"ln": "zh",
"density": "3.0",
"tel": "",
"userid": "",
"appid": "731224921",
"mac": "f4:8b:32:af:22:e9",
"iscrack": "0",
"timezone": "Asia/Shanghai",
"nettype": "lte",
"longitude": "",
"os": "4.4.4",
"platform": "android",
"module": "MI 4LTE",
"sr": "1080*1920",
"sdkver": "1.1.5",
"isp": "46002",
"imei": "867831028457919",
"udid": "",
"appver": "1.0",
"ip": "10.0.2.15",
"ismobile": "1",
"guid": "787f7300-37e2-34d8-b101-c8ef415385ae",
"requesttime": "1451272912",
"vendorid": "",
"netstatus": "1",
"advertid": "",
"sim": "898600310115f0024716",
"latitude": "",
"channel": "12"
},
"events": [
{
"eventtime": "1451028244",
"duration": "",
"isfirst": "",
"pagedur": "1",
"defineid": "",
"moduleid": "",
"prepageid": "",
"params": "",
"modulecnt": "",
"currevent": "page",
"pageid": "MAIN"
},
{
"eventtime": "1451028245",
"duration": "",
"isfirst": "",
"pagedur": "1",
"defineid": "",
"moduleid": "",
"prepageid": "MAIN",
"params": "",
"modulecnt": "",
"currevent": "page",
"pageid": "MAIN"
}
]
}
3.1.6 日志压缩方法
为了节省流程,日志在传输之前需要进行压缩。压缩使用 gzip 方法。直接对 json 字符串进行 gzip,然后输出的数据进行 base64,最后为了用get方法请求,需要再进行一次 UrlEncode。压缩后的数据形式如下。经测试,在包含50个左右的event时,压缩后大小在2k以内。
data=H4sIAAAAAAAAAM1STW%2FUMBD9KyhH1A3%2BSuLkxgHQSgUOIHGgHBx7sms1cYLtVGpX%2Fe%2BMk91qKwFSqx6qXObNeGbee5NDpnsLLmbNIetd1mR3%2B%2BwiM%2BCCjbcIeU4QR%2BgxxmAO4K1ZYzVNS1hxypioGcXcoDRmOtHItuGsUV3DWAM1VmzQXulrrC4D7QB3owOE74NVV%2B%2B%2B7ZXb7ZXFmoMYb6dU%2BrH9uMVEP7qdjbNJKUp5zhmticTCGDAjcvwQTL2K3egHTCln%2FIjckM9o5j71fd6%2BEZffP2Aq%2BDSGSPKW1ixxCeb6BpZkTvNioTqluSUhLKEBLEJZVpJTwqQoqpomRbM5c%2BI0IQ20qZ1WLKdVzoTMGV0NGMbWLmSSU7t5NU9WXcUJ2fAK2IYLIzctJXSjJXSCFlwWCvC1h98zhJhsS%2F2iYKREHmnbDTgzPhwFzQtRxTkc1yiDzOKpGmyyR9YStaEYSosORYqKlslmFU8uM55TVvMipTVexi33pyy7v8gAF0Yc%2F%2FOwho858SJpNbPHYaNbt9rQWR%2FiCia1Aywf6RnorIMTvfVaJzR5SI8foPJqCOcPtTvO1LP3CxeEqeW4Zum8%2FPpp%2BwV5%2F5VttfxG%2F2JLHtF9PtsAIeD4pcQ1iEK3oCtetoSrVtNO1x2pO1Hymsv%2Fi8PT%2Bniu7nnCXuoMLyfsaVeTr%2FMf%2B3X%2FB5yw2A5OBQAA
3.1.7 接入flume测试
flume是一个常用的日志收集工具,下一节做具体说明。flume提供了很多日志的接入方式,http就是其中一种。只需要在app中向flume指定的服务器端口发送http请求,即可完成日志的收集。
3.1.8 部分参考资料
http://stackoverflow.com/questions/5586197/android-user-agent --- 安卓获取 user agent
http://wingjang.blog.163.com/blog/static/479134422013107111424348/ --- base64 编码的换行问题
--- activity 的生命周期
--- 安卓监听程序进入后台
--- 安卓监听手机锁屏
3.2 Flume
flume 的简介参考这篇帖子,http://shiyanjun.cn/archives/915.html。
在这个项目中,配置了一个http source,两个memory channel,两个 sink;一个是 file roll sink,用来把日志写到本地文件,另一个是 kafka sink,用来将日志推到 kafka 上,以便后续处理。
3.3 Kafka
3.4 Storm
storm 的简介参考,http://www.searchtb.com/2012/09/introduction-to-storm.html。
在这个项目中,topology 中一共有一个spout 和 三个 bolt;spout 是从kafka中取数据,然后emit;第一个 bolt 是日志的解析,通过 UrlDecode -> 反base64 -> gunzip,可以恢复出日志json字符串;再将json拆成若干个events数据行,然后以list的形式发送;第二个bolt是将数据写入hbase,每个event对应一行数据,每行数据都带有全部采集项信息;第三个bolt是创建hbase表,日志表目前按照日期每天新建一个。当第二个bolt插入时发现没有建表时,才会执行第三个bolt。
3.5 HBase