http://blog.csdn.net/zwgdft/article/details/53542597html
做爲系列文章的第三篇,本文將重點探討數據採集層中的用戶行爲數據採集系統。這裏的用戶行爲,指的是用戶與產品UI的交互行爲,主要表如今Android App、IOS App與Web頁面上。這些交互行爲,有的會與後端服務通訊,有的僅僅引發前端UI的變化,可是不論是哪一種行爲,其背後老是伴隨着一組屬性數據。對於與後端發生交互的行爲,咱們能夠從後端服務日誌、業務數據庫中拿到相關數據;而對於那些僅僅發生在前端的行爲,則須要依靠前端主動上報給後端才能知曉。用戶行爲數據採集系統,即是負責從前端採集所需的完整的用戶行爲信息,用於數據分析和其餘業務。
舉個例子,下圖所示是一次營銷活動(簡化版)的註冊流程。若是僅僅依靠後端業務數據庫,咱們只能知道活動帶來了多少新註冊用戶。而經過採集用戶在前端的操做行爲,則能夠分析出整個活動的轉化狀況:海報頁面瀏覽量—>>點擊」當即註冊」跳轉註冊頁面量—>>點擊「獲取驗證碼」數量—>>提交註冊信息數量—>>真實註冊用戶量。而前端用戶行爲數據的價值不只限於這樣的轉化率分析,還能夠挖掘出更多的有用信息,甚至能夠與產品業務結合,好比筆者最近在作的用戶評分系統,便會從用戶行爲中抽取一部分數據做爲評分依據。前端
在早期的產品開發中,後端研發人員每人負責一個攤子,雖然也會作些數據採集的事情,可是基本上只針對本身的功能,各作各的。一般作法是,根據產品經理提出的數據需求,設計一個結構化的數據表來存儲數據,而後開個REST API給前端,用來上報數據;前端負責在相應的位置埋點,按照協商好的數據格式上報給後端。隨着業務的發展,這樣的作法暴露了不少問題,給先後端都帶來了混亂,主要表如今:前端四處埋點,上報時調用的API不統一,上報的數據格式不統一;後端數據分散在多個數據表中,與業務邏輯耦合嚴重。
因而,咱們考慮作一個統一的用戶行爲數據採集系統,基本的原則是:統一上報方式、統一數據格式、數據集中存儲、儘量全量採集。具體到實現上,概括起來主要要解決三個問題:android
用戶在前端UI上的操做,大多數表現爲兩類:第一類,打開某個頁面,瀏覽其中的信息,而後點擊感興趣的內容進一步瀏覽;第二類,打開某個頁面,根據UI的提示輸入相關信息,而後點擊提交。其行爲能夠概括爲三種:瀏覽、輸入和點擊(在移動端,有時也表現爲滑動)。其中,瀏覽和點擊是引發頁面變化和邏輯處理的重要事件,輸入老是與點擊事件關聯在一塊兒。
所以,瀏覽和點擊即是咱們要採集的對象。對於瀏覽,咱們關注的是瀏覽了哪一個頁面,以及與之相關的元數據;對於點擊,咱們關注的是點擊了哪一個頁面的哪一個元素,與該元素相關聯的其餘元素的信息,以及相關的元數據。頁面,在Android與IOS上使用View名稱來表示,在Web頁面上使用URL(hostname+pathname)來表示。元素,使用前端開發中的UI元素id來表示。與元素相關聯的其餘元素信息,指的是與「點擊」相關聯的輸入/選擇信息,好比在上面的註冊頁面中,與「提交」按鈕相關聯的信息有手機號、驗證碼、姓名。元數據,是指頁面能提供的其餘有用信息,好比URL中的參數、App中跳轉頁面時傳遞的參數等等,這些數據每每都是很重要的維度信息。ios
除了這些頁面中的數據信息,還有兩個重要的維度信息:用戶和時間。用戶維度,用來關聯同一用戶在某個客戶端上的行爲,採用的方案是由後端生成一個隨機的UUID,前端拿到後本身緩存,若是是登陸用戶,能夠經過元數據中的用戶id來關聯;時間維度,主要用於數據統計,考慮到前端可能延遲上報,前端上報時會加上事件的發生時間(目前大多數正常使用的移動端,時間信息應該是自動同步的)。
綜合起來,將前端上報的數據格式定義以下。uuid、event_time、page是必填字段,element是點擊事件的必填字段,attrs包含了上述的元數據、與元素相關聯的其餘元素的信息,是動態變化的。web
{ "uuid": "2b8c376e-bd20-11e6-9ebf-525499b45be6", "event_time": "2016-12-08T18:08:12", "page": "www.example.com/poster.html", "element": "register", "attrs": { "title": "test", "user_id": 1234 } }
而針對不一樣客戶端的不一樣事件,經過不一樣的REST API來上報,每一個客戶端只需調用與本身相關的兩個API便可。數據庫
REST API | 說明 |
---|---|
/user_action/web/pv | 上報Web頁面的瀏覽事件 |
/user_action/ios/pv | 上報IOS頁面的瀏覽事件 |
/user_action/android/pv | 上報Android頁面的瀏覽事件 |
/user_action/web/click | 上報Web頁面的點擊事件 |
/user_action/ios/click | 上報IOS頁面的點擊事件 |
/user_action/android/click | 上報Android頁面的點擊事件 |
整理好數據格式和上報方式後,前端的重點工做即是如何埋點。傳統的埋點方式,就是在須要上報的位置組織數據、調用API,將數據傳給後端,好比百度統計、google analysis都是這樣作的。這是最經常使用的方式,缺點是須要在代碼裏嵌入調用,與業務邏輯耦合在一塊兒。近幾年,一些新的數據公司提出了「無埋點」的概念,經過在底層hook全部的點擊事件,將用戶的操做盡可能多的採集下來,所以也能夠稱爲「全埋點」。這種方式無需嵌入調用,代碼耦合性弱,可是會採集較多的無用數據,可控性差。通過一番調研,結合咱們本身的業務,造成了這樣幾點設計思路:json
咱們首先在Web的H5頁面中作了實踐,核心的代碼很簡單。第一,在頁面加載時綁定全部的click事件,上報頁面瀏覽事件數據。第二,經過user_action_id屬性來表示一個元素是否須要上報點擊事件,經過user_action_relation屬性來聲明當前元素被關聯到哪一個元素上面,具體代碼實現不解釋,很簡單。後端
$(d).ready(function() { // 頁面瀏覽上報 pvUpload({page: getPageUrl()}, $.extend({title: getTitle()}, getUrlParams())); // 綁定點擊事件 $(d).bind('click', function(event) { var $target = $(event.target); // 查找是不是須要上報的元素 var $ua = $target.closest('[user_action_id]'); if ($ua.length > 0) { var userActionId = $ua.attr('user_action_id'); var userActionRelation = $("[user_action_relation=" + userActionId + "]"); var relationData = []; // 查找相關聯的元素的數據信息 if (userActionRelation.length > 0) { userActionRelation.each(function() { var jsonStr = JSON.stringify({ "r_placeholder_element": $(this).get(0).tagName, 'r_placeholder_text': $(this).text() }); jsonStr = jsonStr.replace(/\placeholder/g, $(this).attr('id')); jsonStr = JSON.parse(jsonStr); relationData.push(jsonStr); }); } // 點擊事件上報 clickUpload({page: getPageUrl(), element: userActionId}, $.extend({title: getTitle()}, getUrlParams(), relationData)); } }); });
上述代碼能夠嵌入到任何HTML頁面,而後只要在對應的元素中進行申明就行了。舉個例子,緩存
<div> <div> <textarea id="answer" cols="30" rows="10" user_action_relation="answer-submit"></textarea> </div> <button user_action_id="answer-submit">提 交</button> </div>
數據進入後臺後,首先接入Kafka隊列中,採用生產消費者模式來處理。這樣作的好處有:第一,功能分離,上報的API接口不關心數據處理功能,只負責接入數據;第二,數據緩衝,數據上報的速率是不可控的,取決於用戶使用頻率,採用該模式能夠必定程度地緩衝數據;第三,易於擴展,在數據量大時,經過增長數據處理Worker來擴展,提升處理速率。app
除了前端上報的數據內容外,咱們還須要在後端加入一些其餘的必要信息。在數據接入Kafka隊列以前,須要加入五個維度信息:客戶端類型(Web/Android/IOS)、事件類型(瀏覽/點擊)、時間、客戶端IP和User Agent。在消費者Worker從Kafka取出數據後,須要加入一個名爲event_id的字段數據,具體含義等下解釋。所以,最後存入的數據格式便以下所示:
{ "uuid": "2b8c376e-bd20-11e6-9ebf-525499b45be6", "event_time": "2016-12-08T18:08:12", "page": "www.example.com/poster.html", "element": "register", "client_type": 0, "event_type": 0, "user_agent": "Mozilla\/5.0 (Linux; Android 5.1; m3 Build\/LMY47I) AppleWebKit\/537.36 (KHTML, like Gecko) Version\/4.0 Chrome\/37.0.0.0 Mobile MQQBrowser\/6.8 TBS\/036887 Safari\/537.36 MicroMessenger\/6.3.31.940 NetType\/WIFI Language\/zh_CN", "ip": "59.174.196.123", "timestamp": 1481218631, "event_id": 12, "attrs": { "title": "test", "user_id": 1234 } }
再來看event_id的含義。前端傳過來的一組組數據中,經過page和element能夠區分出到底是發生了什麼事件,可是這些都是前端UI的名稱,大部分是開發者才能看懂的語言,所以咱們須要爲感興趣的事件添加一個通俗易懂的名稱,好比上面的數據對應的事件名稱爲「在海報頁面中註冊」。將page+element、事件名稱進行關聯映射,而後將相應的數據記錄id做爲event id添加到上述的數據中,方便後期作數據分析時根據跟event id來作事件聚合。作這件事有兩種方式:一種是容許相關人員經過頁面進行配置,手動關聯;一種是前端上報時帶上事件名稱,目前這兩種方式咱們都在使用。
最後,來看看數據存儲的問題。傳統的關係型數據庫在存儲數據時,採用的是行列二維結構來表示數據,每一行數據都具備相同的列字段,而這樣的存儲方式顯示不適合上面的數據格式,由於咱們沒法預知attrs中有哪些字段數據。象用戶行爲數據、日誌數據都屬於半結構化數據,所謂半結構化數據,就是結構變化的結構化數據(WIKI中的定義),適合使用NoSQL來作數據存儲。咱們選用的是ElasticSearch來作數據存儲,主要基於這麼兩點考慮:
Elasticsearch的使用方法能夠參考Elasticsearch使用總結一文,這裏不作過多講解。使用Elasticsearch來作數據存儲,最重要的是兩件事:創建Elasticsearch的映射模板、批量插入。Elasticsearch會根據插入的數據自動創建缺失的index和doc type,並對字段創建mapping,而咱們要作的建立一個dynamic template,告訴Elasticsearch如何自動創建,參考以下。批量插入,能夠經過Elasticsearch的bulk API輕鬆解決。
"user_action_record": { "order": 0, "template": "user_action_record_*", "settings": { }, "mappings": { "_default_": { "dynamic_templates": [{ "string_fields": { "mapping": { "type": "string", "fields": { "raw": { "index": "not_analyzed", "ignore_above": 256, "type": "string" } } }, "match_mapping_type": "string" } }], "properties": { "timestamp": { "doc_values": true, "type": "date" } }, "_all": { "enabled": false } } } }