來新公司20天,完成了第一個任務,安卓端日志收集流程的開發,在這里總結一下。
1.場景介紹
公司有多個產業,各產業產生若干app,現在需要收集app的日志信息,並做相關計算,例如流量統計、用戶畫像等。
用戶的數量級目前不易確定,因為有大半app還在開發中,並沒有發布。因為我們是新成立的數據組,沒有人熟悉安卓相關的東西,所以老板讓我研究一下把這條線打通。
2.技術方案
Android SDK(日志產生) -> Flume (日志收集) -> kafka(消息緩存) -> storm(日志解析) -> hbase (落地)
每一個模塊我之前都沒有接觸過,所以都要從頭了解,好在flume之后的這些服務已經搭好,我只要擺正姿勢使用就可以。
3.技術細節
3.1 Android SDK
安卓日志的統計對象是用戶的行為,即點擊、滑動、翻頁、跳轉等事件;統計內容主要包括設備信息、用戶信息、事件信息等。
3.1.1 接口設計
SDK開發完后,最終會提供給app開發團隊,讓他們接入到app中,在需要產生日志的地方埋點;因此SDK的接口不能復雜,不能帶來太多的接入開發工作。考慮之后,第一階段只提供下面幾個接口
(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 日志格式
日志最終以json格式發送,例子如下。client部分是設備相關的信息,events是對應的事件信息。
{ "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 編碼的換行問題
http://blog.csdn.net/liuhe688/article/details/6733407 --- activity 的生命周期
http://blog.csdn.net/gouguofei/article/details/7775752 --- 安卓監聽程序進入后台
http://blog.csdn.net/m_changgong/article/details/7608911 --- 安卓監聽手機鎖屏
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