需求背景:有个调用统计日志存储和统计需求,要求存储到 mysql 中;存储数据高峰能达到日均千万,瓶颈在于直接入库并发太高,可能会把 mysql 干垮。

问题分析

思考:应用网站架构的衍化过程中,应用最新的框架和工具技术固然是最优选择;但是,如果能在现有的框架的基础上提出简单可依赖的解决方案,未尝不是一种提升自我的尝试。解决:

  • 问题一:要求日志最好入库;但是,直接入库 mysql 确实扛不住,批量入库没有问题,done。【批量入库和直接入库性能差异参考文章】
  • 问题二:批量入库就需要有高并发的消息队列,决定采用 redis list 仿真实现,而且方便回滚。
  • 问题三:日志量毕竟大,保存最近 30 条足矣,决定用 php 写个离线统计和清理脚本。

done,下面是小拽的简单实现过程

一:设计数据库表和存储

  • 考虑到 log 系统对数据库的性能更多一些,稳定性和安全性没有那么高, 
    1. 存储引擎自然是只支持select insert 没有索引的archive
    。如果确实有 update 需求,也可以采用 myISAM。
  • 考虑到 log 是实时记录的所有数据,数量可能巨大, 
    1. 主键采用bigint,自增即可
     。
  • 考虑到 log 系统 
    1. 以写为主,统计采用离线计算,字段均不要出现索引
     ,因为一方面可能会影响插入数据效率,另外读时候会造成死锁,影响写数据。

二:redis 存储数据形成消息队列

由于高并发,尽可能简单,直接,上代码。

  1. connect('xx', 6379);
  2. $redis->auth("password");
  3. // 加上时间戳存入队列
  4. $now_time = date("Y-m-d H:i:s");
  5. $redis->rPush("call_log", $interface_info . "%" . $now_time);
  6. $redis->close();
  7.  
  8.  
  9. / vim: set ts=4 sw=4 sts=4 tw=100 /
  10. ?>

三:数据定时批量入库。

定时读取 redis 消息队列里面的数据,批量入库。

  1. connect('ip', port);
  2. $redis_xx->auth("password");
  3. // 获取现有消息队列的长度
  4. $count = 0;
  5. $max = $redis_xx->lLen("call_log");
  6.  
  7.  
  8. // 获取消息队列的内容,拼接sql
  9. $insert_sql = "insert into fb_call_log (interface_name, createtime) values ";
  10.  
  11.  
  12. // 回滚数组
  13. $roll_back_arr = array();
  14.  
  15.  
  16. while ($count lPop("call_log");
  17.     $roll_back_arr = $log_info;
  18.     if ($log_info == 'nil' || !isset($log_info)) {
  19.         $insert_sql .= ";";
  20.         break;
  21.     }
  22.  
  23.  
  24.     // 切割出时间和info
  25.     $log_info_arr = explode("%",$log_info);
  26.     $insert_sql .= " ('".$log_info_arr[0]."','".$log_info_arr[1]."'),";
  27.     $count++;
  28. }
  29.  
  30.  
  31. // 判定存在数据,批量入库
  32. if ($count != 0) {
  33.     $link_2004 = mysql_connect('ip:port', 'user', 'password');
  34.     if (!$link_2004) {
  35.         die("Could not connect:" . mysql_error());
  36.     }
  37.  
  38.  
  39.     $crowd_db = mysql_select_db('fb_log', $link_2004);
  40.     $insert_sql = rtrim($insert_sql,",").";";
  41.     $res = mysql_query($insert_sql);
  42.  
  43.  
  44.     // 输出入库log和入库结果;
  45.     echo date("Y-m-d H:i:s")."insert ".$count." log info result:";
  46.     echo json_encode($res);
  47.     echo "n";
  48.  
  49.  
  50.     // 数据库插入失败回滚
  51.     if(!$res){
  52.        foreach($roll_back_arr as $k){
  53.            $redis_xx->rPush("call_log", $k);
  54.        }
  55.     }
  56.  
  57.  
  58.     // 释放连接
  59.     mysql_free_result($res);
  60.     mysql_close($link_2004);
  61. }
  62.  
  63.  
  64. // 释放redis
  65. $redis_cq01->close();
  66. ?>

四:离线天级统计和清理数据脚本

  1. ? php
  2. /**
  3. * static log :每天离线统计代码日志和删除五天前的日志
  4. *
  5. * @Author:cuihuan@baidu.com
  6. * 2015-11-06
  7.  /
  8. // 离线统计
  9. $link_2004 = mysql_connect('ip:port', 'user', 'pwd');
  10. if (!$link_2004) {
  11.     die("Could not connect:" . mysql_error());
  12. }
  13.  
  14.  
  15. $crowd_db = mysql_select_db('fb_log', $link_2004);
  16.  
  17.  
  18. // 统计昨天的数据
  19. $day_time = date("Y-m-d", time() - 60  60  24 * 1);
  20. $static_sql = "get sql";
  21.  
  22.  
  23. $res = mysql_query($static_sql, $link_2004);
  24.  
  25.  
  26. // 获取结果入库略
  27.  
  28.  
  29. // 清理15天之前的数据
  30. $before_15_day = date("Y-m-d", time() - 60  60  24  15);
  31. $delete_sql = "delete from xxx where createtime*/

五:代码部署

主要是部署,批量入库脚本的调用和天级统计脚本,crontab 例行运行。

  1. # 批量入库脚本
  2. /2     /home/cuihuan/xxx/lamp/php5/bin/php/home/cuihuan/xxx/batchLog.php>>/home/cuihuan/xxx/batchlog.log#
  3.  天级统计脚本
  4. 0 5   * /home/cuihuan/xxx/php5/bin/php/home/cuihuan/xxx/staticLog.php>>/home/cuihuan/xxx/staticLog.log

总结:相对于其他复杂的方式处理高并发,这个解决方案简单有效:通过 redis 缓存抗压,mysql 批量入库解决数据库瓶颈,离线计算解决统计数据,通过定期清理保证库的大小。