php在fpm運行模式下實現服務之間的服務熔斷、服務監控、調用日誌

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']]);
```

最後附上成果圖

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章