@leptune
2017-03-02T07:07:56.000000Z
字数 13615
阅读 10578
唯中科技
从海量的业务日志中,挖掘出统计数据。
从下列原始数据
要加工得到:
这就是统计系统做的事!
用redis的List(列表)来存储,一个api对应一个列表。api有:
用redis的Hash(哈希表)来存储,一个task(任务)对应一个哈希表。任务集有:
├── ApiServer // 统计底层相关
│ ├── backup // 日志备份目录
│ │ ├── ... // 众多日志
│ ├── data
│ │ ├── cronfile // 计划任务,通过crontab cronfile命令来装载任务
│ │ ├── GeoIP.conf // 用来获取ip地址库的配置文件
│ │ ├── GeoLite2-City.mmdb // ip国家库
│ │ └── GeoLite2-Country.mmdb // ip城市库
│ ├── pid // 众多进程的pid文件(暂时没用到,后续可以用于监控进程)
│ │ ├── ... // pid文件
│ ├── log // 今日日志(众多进程的,如实时分析进程,分析历史数据进程,校验数据进程等)
│ │ ├── ... // 众多日志
│ ├── process // 第三方工具安装文件(方便迁移时可以直接使用)
│ │ ├── geoipupdate // 更新ip地址库
│ │ │ ├── ...
│ │ ├── mmonit // 监控工具WEB界面(暂时尚未布置)
│ │ │ ├── ...
│ │ └── monit // 监控工具(暂时尚未布置)
│ │ ├── ...
│ ├── src // 【统计核心目录】--统计源码!
│ │ ├── Base // 顶级文件
│ │ │ ├── Base.php
│ │ │ ├── ConsumerBase.php
│ │ │ ├── ConsumerImportBase.php
│ │ │ └── TasksBase.php
│ │ ├── Common // 公共文件
│ │ │ └── config.php
│ │ ├── Consumer // 消费者
│ │ │ ├── ConsumerHistory.php // 分析历史数据
│ │ │ └── ConsumerRealTime.php // 分析实时数据
│ │ ├── GetApiFuncs // 提供给open平台、推广平台等的Api
│ │ │ ├── ...
│ │ ├── Hooks // 没用到
│ │ │ └── Hooks.php
│ │ ├── Import // 导入历史数据
│ │ │ ├──...
│ │ ├── Org // 第三方插件
│ │ │ ├── ...
│ │ │ └── Sync.php // redis里的预定义变量
│ │ ├── Producter // 生产者
│ │ │ ├── GetApi.php // 读取数据的Api处理
│ │ │ └── Producter.php // Api顶级文件,接收客户端请求
│ │ ├── Scheduled // 计划任务程序
│ │ │ └── check_and_fix.php // 定时校验并修复数据
│ │ └── Tasks // 统计任务集
│ │ ├── submit_download // 下载
│ │ │ ├── ...
│ │ ├── submit_login // SDK登录
│ │ │ ├── ...
│ │ ├── submit_online_time // 在线时长
│ │ │ ├── ...
│ │ ├── submit_order // SDK订单
│ │ │ ├── ...
│ │ ├── submit_order_other // SDK订单的补单、退单等
│ │ │ ├── ...
│ │ ├── submit_regist // 说玩注册_SDK
│ │ │ ├── ...
│ │ ├── submit_search // 搜索
│ │ │ ├── ...
│ │ ├── submit_shuowan_game_role // SDK注册
│ │ │ ├── ...
│ │ ├── submit_shuowan_login // 说玩登录
│ │ │ ├── ...
│ │ └── submit_shuowan_regist // 说玩注册
│ │ │ ├── ...
│ ├── test // 测试文件目录(只测试用)
│ │ ├── ...
│ └── tools // 【统计核心目录】--维护统计项目脚本(密码:查看check.sh文件)
│ ├── auto_reload.sh // 【计划任务】--自动重载php-fpm(防止其内存溢出)(每分钟执行)
│ ├── backup_log.sh // 【计划任务】--备份日志(每天凌晨)
│ ├── restartApi.sh // 【计划任务】--重新分析某API的全部数据
│ ├── restartImport.sh // 【计划任务】--重新导入某API的历史数据
│ ├── sync_geolite_date.sh // 【计划任务】--定时更新ip地址库(每周一凌晨1点执行)
│ ├── check.sh // 【辅助脚本】--脚本环境校验,用于其他脚本调用
│ ├── restartConsumeRealTimeProcess.sh // 【辅助脚本】--重启实时分析进程(更改实时分析代码或统计任务时,必须运行此脚本)
│ ├── restartTask.sh // 【辅助脚本】--重新分析某任务
│ ├── run_consumer.sh // 【辅助脚本】--运行某php程序为守护进程
│ ├── count.sh // 【无需了解】--统计redis某列表类型长度
│ └── update_json.sh // 【无需了解】--统计API文档
│ ├── init.sh // 【慎用!】--初始化统计系统
│ ├── restart.sh // 【慎用!】--重新分析所有统计任务
├── App // 【统计核心目录】--统计后台管理
│ ├── ... // Thinkphp框架文件
├── bower_components // 前端插件
│ ├── ...
├── bower.json // 前端bower管理插件配置文件
├── crossdomain.xml // 跨域策略文件
├── favicon.ico // 网站图标
├── index.php // TP入口文件
├── Libs // TP底层
│ ├── ...
├── Public // TP public文件
│ ├── ...
├── robots.txt // 防止百度等蜘蛛文件
├── .htaccess // nginx重定向路由文件
├── static // 资源文件
│ ├──...
- Thinkphp框架
- 用于统计后台页面
- Swoole框架
- 用于统计Api,框架官网:http://www.swoole.com
- sosoapi
- 用于撰写API文档,官网:http://www.sosoapi.com leptune@sina.com 密码查看
ApiServer/tools/check.sh
文件- Redis存储
- 用于存储数据,文档:http://redis.cn/commands.html
- Shell脚本
- 用于日常维护及统计任务的管理,见
ApiServer/tools
目录- Linux计划任务crontab
- 用于统计的计划任务,见
ApiServer/data/cronfile
文件- Medoo
- 用于对Mysql数据库进行增删改查,文档:http://medoo.in/doc
- Git
- 内存要求
- 至少16G
- 数据库:请查看ApiServer/src/Common/config.php文件
- 统计后台:http://admin.dbtrix.com admin 密码查看
ApiServer/tools/check.sh
文件- 统计终端:请联系运维获知
- git仓库地址:ssh://gituser@125.208.12.83:1283/data/git/tongji (密码请联系运维获知)
- 假设给SDK订单增加个统计任务,要统计每天每个游戏的失败订单量及失败订单额
首先,在`ApiServer/src/Tasks/submit_order`文件夹增加`TasksFail.php`文件
<?php
namespace submit_order; // 哪个api的任务就写那个api
require_once __DIR__ . "/../../Base/TasksBase.php"; // 引入base文件
class TasksFail extends \TasksBase { // 请保证类名和文件名一致!
public function execute() { // 任务入口函数,每个任务文件都必须有该函数!
return $this->_execute(function ($data) { // 调用base文件的_execute函数,来遍历要统计的数据
if ($data["pay_status"] == 0) { // 失败的订单
$fields = []; // $fields数组为要自增的数据集
$fields["{$data['gameid']}:count"] = 1; // 每来一笔失败订单就自增1,统计订单数
$fields["{$data['gameid']}:amount"] = $data['amount']; // 每来一笔失败订单就自增amount,统计订单额
return $fields;
}
return false; // 如果不是失败的,则不处理这条数据
}, true);
// _execute函数接收三个参数:
// 【$func】:回调函数
// 【$inCreByFloat】:是否以浮点数模式自增,默认false
// 【$timeFormat】:统计任务集名字后缀的时间格式,默认'Ymd'(即统计集按每天划分)
}
}
cd /data/www/wztj # 切换到统计目录
sudo chmod 0777 -R . # 更改文件权限
sudo chown www:www -R . # 更改文件拥有者
php ApiServer/src/Consumer/ConsumerHistory.php submit_order TasksFail # 生成统计数据
redis-cli keys '*:submit_order:TasksFail:*' # 确认有无生成统计数据
redis-cli hgetall analyze:submit_order:TasksFail:20160725 # 查看某天的统计数据
在`App/Admin/View/Analyze/order.html`文件中的`<script></script>`里在最后一行加入:
// drawEs函数是封装的用于生成统计图表的函数
drawEs({
type:'table', // 图表类型,详见static/js/draw_es.js的genEsOption函数
id:'fail_count', // 后台Controller对应的函数名
name:'失败订单分析', // 统计图表名字
cols:12, // 统计图表宽度
});
保存后,刷新页面,打开统计后台的`分析图表-SDK订单分析`最下面,就会出现`失败订单分析`了
在`App/Admin/Controller/AnalyzeOrderController.class.php`文件的最下面增加代码:
public function fail_count() {
$Ymd = date('Ymd', strtotime(I('get.Ymd', date('Y-m-d')))); // 默认今天
$data = $this->redis->hGetAll("analyze:{$this->api}:TasksFail:{$Ymd}"); // 取出统计数据
$res = [];
foreach ($data as $key => $count) {
// key格式为:gameid:count或gameid:amount
list($gameid, $type) = explode(':', $key);
$res[$gameid][$type] += $count;
}
$gameid2name = $this->sdkGameId2Name(array_keys($res)); // 从redis缓存里获取gameid对应的游戏名,不从缓存获取也可,可以直接查数据库
foreach ($res as $gameid => $d) {
$res[$gameid]['game_name'] = $gameid2name[$gameid]; // 获取游戏名
}
// 此为统计图表table所需的表结构,含义分别为:
// name => 字段名
// desc => 字段描述
// sorttype => 排序方式
// width => 该字段要显示的宽度
$model = [
['name' => 'game_name' , 'desc' => "游戏名" , 'sorttype' => 'text' , 'width' => '30'] ,
['name' => 'count' , 'desc' => '交易量' , 'sorttype' => 'number' , 'width' => '15'] ,
['name' => 'amount' , 'desc' => '交易额' , 'sorttype' => 'number' , 'width' => '15'] ,
];
return $this->ajaxReturn(['status' => 200, 'data' => [
'data' => array_values($res), // 统计数据
'model' => $model,
]]);
}
好了,现在刷新页面看看,已经有数据了!至此,增加一个统计任务的流程已结束!
- 下面,就修改上面增加的统计任务,让失败订单分析统计增加
渠道名
输出,并增加搜索渠道名功能吧!
# 先把该任务从实时任务集里给移除
redis-cli srem tasks:submit_order TasksFail
# 先确认要删除的key(每个任务集都会生成个isEnd:history:submit_order:TasksFail类似的任务,用来检测php ApiServer/src/Consumer/ConsumerHistory.php submit_order TasksFail该命令是否结束,此任务必须删除)
r keys '*submit_order:TasksFail*'
# 删除任务遗留数据(注:得先知道你写的任务会生成哪些统计key,然后再谨慎删除!)
r del `r keys '*submit_order:TasksFail*'`
将上面的`ApiServer/src/Tasks/submit_order/TasksFail.php`修改如下:
<?php
namespace submit_order;
require_once __DIR__ . "/../../Base/TasksBase.php";
class TasksFail extends \TasksBase {
public function execute() {
return $this->_execute(function ($data) {
if ($data["pay_status"] == 0) {
$fields = [];
// 将下面两行改为如下:(加入了渠道标识,好展示渠道名)
$fields["{$data['chan_no']}:{$data['gameid']}:count"] = 1;
$fields["{$data['chan_no']}:{$data['gameid']}:amount"] = $data['amount'];
return $fields;
}
return false;
}, true);
}
}
public function fail_count() {
$Ymd = date('Ymd', strtotime(I('get.Ymd', date('Y-m-d'))));
$searchChanNo = I('get.key'.__function__.'0_id', NULL);
$data = $this->redis->hGetAll("analyze:{$this->api}:TasksFail:{$Ymd}");
$res = [];
// 增加下面两行
$gameids = [];
$chanNos = [];
// 修改此foreach为:
foreach ($data as $key => $count) {
// key格式为:chan_no:gameid:count或chan_no:gameid:amount
list($chanNo, $gameid, $type) = explode(':', $key);
if ($searchChanNo && $chanNo != $searchChanNo) {
continue;
}
$res["{$chanNo}:{$gameid}"][$type] += $count;
$gameids []= $gameid;
$chanNos []= $chanNo;
}
// 修改下面两行为:
$gameid2name = $this->sdkGameId2Name($gameids);
$channo2name = $this->channelId2Name($chanNos);
// 修改此foreach为:
foreach ($res as $key => $d) {
list($chanNo, $gameid) = explode(':', $key);
$res[$key]['channel_name'] = $channo2name[$chanNo];
$res[$key]['game_name'] = $gameid2name[$gameid];
}
$model = [
// 增加下面一行
['name' => 'channel_name' , 'desc' => "渠道名" , 'sorttype' => 'text' , 'width' => '30'] ,
['name' => 'game_name' , 'desc' => "游戏名" , 'sorttype' => 'text' , 'width' => '30'] ,
['name' => 'count' , 'desc' => '交易量' , 'sorttype' => 'number' , 'width' => '15'] ,
['name' => 'amount' , 'desc' => '交易额' , 'sorttype' => 'number' , 'width' => '15'] ,
];
return $this->ajaxReturn(['status' => 200, 'data' => [
'data' => array_values($res),
'model' => $model,
'key0' => $this->genKeyword($channo2name),
]]);
}
cd /data/www/wztj # 切换到统计目录
php ApiServer/src/Consumer/ConsumerHistory.php submit_order TasksFail # 生成统计数据
修改`App/Admin/View/Analyze/order.html`中的该任务代码为:
drawEs({
type:'table',
id:'fail_count',
name:'失败订单分析',
cols:12,
// 增加下面一行
search:[{name:'渠道', col:3}],
});
好了,刷新页面,就会发现成功了!
以上,成功修改了该统计任务!
- 下面,就把上面增加的统计任务给删除吧!
# 先把该任务从实时任务集里给移除
redis-cli srem tasks:submit_order TasksFail
# 删除任务代码文件(若不删除,则运行restartApi.sh脚本时,又会自动添加该任务)
rm ApiServer/src/Tasks/submit_order/TasksFail.php
# 先确认要删除的key(每个任务集都会生成个isEnd:history:submit_order:TasksFail类似的任务,用来检测php ApiServer/src/Consumer/ConsumerHistory.php submit_order TasksFail该命令是否结束,此任务必须删除)
r keys '*submit_order:TasksFail*'
# 删除任务遗留数据(注:得先知道你写的任务会生成哪些统计key,然后再谨慎删除!)
r del `r keys '*submit_order:TasksFail*'`
然后,把上面前端和后端的该任务对应的代码也注释或删除
以上,成功删除了该统计任务!
- 计划任务存储在
ApiServer/data/cronfile
- 之所以每天凌晨3点重新导入并分析说玩注册、说玩注册_SDK、SDK注册这三个数据,是因为这三个在用户中心存有原始数据,同步可以保证用户中心数据和统计这边的数据一致!
# 每分钟检查php-fpm是否超过所占内存
* * * * * source /etc/profile;$API_ROOT_PATH/tools/auto_reload.sh
# 每5分钟从订单表导入数据
*/5 * * * * source /etc/profile;$API_ROOT_PATH/tools/run_consumer.sh Import/ConsumerImportOrder 1
*/5 * * * * source /etc/profile;$API_ROOT_PATH/tools/run_consumer.sh Import/ConsumerImportOrderOther
# 每周一凌晨1点同步IP地址数据
0 1 * * 1 source /etc/profile;$API_ROOT_PATH/tools/sync_geolite_date.sh
# 备份统计代码生成的日志数据
0 0 * * * source /etc/profile;$API_ROOT_PATH/tools/backup_log.sh
# 修正数据
0 2 * * * source /etc/profile;$API_ROOT_PATH/tools/run_consumer.sh Scheduled/check_and_fix
# 重新导入并分析订单数据
0 3 * * * source /etc/profile;$API_ROOT_PATH/tools/restartImport.sh submit_order ConsumerReImportOrder nopassword
# 重新导入并分析说玩注册数据
15 3 * * * source /etc/profile;$API_ROOT_PATH/tools/restartImport.sh submit_shuowan_regist ConsumerImportShuowanRegist nopassword
# 重新导入并分析说玩注册_sdk数据
30 3 * * * source /etc/profile;$API_ROOT_PATH/tools/restartImport.sh submit_regist ConsumerImportRegist nopassword
# 重新导入并分析SDK注册数据
45 3 * * * source /etc/profile;$API_ROOT_PATH/tools/restartImport.sh submit_shuowan_game_role ConsumerImportShuowanGameRole nopassword
- 环境: CentOS release 6.5 x86_64
- 硬盘要求:>=500G
- 内存要求:>=16G
- 假设上传到服务器的脚本名为deploy.sh
- 执行如下命令:
# 部署环境
sh deploy.sh
# 注:下面命令必须在内存>=10G时才能执行,否则死机
sh /data/www/wztj/ApiServer/tools/init.sh
r keys '*'|ag -v '^post'|ag -v '^analyze'|ag -v '^smids'|ag -v '^mids'|ag -v '^tasks'|ag -v '^isEnd'|ag -v '^lastSyncId'|ag -v '^beforeRunTasks'|ag -v '^id2name'|ag -v '^const'|ag -v '^convert'|ag -v '^other'|ag -v 'search:submit_online_time'
- 万变不离其宗,只要明白项目的输入输出,便一切了然。先理解项目的输入输出,便可自行处理这边提到的一切问题。
- 将输入输出整理如下:
各客户端日志 --> redis的list --> mysql、redis的哈希表等 --> web后台或api查询
所有的项目里的代码,都不过是将其中某个输入,加工成某个输出而已。
下面,就举些常见的问题,来说明一下:
- 现在项目中有做过滤,只统计今天的新增用户。那如果后续需求变化,也要统计老用户呢?要完成此需求,需要先从最原始的输入开始考虑。
- 首先,SDK的在线时长接口有传过来老用户数据,所以这边输入不用改。
- 然后,接口数据是传向
ApiServer/src/Producter/Producter.php
,查看里面的代码,发现不管新老用户,在线时长数据都有存进redis的list,所以这边的输入也不用改。- 接着,存进redis里的list数据被
php /data/www/wztj/ApiServer/src/Consumer/ConsumerRealTime.php submit_online_time
进程读取,查看此php文件, 跟踪代码的数据流向,便可以在ApiServer/src/Base/ConsumerBase.php
中发现如下代码:
if (!$this->redis->sIsMember("analyze:submit_shuowan_game_role:TasksUsernameGameid:{$tmpYmd}", "{$data['username']}:{$data['gameid']}")) { // 非今天新增用户
unset($this->datas[$key]);
}
- 然后将此代码屏蔽!因
php /data/www/wztj/ApiServer/src/Consumer/ConsumerRealTime.php submit_online_time
该进程是启动时将代码导入内存中的,不会实时刷新代码,所以需要重新启动该进程,运行如下命令即可:
cd /data/www/wztj && ApiServer/tools/restartConsumeRealTimeProcess.sh submit_online_time
- 该命令会杀掉该进程后,清理日志,然后重启该进程,并让该进程成为守护进程,并日志文件记录在
ApiServer/log/
下。- 现在,客户端进来的sdk在线时长数据,都可以进行统计了。但还有个问题,那就是以前的老用户数据没进行统计。所以,接下来需要重新统计历史数据,运行如下命令即可:
# 密码请查看`ApiServer/tools/check.sh`文件
cd /data/www/wztj && $API_ROOT_PATH/tools/restartApi.sh submit_online_time log_sdk_online_time
# 如需查看导入进度,则运行此命令来实时显示导入日志:
tail -f ApiServer/log/ConsumerHistorysubmit_online_timeTasks*
# 如果想知道导入结束是否结束,则运行下面命令,如输出为空,则结束
st|grep -v ConsumerRealTime|grep submit_online_time
# 备注:st为/etc/profile里自定义的函数,详请查看此文件的函数定义
# 如果想知道导入过程中是否有错误发生,则运行:
cat ApiServer/log/ConsumerHistorysubmit_online_timeTasks* | less
# 然后在里面查找"php"字符串
- 以上,成功的让sdk在线时长加入了统计老用户功能!!
- 先通过mysql里的数据用sql语句进行查询
- 如结果和统计一样,那么就是源数据错了,此时再追踪源头,看redis里list存储的源数据有无错,每个api对应的redis里的源数据key格式为:【post:{api}:{date}】或【post:{api}:{date}_backup】
- 如结果和统计不一样,那么就是统计代码错了,此时只需到该统计对应的代码中,进行调试,便可知道哪一行错了,修正即可。然后请参考 修改统计任务 ,进行修改即可。
Producter.php
文件,但没生效?
- 运行下面命令重启Producter即可:
kill -9 `st|ag pro|awk '{print $1}'`; ApiServer/tools/run_consumer.sh Producter/Producter
- 如订单表,以前的数据要更新,如更新agent之类的,这时,只能重新导入并生成统计数据。
- 重新导入并生成该api所有任务的统计数据,必定是一件麻烦的事的,所以有写脚本来处理。
- 所有api的重新导入并生成统计数据的脚本命令如下:
- (注:只有客户端有备份原始数据的api才可以重新导入,故只有下面4个支持重新导入)
# 重新导入并分析订单数据
$API_ROOT_PATH/tools/restartImport.sh submit_order ConsumerReImportOrder
# 重新导入并分析说玩注册数据
$API_ROOT_PATH/tools/restartImport.sh submit_shuowan_regist ConsumerImportShuowanRegist
# 重新导入并分析说玩注册_sdk数据
$API_ROOT_PATH/tools/restartImport.sh submit_regist ConsumerImportRegist
# 重新导入并分析SDK注册数据
$API_ROOT_PATH/tools/restartImport.sh submit_shuowan_game_role ConsumerImportShuowanGameRole