用户事件的存储与分析

15 DECEMBER 2014 on  infrastructureanalyticsdatabase

许多时候咱们说一款产品的设计是数据驱动的,是指许多产品方面的决策都是把用户行为量化后得出的。一个典例的例子就是注册流程的设计,若是用户须要填写的注册信息较多,通常就会分红多个页面,而产品设计师最关心的就是每一个页面的流失率,从而不断的对这个流程做调整以达到信息量与流失率之间的平衡。python

为了可以量化用户的行为,前提是要将各类用户事件都保存下来。其中最典型的事件包括user creation, page view和button click,但实际上还有许多其余事件,好比用户更改了状态或是录入了某些数据等等。目前有许多第三方的服务能够帮助你作这方面的统计,国内有友盟,国外有Google Analytics和Mixpanel。但若是你记录的事件数量很是庞大,或是对以后的数据分析有很是定制化的要求,那就要考虑本身构建事件分析的平台,而这个过程当中最关键的一步就是如何存储用户事件。android

首先咱们来分析一下用户事件存储有哪些特性web

  • 数据量巨大 用户在应用中产生的事件数量远远大于他们产生的数据。很是简单的一个例子,就是用户在浏览各个页面时,他们并不产生任何数据,但却产生了大量的page view事件。因此事件数据的量每每是主数据库的几十倍甚至上百倍。
  • 不一致的数据结构 虽然全部的事件都有一些公共的属性,好比事件名称,事件时间,应用的版本与操做系统等等,但有不少事件有本身特定的属性,好比用户注册事件,咱们会很是关心注册的渠道,是用email注册仍是用社交网络注册(好比微博,微信等),一样一个论坛贴子的查看事件,咱们会想要记录贴子的ID与版块的ID。这种不一致性,给咱们设计数据存储结构带来了许多麻烦。
  • 聚合式查询 咱们在使用用户事件的数据时,每每不关心单我的的事件,而只关心统计结果。因此一个典型的查询模式就是访问大量的历史数据,对查询结果按某一个特定的维度聚合。

存储这类数据的方法通常能够分为三类sql

  • 传统关系型数据库,如MySQL, PostgreSQL
  • Hadoop HDFS + Hive
  • 数据仓库,如Amazon Redshift, Microsoft SQL Server for PDW

后两种方案有先天的技术优点,但维护成本高,而且其优点须要在数据量突破某个临界点以后才能真正显现。第一种方案看似毫无亮点,但对于创业型小团队来讲,却有其价值在。由于关系数据库你们都很熟悉,对于运维来讲,没有额外的维护成本。当数据量在TB如下时,若是正确地创建索引,查询速度也是很是快,而且也能够经过Sharding的方法作分布式的扩容。Glow目前正处于从MySQL存储到Redshift的转型,因此今天咱们主要想分享一下用关系数据库来存储与分析用户事件的一些经验,咱们会在未来的博客中介绍后两种系统(它们每每是共存的)数据库

表结构的设计

第一个要解决的问题是,咱们应该将全部的事件存在单一表中,仍是每一个事件存在单独的表里。二者有其各自的优点。好比后者,每一个事件单独建表,表结构很是清晰,易于理解。但缺点是每次定义新事件都须要改动数据库结构。咱们但愿事件的定义是很是轻量的,因此在Glow咱们选择了前者。前者的关键问题是,各类事件都有不一样的属性集合,难道把全部事件的全部属性都放在表结构的定义中?这样很快这个事件就会有成百上千的字段,对于存储与查询来讲都很是的低效。咱们的作法是定义一组通用的字段用于事件属性,并在代码中定义映射关系。编程

咱们的事件表结构大体是这样的api

