用户事件的存储与分析

15 DECEMBER 2014 on infrastructure, analytics, database

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

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

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

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

存储这类数据的方法一般可以分为三类

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

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

表结构的设计

第一个要解决的问题是,我们应该将所有的事件存在单一表中,还是每个事件存在单独的表里。两者有其各自的优势。比如后者,每个事件单独建表,表结构非常清晰,易于理解。但缺点是每次定义新事件都需要改动数据库结构。我们希望事件的定义是非常轻量的,所以在Glow我们选择了前者。前者的关键问题是,各种事件都有不同的属性集合,难道把所有事件的所有属性都放在表结构的定义中?这样很快这个事件就会有成百上千的字段,对于存储与查询来说都非常的低效。我们的做法是定义一组通用的字段用于事件属性,并在代码中定义映射关系。

我们的事件表结构大致是这样的

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

转载于:https://www.cnblogs.com/jidan/p/5261069.html

用户事件的存储与分析相关推荐

  1. ztree树封装 json实例_小白7天入门PHP Web开发 - Day 6[下](综合)个人博客实例讲解用户数据的存储...

    <小白7天入门PHP Web开发>系列文章,面向单纯善良的完全不懂Web开发编程的入门速成课程,小白们如果感兴趣可以研读此系列文章,也可以连线提问.各路大神有何指教还请指点一二.希望各路大 ...

  2. 【Android 事件分发】事件分发源码分析 ( ViewGroup 事件传递机制 六 )

    Android 事件分发 系列文章目录 [Android 事件分发]事件分发源码分析 ( 驱动层通过中断传递事件 | WindowManagerService 向 View 层传递事件 ) [Andr ...

  3. 面向智能电网的电力大数据存储与分析应用

    面向智能电网的电力大数据存储与分析应用 崔立真1, 史玉良1, 刘磊1, 赵卓峰2, 毕艳冰3 1. 山东大学计算机科学与技术学院,山东 济南 250101 2. 北方工业大学云计算研究中心,北京 1 ...

  4. 【万字干货】OpenMetric与时序数据库存储模型分析

    摘要:解读OpenMetric规范和指标的模型定义基础上,结合当下主流的时序数据库核心存储及处理技术,尝试让用户(架构师.开发者或使用者)结合自身业务场景选择合适的产品,消除技术选型的困惑. 本文分享 ...

  5. 万字用户画像标签体系建设分析指南!

    转自:大数据梦想家 01 什么是用户画像 用户画像是指根据用户的属性.用户偏好.生活习惯.用户行为等信息而抽象出来的标签化用户模型.通俗说就是给用户打标签,而标签是通过对用户信息分析而来的高度精炼的特 ...

  6. 用户密码加密存储十问十答,一文说透密码安全存储

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 作者 | 程序员赵鑫 来源 | cnblogs.com/xinzh ...

  7. 【Android 事件分发】ItemTouchHelper 源码分析 ( OnItemTouchListener 事件监听器源码分析 二 )

    Android 事件分发 系列文章目录 [Android 事件分发]事件分发源码分析 ( 驱动层通过中断传递事件 | WindowManagerService 向 View 层传递事件 ) [Andr ...

  8. 【Android 事件分发】ItemTouchHelper 源码分析 ( OnItemTouchListener 事件监听器源码分析 )

    Android 事件分发 系列文章目录 [Android 事件分发]事件分发源码分析 ( 驱动层通过中断传递事件 | WindowManagerService 向 View 层传递事件 ) [Andr ...

  9. 【Android 事件分发】事件分发源码分析 ( ViewGroup 事件传递机制 七 )

    Android 事件分发 系列文章目录 [Android 事件分发]事件分发源码分析 ( 驱动层通过中断传递事件 | WindowManagerService 向 View 层传递事件 ) [Andr ...

  10. 【Android 事件分发】事件分发源码分析 ( ViewGroup 事件传递机制 五 )

    Android 事件分发 系列文章目录 [Android 事件分发]事件分发源码分析 ( 驱动层通过中断传递事件 | WindowManagerService 向 View 层传递事件 ) [Andr ...

最新文章

  1. Python3 编码讲解
  2. Python错误:TypeError: 'list' object is not callable
  3. 瑞士电信vCPE商用落地 华三通信NFV方案成最大功臣
  4. Golang系列(三)之并发编程
  5. php重写html不刷新,html5,html_两个页面进行交互,如何实现页面不刷新就更改html?,html5,html,javascript,php - phpStudy...
  6. 编程语言性能实测,Go 比 Python 更胜一筹?
  7. react 使用webpack打包问题汇总
  8. 软件测试从业者,Linux知识从入门到玩转(必读)
  9. c#中类的简单使用学习
  10. Hive map阶段缓慢,优化过程详细分析
  11. php 中文地址伪静态,.htaccess实现含中文的url伪静态跳转
  12. ArcGIS 软件中路网数据的制作,手把手教学
  13. IP地址-子网划分详解
  14. 前端js正则表达式2
  15. onvif 添加H265开发流程
  16. 《神经科学:探索脑》学习笔记(第5章 突触传递)
  17. 2020CCPC绵阳站 D-Defuse the Bombs (二分答案)
  18. 网页404是不是服务器没开,无法打开的网页出现404错误 知道什么意思吗?
  19. python代码编程教学入门,python代码编程火影忍者
  20. jQuery中 hide和fadeOut的区别 show和fadeIn的区别

热门文章

  1. windows——JDK下载与安装及环境变量配置
  2. OkHttp Interceptors(二)
  3. 使用Go语言实现简单MapReduce框架
  4. MySQL Information_Schema表使用注意事项
  5. 美国将采纳TMF智慧城市成熟度模型
  6. 中国孩子的micro:bit:TurnipBit自制小乐器教程实例
  7. Firefox for iOS 终于登陆中国
  8. Hdu 5806 NanoApe Loves Sequence Ⅱ(双指针) (C++,Java)
  9. Android性能优化-过度绘制解决方案
  10. [JNI]开发之旅(7)JNI函数中调用java对象的方法