@leptune
2017-03-02T07:07:56.000000Z
字数 13615
阅读 10635
唯中科技
从海量的业务日志中,挖掘出统计数据。
从下列原始数据
要加工得到:
这就是统计系统做的事!

用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`文件
<?phpnamespace submit_order; // 哪个api的任务就写那个apirequire_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:amountlist($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`修改如下:
<?phpnamespace 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:amountlist($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