CREATE TABLE `EventLog` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `event_time` bigint(20) NOT NULL, `event_name` varchar(255) NOT NULL, `user_id` bigint(20), `platform` tinyint(4), `app_version` varchar(20), `ip_address` varchar(20), `device_id` bigint(20), `data_1` bigint(20), `data_2` bigint(20), `data_3` bigint(20), `data_4` bigint(20), `data_5` bigint(20), `data_6` bigint(20), `text_1` varchar(255), `text_2` varchar(255), `text_3` varchar(255), `text_4` text, `text_5` text, `text_6` text, PRIMARY KEY (`id`), KEY `idx_event_id` (`event_time`, `event_id`), KEY `idx_event_and_platform` (`event_time`, `event_id`, `platform`), KEY `idx_event_and_version` (`event_time`, `event_id`, `version`), KEY `idx_user` (`user_id`), ); 

首先,咱们会记录事件的名称event_name与时间event_time,而后是全部事件共有的属性user_idplatformapp_versionip_addressdevice_id。随后的data_*text_*则是用于各个事件的特有属性。事件在代码中的定义大体以下缓存

FORUM_NEW_TOPIC = {  
    'name': 'forum new topic', 'mapping': { 'room_id': 'data_1', 'subject': 'text_1', 'content': 'text_4', } } 

这是论坛中发贴事件的定义,应该很容易看懂。讨论区的IDroom_id是整型,标题与贴子正文都是字符串,但正文极可能超出255长度的限制,因此被放入text_4。再看一个更有趣些的例子服务器

SHARE_APP = {  
    'name': 'share app', 'mapping': { 'channel': {'field': 'data_1', 'enum': ['facebook', 'twitter', 'sms', 'email']}, 'message': 'text_1', } } 

这是分享app的事件,其中分享渠道channel是一个枚举类型,因此被映射到了data_1而不是text_1。在存储该类事件时,咱们会验证事件中的channel的值是否为上述4个字串之一,而且只保存字符串的hash值。在从数据库读取该类事件时,当咱们解析data_1字段的值时,会反向查找hash值对应的原始字串。在实际使用中,text_*的字段的使用率是比较低的,由于大部分的用户事件中的字符串都是枚举类型。枚举型的存储占用空间更小,查询也更快,由于整数比较要明显快于字串比较。微信

在Glow中,有一个事件定义文件,咱们称为事件的masterfile,这个文件定义了Glow中全部的事件,由数据分析团队管理与修改。另外有一个模块专门负责将系统中接收到事件,根据masterfile,转化成正确的数据格式并存入数据库。

性能与扩容

以前也提到,用户事件的数据远多于其余的生产环境数据。当单表的数据条数过大时,不管是查询仍是插入性能都会降低,那么如何扩容与保特性能呢?由于本质上这个事件数据是一个时间序列,因此第一步就是按时间维度分表。咱们把天天的数据放在一张单独的表中,表的命名方式是event_log_YYYY_MM_DD。这样作有不少的好处

  1. 当前写入表的记录数量仅仅只有一天的数据量,提高插入的性能
  2. 因为大部分查询都会有一个时间范围,咱们只须要查询该时间范围所涉及的表便可。
  3. 能够很方便地将历史数据表归档。

同时为了方便Ad-hoc的查询,咱们能够把多个单日表合并成一个月视图或是年视图。

CREATE VIEW event_log_2014_01 AS SELECT * from event_log_2014_01_01 UNION ALL SELECT * from event_log_2014_01_02 ... UNION ALL SELECT * from event_log_2014_01_31; 

对于事件的写入,因为时效性并不重要,因此咱们应尽可能将一段时间内的事件对象缓存在内存中,而后批量一次性的写入。这样对数据库系统的负载会小不少。在实际的系统架构中,咱们为用户事件的收集与写入单独起一个Service进程,经过unix socket与web服务的主进程通讯。

事件数据库的分布式扩容很是容易,能够经过user_id作为hash-key来分库,也能够随机分库。而后只需简单的经过增长数据库集群中服务器的数量就能够扩容了。

分析与统计

