https://github.com/hongg-coder/http-manager
前言
相信在場各位的泥腿子(如果大佬請跳過這段話)每天工作都是穿梭在curd和curl的愛恨情仇之中,但是本文不對curd過多講解,讓我們看看curl的日常
場景一
某泥腿子程序員A: 某泥腿子程序員B,在嗎 你們A接口返回的格式不對啊 B接口返回500了啊
某泥腿子程序員B: 沒有啊 我們這裏看都是正常的啊
某泥腿子程序員A:?????
場景二
某泥腿子程序員A: 好像隔壁部門的接口掛了,導致我們接口一直超時把fpm佔滿了,整個系統都掛了
某泥腿子程序員B: 坑比隊友,接口天天掛了
。。。。。省略一大堆的吐槽
領導: 爲什麼我們系統天天掛
某泥腿子程序員A、B:因爲我們調用隔壁部門接口 他們掛了,我們也掛了
領導:你們怎麼不跟着一起掛,給我解決這個問題
於是乎
秉承着能用就行,看看市面上沒有現成的解決方案,泥腿子A打開了某國內搜索引擎 輸入了 php服務熔斷和過載保護 發現一無所獲 只能硬着禿頭開始自己擼一個
插曲:可以把php換成任何的語言都有收穫,具體原因可以自行學習fpm的工作機制
實現的功能
往往我們在設計一個系統或者bug的時候,都需要明確要實現什麼、完成什麼,而不是瞎來
需要實現的功能如下:
1.如果服務超時某個次數,則不再訪問
2.如果服務頻繁掛了,我們需要監控提早處理 ---- 事實上大部分的系統宕機都是後知後覺
3.如果順帶能把每次的請求記錄保存下來 那就是更好啦
那麼歸類爲熔斷、監控、日誌
熔斷
熔斷在請求某個接口的時候去判斷該接口是否能被請求,如果不能請求只能返回對應錯誤碼、或者異常
這裏還會涉及怎麼算是熔斷,我們可以根據每個http請求的開始時間進行判斷,如果A接口在**時間內超時**秒以上的達到**次數認爲這段時間該接口不穩定需要熔斷保護自身的系統
日誌
目前使用了guzzle的http請求的庫 可參照裏面的middleware
https://guzzle-cn.readthedocs.io/zh_CN/latest/quickstart.html
註冊兩個中間件
請求開始中間件
記錄請求的開始時間、請求url、請求參數、請求頭
請求結束中間件
記錄請求返回的response、status、結束時間
類似於
$stack = HandlerStack::create(); $this->result = new Result(); $stack->push(Middleware::mapRequest(function (RequestInterface $request) { $this->result->setRequest($request); $this->result->setStartTime(microtime(true)); return $request; })); $stack->push(Middleware::mapResponse(function (ResponseInterface $response) { $this->result->setResponse($response); $this->result->setEndTime(microtime(true)); // 把result對象傳入日誌類處理 return $response; }));
監控
我們可以計算下什麼時候需要監控
1.服務出現非正常狀態返回 (400~500)
2.服務超時
3.對某個服務進行熔斷
那麼整個流程我們可以歸類爲
talk is cheap, show me the code
約束說明
監控 約束Interface
<?php namespace Hgg\HttpManager\Contracts; use Hgg\HttpManager\UrlRule;use GuzzleHttp\Exception\RequestException; interface MonitInterface { public function requestExceptionReport(RequestException $requestException); public function curlErrorReport(UrlRule $urlRule); public function lockReport(UrlRule $urlRule); } |
requestExceptionReport
觸發條件:http請求出發了Guzzle RequestException 異常
監控目的:需要告訴大羣 這個接口發生了異常 一般都是第三方服務的崩潰
推薦實現:將異常信息和request對象信息組裝成消息發到微信報警羣
curlErrorReport
觸發條件:http請求的失敗次數(response的code 認爲失敗)在一定期間(UrlRule.$errorInterval)那達到設置的次數(UrlRule.$errorLimit)
監控目的:需要告訴大羣 這個接口 一直在失敗 一般都是第三方服務故障
推薦實現: ****接口在***事件那失敗次數達到****次數 發送到微信報警羣
不做熔斷處理
lockReport
觸發條件:http請求超時超過了(UrlRule.$timeoutLimit)秒 的次數(UrlRule.$timeoutInterval)在一定期間內(UrlRule.$timeoutInterval)
監控目的:因爲接口大幅度的超時會影響自己業務的穩定性,需要暫時屏蔽接口 讓我們業務保持穩定 一般都是第三方服務出現壓力 超時導致
推薦實現:****接口在***事件那超時超過***秒達到****次數 發送到微信報警羣
熔斷根據UrlRule.$isNeedLock判斷 熔斷與監控不衝突 可以不熔斷 但是能觸發監控
日誌LoggerInterface
interface LoggerInterface { public function info(Result $result); public function error(RequestException $exception); } |
info
觸發條件:每次http請求結束後
日誌目的: 保存每條http的日誌 扔到elk上
參數解析:Result.Request := Guzzle.RequestIntefece ,Result.Response := Guzzle.ResponseInterface ,請求間隔 :=Resule.endTime - Result.startTime
推薦實現:{"request":{"method":"","params","","url":""},"response":{"code":"","return":""},"excute_time":""}
強烈推薦後者統一規範
Error
觸發條件:http請求出發了Guzzle RequestException 異常
日誌目的:保存每條異常的日誌 可以 elk分析 or 分析當時的上下文 進行數據修復
參數解析:Guzzle RequestException
推薦實現:{"request":"*****","exception":{"message":"","file":"","line:""}}
緩存約束CacheInterface
``` interface CacheInterface { public function get($key); public function set($key, $value, $ttl = 0); public function incr($key, $step = 1); public function del($key); } ``` |
這段代碼用各自項目的緩存驅動去實現對應內容 可以各個框架
url監控配置
``` class UrlRule { //對應的url 全路徑 protected $uri = ''; //是否需要熔斷 protected $isNeedLock = false; //超時限制 超過該值代表 錯誤請求 protected $timeoutLimit = 10; //規定時間內超時的次數 protected $timeoutErrorLimit = 2; //規定時間那超過超時的次數 protected $timeoutInterval = 60; //規定時間的錯誤次數限制 protected $errorLimit = 2; //錯誤時間間隔 60s protected $errorInterval = 60; //鎖住接口時間 洪呂石強烈推薦 不要超過20s protected $lockTime = 5; // 響應返回錯誤嗎白名單列表 如果response > 300 但是在白名單那 認爲接口沒有出錯 protected $whiteResponseCodeList = [ ]; } ``` |
如何配置每個url的規則?
``` //如果不修改走父類默認屬性 class QueryMapUrl extend UrlRule { //對應的url 全路徑 protected $uri = 'https://map.baidu.com/query'; //是否需要熔斷 protected $isNeedLock = false; // 響應返回錯誤嗎白名單列表 如果response > 300 但是在白名單那 認爲接口沒有出錯 protected $whiteResponseCodeList = [ 404, 405, ]; } Container::registerUrl(new QueryMapUrl()); ``` |
異常
LockException (接口熔斷異常)
``` class LockException extends \Exception { private $url; /** * @return mixed */ public function getUrl() { return $this->url; } public function __construct($url) { parent::__construct("{$url}接口被鎖定,目前無法訪問", 9990); } } ``` |
RequestException (Guzzle 請求異常)
1.dns解析失敗
2.超時異常 超過 config.timeout
3.網絡包異常
.....具體參照https://guzzle-cn.readthedocs.io/zh_CN/latest/quickstart.html#id13
事件說明
時間依賴event-dispatch設計
事件列表
HttpExceptionEvent - http請求異常事件
HttpLockEvent - http接口鎖住事件
HttpResponseEvent - http接口結束事件
監聽列表
``` public static function getSubscribedEvents() { return [ HttpResponseEvent::class => [ //http結束日誌處理 ["httpResponseLog", 3], //http結束超時處理 ["httpResponseTimeout", 2], //http結束失敗處理 ["httpResponseError", 1], ], HttpExceptionEvent::class => [ //http異常處理 ["httpException", 1] ], HttpLockEvent::class => [ //http鎖住處理 ["httpLock", 1] ] ]; } ``` |
事件管理
有人會問:泥腿子你寫的compose代碼太垃圾 我不想用你的事件代碼 我可以自己複寫嗎?
當然可以的 還是可以非入侵複寫
增加事件
``` //http 異常後需要再 通知下平臺組 //1閉包傳入 Container::enableEvent(); Events::addListener(HttpExceptionEvent::class,function (HttpExceptionEvent $httpExceptionEvent) { echo "debug"; }); //2函數傳入 Container::enableEvent(); Events::addListener(HttpExceptionEvent::class,"honglvshi"); function honglvshi() { echo "none bug appear my life"; } $priority 爲第三個參數 叫做權重 權重越高 越優先執行 根據自己業務需要 ``` |
刪除事件
``` # 如果你不用http每次請求後都要寫日誌 你可以去掉這個事件 Container::enableEvent(); Events::removeListener(\Hgg\HttpManager\Events\HttpResponseEvent::class,"httpResponseLog");``` |
如何引入該包
初始化
``` <?php //推薦在框架bootstrap的時候 初始化框架 //開啓事件 \Hgg\HttpManager\Container::enableEvent();//開啓監控 \Hgg\HttpManager\Container::setMoint(new ***);//開啓日誌 \Hgg\HttpManager\Container::setLogger(new ***);//開啓緩存 \Hgg\HttpManager\Container::setCache(new ***); //註冊url \Hgg\HttpManager\Container::registerUrl(new ****);\Hgg\HttpManager\Container::registerUrl(new ****);\Hgg\HttpManager\Container::registerUrl(new ****);\Hgg\HttpManager\Container::registerUrl(new ****); ``` |
http調用
你可以用到guzzle所有的特性 我並沒有去更改guzzle的功能 完全依賴
``` $url = "http://****.hls/json.php"; $client = new \Hgg\HttpManager\Http(); //get $ret = $client->get($url, ['query' => ['name' => 'hls']]); ``` |
最後附上成果圖