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 } } } }