一個功能上線後,其實研發內心根本沒底兒,不知道這個功能上線之後是否是真的沒問題;有經驗一些老同窗還知道直接登陸線上機器去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都經過報警的方式發出來,就真成了擾民
了!