Razor 服务器端代码分析(一)

目录

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_skdevicesupplier_sk等字段都会被替换为对应dim表中的ID。fact表中统计的数据也会分为不同的时间维度,比如razor_fact_usinglograzor_fact_usinglog_daily表,后者就是数据在天的维度上做一个汇总,同样的还有razor_fact_reserveusers_monthlyrazor_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.shrazor_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任务里面调用的存储过程分别是rundimrunfactrunsum,从命名中就能大致看出他们的用处是什么,rundim的逻辑比较简单,这里就不作分析了,有兴趣的同学可以自己去数据库的warehouse里查看,这里主要看看runfact所作的任务。在前面引用的data warehouse设计中,fact表划分包括 transaction/periodic snapshot/accumulating snapshot 三种,runfact主要做的任务应该是transaction,将数据从原始数据库中插入razor_fact_clientdata 用户客户端数据、razor_fact_usinglograzor_fact_errorlograzor_fact_event,这里在插入数据时,会join一个或者多个dim表。

runsum任务做的,就类似上面提到过的 period snapshot 的工作,下面挨个分析:

  • update fact_clientdata 初始入库的数据isnew_channelisnew这两个字段都是默认值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 这个表主要是记录用户的使用行为。accesscounttotaltime来自对razor_fact_usinglog表中数据的累加,exitcountactivity_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 。

未完待续….