因为咱们对事件的属性作了映射与hash,同时作了按天分表以及分布式的sharding,因此直接用SQL来对数据表查询虽然可行,但并非很方便。咱们能够把数据分析经常使用的一些查询写成API的形式,而且把前面提到的那些复杂性都封装在API的实现中。在系统中,咱们称这类API为Metrics API。在定义API接口的过程当中,咱们主要参考了Mixpanel的API接口定义

整个Metrics API的方法数量小于10个,如下是3个比较经常使用的API

def count(event_name, start_time, end_time, where=None): ''' 返回全部符合条件的事件的总数 >>> events('forum new topic', '2014/12/01', '2014/12/31', where={'room_id': 1}) 1321 2014年12月全部在讨论区1中发贴事件发生的总次数为1321。 ''' ... def group(event_name, property, start_time, end_time, where=None): ''' 对事件按某一个属性进行分组,返回该属性在这类事件中值的分布 >>> group('share app', 'channel', '2014/12/01', '2014/12/31') { 'facebook' : 786, 'twitter' : 439, 'email' : 300, 'sms' : 257, } 2014年12月经过各个渠道分享app的次数统计 ''' ... def retention(start_time, time_unit, retention_length, born_event, retention_event, where=None): ''' 用户的粘性分析,将某个时间段内诞生的用户作为实验组,观察这组用户在以后的几个时间段里的活跃度 诞生事件由born_event决定,活跃事件由retention_avent决定 >>> retention('2014/12/01', 'week', 4, 'user created', 'app open', where={'platform': 'android'}) { 'cohort_size': 34032, 'retentions': [0.54, 0.42, 0.31, 0.25] } 以2014年12月1日为始的那一周(12/01 - 12/07)在Android平台上注册的用户作为一个集合,共34032个用户。 他们中在以后第一周(12/08 - 12/14)打开app的人数占集合总数的54% 他们中在以后第二周(12/15 - 12/21)打开app的人数占集合总数的42% 他们中在以后第三周(12/22 - 12/28)打开app的人数占集合总数的31% 他们中在以后第四周(12/29 - 01/04)打开app的人数占集合总数的25% ''' ... 

数据分析团队是Metrics API的主要用户,他们95%以上的工做均可以经过这套API来完成。开发团队则会经过并发或是缓存等方法,持续的优化API的性能。

总结

此次与你们分享了基于关系数据库的用户事件存储与分析,但愿之后能将这套方案开源,但暂时尚未具体的时间计划。在本文的开始,我提过目前Glow正在向用Redshift + Hadoop + Hive的平台转型,等这部分工做完成后再和你们分享经验。

叶剑烨

Head of Technology at Glow

中国,上海 http://yejianye.com

Share this post

  

代码规范和Android项目中的一些可用工具

这里主要讲一下关于代码规范的相关问题,和在Android项目中如何利用一些工具进行规范和检查。代码规范不是一个Android项目特有的问题,因此前部份内容是不单针对Android的。 什么是代码规范? 代码规范通常是指在编程过程当中的一系列规则规范。 通常来讲代码规范能够分为两种。 一是编程语言自己在设计时所规定的一些原则,这类规则大部分都是强制的,像Python里用缩进表示逻辑块,Go里用首字母大小写表示可见度。 另一种是在一些组织约定的一些规范模式或我的在编写代码时的一些偏好,这种通常都是非强制的。好比大括号是放在方法名的同一行呢仍是另起一行,不一样的人有不一样的想法,我也不知道谁好,因此别问我。 假如是强制的,你们暂时也不能反抗,…

UIScrollView 实践经验

UIScrollView(包括它的子类 UITableView 和 UICollectionView)是 iOS 开发中最经常使用也是最有意思的 UI 组件,大部分 App 的核心界面都是基于三者之一或三者的组合实现。UIScrollView 是 UIKit 中为数很少能响应滑动手势的 view,相比本身用…

Glow 技术团队博客 © 2016Proudly published with  Ghost
相关文章
相关标签/搜索