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']]); ``` |
最后附上成果图