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