cobub razor是开源的移动统计后台,github主页:https://github.com/cobub/razor。分为客户端SDK和服务器端两部分,客户端主要负责采集数据,而razor的服务器端需要完成数据的采集、统计分析、展示的工作。数据采集依赖于提供的api接受client发回的数据,统计分析是依赖于mysql数据库的存储过程来完成的,而数据的展示则是依靠一个php开发的后台实现,里面大量的报表展示和导出,还是非常不错的。
#数据分库 razor有两个database,分别是原始数据和数据的data warehouse,这里我们把两个库的名字分别命名为razor和razordw。 ##原始数据 原始数据库的所有表如下,下面会做一些简单的介绍:
+------------------------+
| Tables_in_razor |
+------------------------+
| razor_alert |
| razor_alertdetail |
| razor_cell_towers |
| razor_channel |
| razor_channel_product |
| razor_ci_sessions |
| razor_clientdata |
| razor_clientusinglog |
| razor_config |
| razor_errorlog |
| razor_event_defination |
| razor_eventdata |
| razor_login_attempts |
| razor_markevent |
| razor_mccmnc |
| razor_networktype |
| razor_platform |
| razor_product |
| razor_product_category |
| razor_product_version |
| razor_productfiles |
| razor_reportlayout |
| razor_target |
| razor_targetevent |
| razor_user2role |
| razor_user_autologin |
| razor_user_permissions |
| razor_user_profiles |
| razor_user_resources |
| razor_user_roles |
| razor_users |
| razor_wifi_towers |
+------------------------+ ###客户端基本信息表 razor_cliendata clientdata中保存了用户设备的基本信息,每次在客户端调用了`postClientData()`会发送一条数据库到服务器端并保存到clientdata表。clientdata中包含了用户的基本硬件配置信息、地理位置、设备标识以及时间信息,时间维度是一个重要的信息,需要基于它来判断新增用户、活跃用户等。这里的时间是采用了客户端的时间,这样会带来一个问题,客户端的时间并不一定就是准确的时间,有些用户可能手机刚买来没多久,设置的时间日期并不一定准确,那就导致了数据日期的混乱;但是因为采集到的日志可能来自离线日志数据,如果采用服务器时间也不太合适。这里我作了一个修改,作了一个折中的方案。在数据插入原始数据表的时候,判断时间是否大于我们系统的上线时间,比如“2013-05-01”,如果大于这个时间的就认为是可信的数据,否则可能是客户端设置的时间不准确,就修改为服务器端时间。
###事件信息表 razor_eventdata
eventdata表保存了用户自己定义的事件信息,客户端在每次调用onEvent()
方法的时候会向服务器发送一次请求。event表中最重要的两个字段就是eventid和label了,eventid用来标识是哪个事件,而label可以用来区分同一事件中的不同行为。目前razor对label还没有做统计展示,但是umeng的产品中已经有了对label的分析,我觉得这个功能还是很需要的。
##数据仓库 数据仓库之中的表包括以下这些:
mysql> show tables;
+-------------------------------------+
| Tables_in_razordw |
+-------------------------------------+
| razor_dim_activity |
| razor_dim_date |
| razor_dim_devicebrand |
| razor_dim_devicelanguage |
| razor_dim_deviceos |
| razor_dim_deviceresolution |
| razor_dim_devicesupplier |
| razor_dim_errortitle |
| razor_dim_event |
| razor_dim_location |
| razor_dim_network |
| razor_dim_product |
| razor_dim_segment_launch |
| razor_dim_segment_usinglog |
| razor_fact_clientdata |
| razor_fact_errorlog |
| razor_fact_event |
| razor_fact_launch_daily |
| razor_fact_reserveusers_monthly |
| razor_fact_reserveusers_weekly |
| razor_fact_usinglog |
| razor_fact_usinglog_daily |
| razor_hour24 |
| razor_log |
| razor_sum_accesslevel |
| razor_sum_accesspath |
| razor_sum_basic_activeusers |
| razor_sum_basic_byhour |
| razor_sum_basic_channel |
| razor_sum_basic_channel_activeusers |
| razor_sum_basic_product |
| razor_sum_basic_product_version |
| razor_sum_usinglog_activity |
+-------------------------------------+
从表名上就可以明显看出,dataware中的表分为四大类:dim表、fact表、sum表还有log类表。
昨天在razor的讨论群里咨询了一下razor的开发人员,才明白这套划分是有依据的,设计原则来源于数据仓库的设计原则Dimensional Modeling,算是写的还不错的中文blog。里面对fact表和dim表进行了描述:
1,Fact Table
包含业务数据的表,如daily_sales_fact_table(date, product_key, store_key, quantity_sold, dollar_sales_amount)
fact table分三种粒度类别:transaction/periodic snapshot/accumulating snapshot
2,Dimension Table
Dimension table是fact table的entry point,包含了业务对象的文本描述,如
product_dimension_table(product_key, product_description, sku_number, brand_description, category_description, department_description,...)
Fact table和Dimension table需要join来查询数据,所以又称之为join star schema
每个数据集市可能包含多个fact tables,每个fact table可能对应5到15个dimension tables 然后这里还有一篇stackoverflow的文章[what-is-dim-what-is-fact](http://stackoverflow.com/questions/3189512/what-is-dim-what-is-fact),也不错。还有wiki上对[star schema](http://en.wikipedia.org/wiki/Star_schema)的介绍,简单来讲的话,其实就是一个1..n的表关联。
dim表
dim表主要是保存类型信息,这里的数据可能是静态的,也有可能是动态的。比如razor_dim_event表保存了用户自己定义的所有event,里面的数据都来自用户定义,是静态的数据;而razor_dim_errortitle表中的数据来自原始数据表中采集到的客户端崩溃日志数据,将其title进行distinct之后保存到dim表中。
fact表
fact表主要保存与时间维度相关的统计数据,表中的一些数据比如date字段不再保存原始的日期,而是存为了dim表中对应类型的ID。例如razor_fact_clientdata表中的devicelanguage_sk
、devicesupplier_sk
等字段都会被替换为对应dim表中的ID。fact表中统计的数据也会分为不同的时间维度,比如razor_fact_usinglog
和razor_fact_usinglog_daily
表,后者就是数据在天的维度上做一个汇总,同样的还有razor_fact_reserveusers_monthly
和razor_fact_reserveusers_weekly
。这些工作就是由后面将要提到的存储过程来完成。
sum表
目前还没太理解sum表和fact表区分在何处。从数据上看,sum表中的数据进行了更进一步的处理,而不仅仅是在时间维度上的聚合,比如计算了用户的周活跃度、月活跃度等数据,可以直接用于在razor的后台的报表展示,比如其中的razor_sum_basic_product
表。
log表
主要是razor_log这个表,存储过程在运行的过程中,会把操作的每一步记录保存在razor_log表中,便于分析问题的时候使用,例如:
mysql> select * from razor_log limit 10;
+----+---------+----------------------------+---------------------+---------------+----------+
| id | op_type | op_name | op_date | affected_rows | duration |
+----+---------+----------------------------+---------------------+---------------+----------+
| 1 | rundim | razor_dim_location | 2013-04-28 18:57:46 | 0 | 0 |
| 2 | rundim | razor_dim_deviceos | 2013-04-28 18:57:46 | 0 | 0 |
| 3 | rundim | razor_dim_devicelanguage | 2013-04-28 18:57:46 | 0 | 0 |
| 4 | rundim | razor_dim_deviceresolution | 2013-04-28 18:57:46 | 0 | 0 |
| 5 | rundim | razor_dim_devicesupplier | 2013-04-28 18:57:46 | 0 | 0 |
| 6 | rundim | razor_dim_product | 2013-04-28 18:57:46 | 0 | 0 |
| 7 | rundim | razor_dim_network | 2013-04-28 18:57:46 | 0 | 0 |
| 8 | rundim | razor_dim_activity | 2013-04-28 18:57:46 | 0 | 0 |
| 9 | rundim | razor_dim_errortitle | 2013-04-28 18:57:46 | 0 | 0 |
| 10 | rundim | razor_dim_event | 2013-04-28 18:57:46 | 0 | 0 |
+----+---------+----------------------------+---------------------+---------------+----------+
#存储过程
razor的数据处理是通过存储过程来定时完成的,分为hourly、daily、weekly、monthly4个任务,还有一个laterdata。
运行方法是在crontab中定时执行脚本razor_hourly_archive.sh
、razor_daily_archive.sh
等,脚本会调用 razor/application/controllers/archive.php,然后通过这个php代码来执行对应的存储过程。
##hourly任务
hourly任务每小时执行一次,主要是用来将原始数据导入数据仓库中,运行方法是在crontab中定时执行脚本razor_hourly_archive.sh
,这个脚本会调用。查看存储过程的方法:在dataware运行mysql> select body from mysql.proc where specific_name='runfact'\G;
,hourly任务里面调用的存储过程分别是rundim
、runfact
、runsum
,从命名中就能大致看出他们的用处是什么,rundim
的逻辑比较简单,这里就不作分析了,有兴趣的同学可以自己去数据库的warehouse里查看,这里主要看看runfact
所作的任务。在前面引用的data warehouse设计中,fact表划分包括 transaction/periodic snapshot/accumulating snapshot 三种,runfact
主要做的任务应该是transaction,将数据从原始数据库中插入razor_fact_clientdata
用户客户端数据、razor_fact_usinglog
、razor_fact_errorlog
和razor_fact_event
,这里在插入数据时,会join一个或者多个dim表。
runsum
任务做的,就类似上面提到过的 period snapshot 的工作,下面挨个分析:
- update fact_clientdata 初始入库的数据
isnew_channel
和isnew
这两个字段都是默认值1
。这里会根据clientdata的id、入库时间来判断,将后入库的记录这两个字段的值标记为0
,即非新增用户。 - sum usinglog for each sessions 把 razor_fact_usinglog 表中当天的数据插入 razor_fact_usinglog_daily 中,在这个过程中会把同session的duration进行合并,方便计算每个session的用户停留时间。
- sum_basic_product是个很重要的表,记录了我们一般都十分关心的用户行为,比如新增、活跃、使用时间等,里面的每个field基本都通过一条单独的sql来统计并插入,sum_basic_product表中每个appkey每天作为一条记录.
这里采用的是“insert on key duplicate update”的方式,之前对这种方式不太了解,网上找了段它和“replace”的对比
如果表中不存在主键记录,replace和insert*update都与insert是一样的特点。
如果表中存在主键记录,replace相当于执行delete 和 insert两条操作,而insert*update的相当于执行if exist do update else do insert操作。因此,如果replace填充的字段不全,则会导致未被更新的字段都会修改为默认值,并且如果有自增id的话,自增id会变化为最新的值(这样如果是以自增id为标志的话可能导致记录丢失);而insert*update只是更新部分字段,对于未被更新的字段不会变化(不会强制修改为默认值)。
这里举个例子:
insert into razor_sum_basic_product(product_id,date_sk,startusers)
select p.product_id, d.date_sk,count(distinct f.deviceidentifier)
from razor_fact_clientdata f,
razor_dim_date d,
razor_dim_product p
where d.datevalue = today
and f.date_sk = d.date_sk
and p.product_sk=f.product_sk
group by p.product_id on duplicate key update startusers = values(startusers);
这个sql通过判断用户的设备ID,通过distinct之后计算使用的独立用户。
insert into razor_sum_basic_product(product_id,date_sk,newusers)
select p.product_id, f.date_sk,sum(f.isnew)
from razor_fact_clientdata f,
razor_dim_date d,
razor_dim_product p
where d.datevalue = today
and f.date_sk = d.date_sk
and p.product_sk = f.product_sk
and p.product_active = 1
and p.channel_active = 1
and p.version_active = 1
group by p.product_id,f.date_sk on duplicate key update newusers = values(newusers);
这个是判断isnew标识为新用户的,通过sum计算出来插入库中。
剩余的字段,比如sessions
是提取当天所有clientdata表数据,upgradeusers
则是统计razor_fact_clientdata表中今天有记录,今天之前同样有记录,并且之前记录中versionname要小于今天记录中的versionname,这里在比较的时候使用的条件是STRCMP( pp.version_name, p.version_name ) < 0
,个人觉得不是特别严谨,如果采集到客户端的version code然后做比较的话,应该更可靠。allusers
是把今天之前每天的记录所有记录的newusers
进行累加,allsessions
同样是对今天之前所有记录的allsessions
进行累加,usingtime
是把razor_fact_usinglog_daily表中当天记录的duration进行累加.
- sum_basic_channel 和razor_sum_basic_product类似,只不过是从渠道的角度去做数据的汇总
- sum_basic_product_version 则是从app版本的角度进行汇总
- update segment_sk column 根据每个session的duration确定它具体属于哪个时间段,比如0-3秒、3-10秒、30-60秒等。
- sum_basic_byhour 在razor_sum_basic_byhour表中计算每小时的活跃用户、新增用户、启动次数等数据
- sum_usinglog_activity 这个表主要是记录用户的使用行为。
accesscount
和totaltime
来自对razor_fact_usinglog表中数据的累加,exitcount
和activity_sk
是根据razor_fact_usinglog表中的数据按照sessionid进行group之后按照时间进行倒排,计算出每个session最后一次使用的activity,然后就能得出每个activity的跳出率了。 - razor_fact_launch_daily 这个表记录了每个app的每个版本在用户当天启动次数维度上的统计数据,比如某个app的版本当天启动0~3次的用户数是多少。
因为这个sql比较庞大,这里先把它提取出来:
insert into razor_fact_launch_daily
(product_sk,
date_sk,
segment_sk,
accesscount)
select rightf.product_sk,
rightf.date_sk,
rightf.segment_sk,
ifnull(ffff.num,0)
from (select fff.product_sk,
fff.date_sk,
fff.segment_sk,
count(fff.segment_sk) num
from (select fs.datevalue,
dd.date_sk,
fs.product_sk,
fs.deviceidentifier,
fs.times,
ss.segment_sk
from (select d.datevalue,
p.product_sk,
deviceidentifier,
count(*) times
from razor_fact_clientdata f,
razor_dim_date d,
razor_dim_product p
where d.datevalue = today
and f.date_sk = d.date_sk
and p.product_sk = f.product_sk
group by d.datevalue,p.product_sk,deviceidentifier) fs,
razor_dim_segment_launch ss,
razor_dim_date dd
where fs.times between ss.startvalue and ss.endvalue
and dd.datevalue = fs.datevalue) fff
group by fff.date_sk,fff.segment_sk,fff.product_sk
order by fff.date_sk,
fff.segment_sk,
fff.product_sk) ffff right join (select fff.date_sk,fff.product_sk,sss.segment_sk
from (select distinct d.date_sk,p.product_sk
from razor_fact_clientdata f,razor_dim_date d,razor_dim_product p
where d.datevalue=today and f.date_sk=d.date_sk and p.product_sk = f.product_sk) fff cross join
razor_dim_segment_launch sss) rightf on ffff.date_sk=rightf.date_sk and
ffff.product_sk=rightf.product_sk and ffff.segment_sk=rightf.segment_sk
on duplicate key update accesscount = values(accesscount); 这里可以把它分成两个小部分的sql来看,比如sql1,
select fs.datevalue,
dd.date_sk,
fs.product_sk,
fs.deviceidentifier,
fs.times,
ss.segment_sk
from (select d.datevalue,
p.product_sk,
deviceidentifier,
count(* ) times
from razor_fact_clientdata f,
razor_dim_date d,
razor_dim_product p
where d.datevalue = str_to_date('2013-08-06','%Y-%m-%d')
and f.date_sk = d.date_sk
and p.product_sk = f.product_sk
group by d.datevalue,p.product_sk,deviceidentifier) fs,
razor_dim_segment_launch ss,
razor_dim_date dd
where fs.times between ss.startvalue and ss.endvalue
and dd.datevalue = fs.datevalue; 它的功能是首先在fact_clientdata表中对app版本id、日期、设备标识进行group,统计出每个app在当天每个设备上的启动次数,然后联合dim表计算出该启动次数对应的segment_sk。 而sql2,
select fff.date_sk,fff.product_sk,sss.segment_sk
from (select distinct d.date_sk,p.product_sk
from razor_fact_clientdata f,razor_dim_date d,razor_dim_product p
where d.datevalue=str_to_date('2013-08-06','%Y-%m-%d') and f.date_sk=d.date_sk and p.product_sk = f.product_sk) fff cross join
razor_dim_segment_launch sss 这里select出来的结果供sql1 right join,这里不太理解,感觉这个join不太有必要,还需要等高人解答。问题提出去之后收到了razor官方的[回答](https://github.com/cobub/razor/issues/84#issuecomment-22306244),这里摘抄一下
right join 的右边是一个cross全表,为了保证每天每个应用的segment启动分段全都显示,没有值就是零.否则如果没有这个分段的值,前面的查询结果是无法显示没有匹配到的segment分段的.
在insert语句的末尾有 on duplicate key update accesscount = values(accesscount)
,保证了一天内多次执行hourly任务的时候,会不断更新razor_fact_launch_daily表中每条记录的count。
##daily任务
daily任务每天执行一次,调用archive.php中的archiveUsingLog
方法,通过这个函数来调用存储过程 rundaily 。
未完待续….