php在fpm运行模式下实现服务之间的服务熔断、服务监控、调用日志

952 阅读5分钟
github.com/hongg-coder…

前言

相信在场各位的泥腿子(如果大佬请跳过这段话)每天工作都是穿梭在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

guzzle-cn.readthedocs.io/zh_CN/lates…

注册两个中间件

请求开始中间件

记录请求的开始时间、请求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.网络包异常

.....具体参照guzzle-cn.readthedocs.io/zh_CN/lates…

事件说明

时间依赖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']]);
```

最后附上成果图