一个功能上线后,其实研发内心根本没底儿,不知道这个功能上线之后是否是真的没问题;有经验一些老同窗还知道直接登陆线上机器去tail -f php.error.log
,可是对于新同窗来讲,基本就只能等着被通知
服务故障。javascript
退一步说,即使是能去线上去tail -f
查看错误日志,可是线上是多集群部署的,服务器都特别多,研发不可能在每一台机器上都能看到日志;即使是有日志收集机器,也得在各个集群下分别tail -f
,定位问题很不方便!php
再退一步说,即使是在线上机器看到了php错误日志,也并无足够多的信息辅助信息可以迅速定位出来,怎样的一次访问请求,致使了这个错误。由于php记录的日志通常都是这个格式:java
[22-Oct-2015 18:39:04 Asia/Shanghai] PHP Fatal error: Call to a member function prepare() on null in /home/work/phplib/db/Database.class.php on line 238
真正线上出问题较多的,其实仍是系统运行过程当中;好比流量忽然增长致使接口处理出错、所依赖的第三方服务宕掉致使的程序错误、网络缘由致使接口不能正常工做,等等。由于平时系统运行中,你们不会有专门的人去线上日志机器一直tail -f
进行观察,效率低,且不现实。nginx
一个接口,可能由于产品上的各类缘由,研发会不停地往上面打补丁进行实现,不少状况下,会由于功能上线比较紧张,因此实现过程当中忽略了接口性能。在一段时间内,一个接口的响应时间从100ms上升到300ms,接口可用性从99.99降低到90.00;也许在正常状况下,咱们不会感知到逐渐改造后的接口对线上形成了什么影响,但其实否则,接口SLA
很是重要!但是,这些信息咱们经过什么样的方式才能得知呢,真正能提供这些信息的同窗,并很少!redis
还有一些状况是,线上出了问题,且其余组的同窗帮助定位到大体的问题范围,抛到研发群之后,没人主动响应;你们都会以为我没改过这个东西
,因此忽略了;因而一个线上问题就只能等着Leader来安排跟进,不然就石沉大海,长期影响用户使用。数据库
综上,咱们必需要有一套自动化的线上服务监控和预警方案,主动发现,及时跟进!json
为了能对线上服务情况了如指掌,咱们须要监控的内容必定得是很全的,但一开始得有一个重点监控的范围,也是平时最容易出问题的地方:api
包括语法错误、以及运行期间的Fatal、Warning等,均可以借助PHP提供的register_sutdown_function
和set_error_handler
组合的形式来实现:缓存
/** * 统一截获并处理错误 * @return bool */ public static function registErrorHandler() { $e_types = array ( E_ERROR => 'PHP Fatal', E_WARNING => 'PHP Warning', E_PARSE => 'PHP Parse Error', E_NOTICE => 'PHP Notice' ); register_shutdown_function(function () use ($e_types) { $error = error_get_last(); if ($error['type'] != E_NOTICE && !empty($error['message'])) { $error['trace'] = self::getStackTrace(); self::error_handler($error); } }); set_error_handler(function ($type, $message, $file, $line) use ($e_types) { if ($type != E_NOTICE && !empty($message)) { $error = array ( 'type' => $type, 'message' => $message, 'file' => $file, 'line' => $line, 'trace' => self::getStackTrace() ); self::error_handler($error); // 被截获的错误,从新输出到错误文件 error_log(($e_types[$type] ?: 'Unknown Problem') . ' : ' . $message . ' in ' . $file . ' on line ' . $line . "\n"); } }, E_ALL); }
固然,这个须要在程序的入口处进行注册,保证每一次的程序执行,都能成功捕获错误:bash
// 全局异常捕获 MonitorManager::registErrorHandler();
经过这个方式,咱们在业务层就能彻底捕获接口执行过程当中的任意错误。
在各个SDK
内部,将执行过程当中的异常都向上抛出(throw new Exception
),内容尽量详细,包括:
同时,咱们经过一个统一工具方法进行收集错误日志,下面说如何收集
。
全部的错误不采起直接上报,由于这必然会直接影响当前接口的性能,因此采起队列方式进行收集,即:业务层或SDK中有错误产生时,统一经过一个工具方法进行收集,收集以后,将该错误内容直接入队列,另外开启一个队列实时消耗进程,将队列中的错误日志数据上报到服务器进行处理。
/** * 添加监控日志,日志会被异步收集到日志平台进行展现 * * @param $type * @param $content */ public static function collect($type, $content) { // 线上集群,而且开关处于打开状态,才进行收集 if (Utilities::isOnlineCluster() && SwitchManager::getSwitch('collect', SwitchManager::SWITCH_MONITOR)) { // 检查当前这种监控类型是否支持 if (self::support($type) && !self::checkWhiteList($type, $content)) { self::queueInstance()->enQueue(json_encode(array ( 'type' => $type, 'data' => $content, 'cluster' => Utilities::getClusterName(), 'reqtime' => isset($_SERVER['REQUEST_TIME']) ? $_SERVER['REQUEST_TIME'] : time(), 'extinfo' => array ( 'domain' => $_SERVER['SERVER_NAME'], 'path' => isset($_SERVER['QUERY_STRING']) ? str_replace('?' . $_SERVER['QUERY_STRING'], '', $_SERVER['REQUEST_URI']) : 'script', 'userAgent' => isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '', 'referer' => isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '', 'serverIp' => Utilities::getServerPhpIP(), 'reqData' => json_encode($_REQUEST, true) ) ), true)); } } }
从上面的方法可看出,除了具体的错误日志,咱们还一并收集了一些很是重要的辅助信息,好比当前集群、出问题的域名、对应接口、userAgent、请求参数、接口从哪儿来的等等。
<?php namespace Mlservice\Script\Monitor; use Framework\Libs\Monitor\MonitorManager; use Framework\Libs\Util\Utilities; /** * 从MC队列中,将各类错误日志上报到日志平台进行汇总监控、报警等 * Class Collect * @package Mlservice\Script\Monitor * @author xianliezhao */ class Collect extends \Framework\FrameworkScript { private $limit = 5; private $interval = 600; public function run() { // 检查脚本可执行 $this->checkCluster(); $start = time(); while (true) { $index = 0; $params = array (); while (true) { $index++; $item = MonitorManager::queueInstance()->deQueue(); if (!empty($item)) { $params[] = $item; } else { // 若是数据为空,则10分钟清理一次队列,作一次初始化,且自杀进程 if (time() - $start > $this->interval) { MonitorManager::queueInstance()->makeEmpty(); exit(0); } break; } if ($index >= $this->limit) { break; } } // 发送到服务器,统一收集 if (!empty($params)) { Utilities::apiRequest('bizfe', 'feapi/monitor/mon/collect', $params); } else { sleep(1); } } } }
队列消耗机制作的很简单,不须要采集到全部的错误,只要保证线上有错误了,咱们能第一时间得知,便可。 日志每最多收集满5条就上报一次,经过HTTP请求方式,上报到bizfe::/feapi/monitor/mon/collect
。
对于这种内容和结构灵活多变的数据,采用MongoDB存储再合适不过了,只须要定义一个简单的一级表结构便可:
/** * 错误日志采集的表结构 * @type {*|Model} */ var monModel = connection.model('monitor', new Schema({ type: String, cluster: String, product: String, data: Object, extinfo: Object, reqtime: Number }, { autoIndex: true }));
/** * 对数据进行加工,而后保存日志到db,批量保存成功之后再进行报警检测 * @param messageModel */ function save(messageModel) { return function (reqParams, callback) { var params = []; for (var i in reqParams) { var item = JSON.parse(reqParams[i]); // 忽略来自标准环境的任何错误 if (item.extinfo.domain.indexOf('rdlab') > 0) { continue; } // 从域名中记录下出问题的模块名称 item.product = item.extinfo.domain.split('\.')[0]; item = cleanParams(messageModel, item); params.push(item); } var count = params.length; if (count == 0) { callback(); } var saveData = []; var done = 0; // 批量保存 params.forEach(function (item) { new messageModel(item).save(function (err, product, effectRows) { !err && (item._id = product._id); saveData.push(item); if (++done == count) { // 当全部的错误日志都进入db成功之后,开始进行报警检测(内存中会维护一个错误池) alarm.addAndCheckPool(saveData); if (err) { callback(err, null); } else { callback(null, {error_code: 0, data: {}}); } } }); }); }; }
经过alarm.addAndCheckPool
会在内存中维护一个日志错误池,只须要开启一个子进程每秒检测错误池中的数据,进行阈值检测便可。
/** * 每秒检测一次,各个错误类型只要达到邮件或者短信的最大阈值,则进行报警 */ var doAlarm = function () { var INTERVAL_TIME = 1000; // 启用监控 if (!alarmListenIng) { alarmListenIng = true; } else { return false; } setInterval(function () { // 遍历全部类型,判断是否进行报警 Object.keys(cachePool).forEach(function (type) { // 当前时间 var nowTime = (new Date()).getTime(); // 控制每分钟最多只能报警N次 var alarmCount = cachePool[type]['alarmCount']; if (alarmCount === undefined) { initCacheByType(type, 1); } else { var lastMtime = cachePool[type]['lastMtime']; if ((nowTime - lastMtime) / 1000 > 60) { // 超过1分钟,直接进行数据从新初始化 initCacheByType(type, 1); } else if (alarmCount >= cachePool[type]['alarmCpm']) { // 若是每分钟的报警次数超过阈值,就不报警了 return false; } } // 控制每N秒内报警一次 var lastAlarmTime = cachePool[type]['lastAlarmTime']; if (lastAlarmTime === undefined) { cachePool[type]['lastAlarmTime'] = nowTime; } else if (Math.ceil((nowTime - lastAlarmTime) / 1000) >= cachePool[type]['timeInterval']) { cachePool[type]['lastAlarmTime'] = nowTime; // 这种状况下,才代表须要报警 if (cachePool[type]['alarms'] && cachePool[type]['alarms']['total'] >= cachePool[type]['maxNumForMail']) { cachePool[type]['type'] = type; cachePool[type]['alarmCount'] += 1; var theAlarmData = cachePool[type]; theAlarmData.theTime = Math.ceil((nowTime - lastAlarmTime) / 1000) || 1; // 邮件报警 sendEmail(buildEmailAlarmContent(theAlarmData)); // 若是是出错量比设定的短信阈值还大,则短信报警 if (cachePool[type]['maxNumForSms'] <= cachePool[type]['alarms']['total']) { sendSmsMessage(buildSmsAlarmContent(theAlarmData)); } // 清空 initCacheByType(type, 0); } } }); }, INTERVAL_TIME); };
固然,各类错误的不一样阈值为了往后的维护,也抽离成配置单独管理,更为合适:
/** * 报警阈值设定 */ alarmLimits: { db_log: { timeInterval: 3, // 每隔3s监控一次 maxNumForMail: 10, // 邮件报警阈值 maxNumForSms: 50, // 短信报警阈值 alarmCpm: 5 // 表示每分钟最多报警5次 }, redis_log: { timeInterval: 3, maxNumForMail: 10, maxNumForSms: 50, alarmCpm: 5 }, mc_log: { timeInterval: 3, maxNumForMail: 5, maxNumForSms: 20, alarmCpm: 5 }, mq_error: { timeInterval: 3, maxNumForMail: 10, maxNumForSms: 30, alarmCpm: 5 }, php_error: { timeInterval: 3, maxNumForMail: 5, maxNumForSms: 20, alarmCpm: 5 }, php_warning: { timeInterval: 3, maxNumForMail: 10, maxNumForSms: 50, alarmCpm: 5 } }
按照这套流程下来,线上只要出任何错误,都会被实时上报到日志服务器,以php_error
为例,每隔3秒检测一次,若是累积出现5次错误,则采起邮件方式进行报警,若是累积出现20次错误,则可理解为错误较严重,进行短信报警!
对于不一样类型的错误报警,会发送给不一样的接收人,抄送给大组,保证该次报警必定不会被忽略。同时提供一个Web平台,对日志进行分析展示,可查询某个错误的详细信息,快速分析出问题出如今什么地方;通常状况下,经过该平台的日志详情页,能够一眼就判断出来该错误应该采起什么方式去修复。
这部分的数据,能够直接从Nginx日志
中进行提取,首先,咱们能够来看看一条完整的Nginx日志包含的内容:
[x.x.x.x] [-] [23/Oct/2015:14:59:59 +0800] [GET /goods/goods_info?goods_id=276096349&fields=platform_type%2Cgoods_id%2Cgoods_param%2Cgoods_detail%2Cshop_id%2Cfirst_sort_id%2Csize%2Cgoods_desc HTTP/1.1] [200] [1957] [xxx.com/share/goods_details] [Snake Connect] [-] [0.010] [y.y.y.y:9999] [0.010] [-] [uid:0;ip:0.0.0.0;v:0;master:0;is_mob:0]
基本就是这个格式:
[$remote_addr] [$remote_user] [$time_local] [$request] [$status] [$body_bytes_sent] [$http_referer] [$http_user_agent] [$http_user_agent] [$http_x_forwarded_for] [$request_time] [$upstream_addr] [$upstream_response_time] [$request_body]
固然,Nginx日志收集的格式,是能够在Nginx配置文件中进行自定义的,具体看业务层须要怎么分析。
基于上面已经产生的这个日志,咱们能够经过这几个数据来作接口性能监控:
$request
具体的接口名称$status
该请求对应的执行状态(200:成功;499:超时;502:服务挂了;500:多是有Fatal...),经过这个信息来衡量接口的可用性
$request_time
一个接口的完整执行时间,经过这个值来衡量接口的响应时间
对于须要监控的对象,能够经过白名单的方式,指定对某些接口进行监控,可是这样不够灵活,尤为是一个服务下的接口在不断增长,常常更新监控的接口列表,维护成本较高。
还有比较智能的方法,就是根据某个接口的访问量,取前N个进行监控,好比能够经过这样的方式来获取监控接口列表:
# 日期 date_time=$(date -d "-1 hours" "+%Y%m%d%H") # 文件位置 file_name="/home/service/nginx/log/xxx.mlservice.access.${date_time}.log" # 获取监控列表 api_list=$(awk '{print $6}' ${file_name} | grep -v 'your_filter_api_here' | sed -e "s/\?.*//" -e "s/^\///" | tr '[A-Z]' '[a-z]' | sort | uniq -c | sort -nr | head -n 20 | awk '{print $2}') ;
这里的api_list
就是是动态获取到的监控对象了,结果形如:
goods/goods_info goods/campaign_info inventory/get_skuinfo inventory/set_inventory campaign/update_campaign campaign/goods_info inventory/spu_set inventory/inventory_decr ...
对于数据的采集,就能够直接经过上面的监控对象,利用grep
提取全部相关的数据,而后经过awk
逐条进行分析,最终得出平均值,输出结果。而对数据结果的上报,直接经过curl
方式发送到bizfe平台进行统一存储以及集中展示。
线上服务出现任何问题,做为一线研发,都应该第一时间知道出了什么问题、问题出在哪儿、大体的影响范围是什么、大体如何修复等。绝对不是等着用户来反馈了,咱们才被动的去找用户报的问题,如何复现?
!
固然,咱们也不能成为监控报警的重度患者
,凡事也得有个度,若是线上无论是什么样的log都经过报警的方式发出来,就真成了扰民
了!