熔断组件封装
熔断技术可以说是一种“智能化的容错”,当调用满足失败次数,失败比例就会触发熔断器打开,有程序自动切断当前的RPC调用,来防止错误进一步扩大。实现一个熔断器主要是考虑三种模式,关闭,打开,半开。
<?php declare(strict_types=1);
namespace App\Http\Controller;
use App\Rpc\Lib\PayInterface;
use App\Rpc\Lib\UserInterface;
use Exception;
use Swoft\Http\Server\Annotation\Mapping\Controller;
use Swoft\Http\Server\Annotation\Mapping\RequestMapping;
use Six\Rpc\Client\Annotation\Mapping\Reference;
/**
* Class RpcController
*
* @since 2.0
*
* @Controller("rpc")
*/
class RpcController
{
/**
* 在程序初始化时候定义好服务降级处理类
* @Reference(pool="pay.pool",fallback="payFallback")
* @var PayInterface
*/
private $payService;
/**
* @RequestMapping("pay")
*
* @return array
*/
public function pay(): array
{
$result = $this->payService->pay();
return [$result];
}
/**
* @RequestMapping()
*
* @return array
*
* @throws Exception
*/
public function exception(): array
{
$this->userService->exception();
return ['exception'];
}
}
bean.php
'pay' => [ //客户端
'class' =>\App\Rpc\Client\Client::class,
'host' => '182.61.147.77',
'serviceName'=>'pay-php',
'port' => 9502,
'setting' => [
'timeout' => 0.5,
'connect_timeout' => 1.0,
'write_timeout' => 10.0,
'read_timeout' => 0.5,
],
'packet' => \bean('rpcClientPacket')
],
'pay.pool' => [
'class' => \Six\Rpc\Client\Pool::class,
'client' => \bean('pay'),
'minActive' => 10,
'maxActive' => 20,
'maxWait' => 0,
'maxWaitTime' => 0,
'maxIdleTime' => 40,
],
定义降级类
<?php
namespace App\Fallback;
use App\Rpc\Lib\PayInterface;
use Six\Rpc\Client\Annotation\Mapping\Fallback;
/**
* Class PayServiceFallback
* @package App\Fallback
* @Fallback(name="payFallback",version="1.0")
*/
class PayServiceFallback implements PayInterface
{
public function pay(): array
{
return ["降级处理:服务开小差了,请稍后再试"];
}
public function test(){
}
}
在组件中,此组件通过composer加载
Fallback.php
<?php declare(strict_types=1);
namespace Six\Rpc\Client\Annotation\Mapping;
use Doctrine\Common\Annotations\Annotation\Attribute;
use Doctrine\Common\Annotations\Annotation\Attributes;
use Doctrine\Common\Annotations\Annotation\Required;
use Doctrine\Common\Annotations\Annotation\Target;
use Swoft\Rpc\Protocol;
/**
*
* @Annotation
* @Target("CLASS")
* @Attributes({
* @Attribute("event", type="string"),
* })
*/
class Fallback
{
/**
* @var string
*
* @Required()
*/
private $name;
/**
* @var string
*/
private $version = Protocol::DEFAULT_VERSION;
/**
* Reference constructor.
*
* @param array $values
*/
public function __construct(array $values)
{
if (isset($values['value'])) {
$this->pool = $values['value'];
} elseif (isset($values['name'])) {
$this->name = $values['name'];
}
if (isset($values['version'])) {
$this->version = $values['version'];
}
}
/**
* @return string
*/
public function getVersion(): string
{
return $this->version;
}
/**
* @return string
*/
public function getName(): string
{
return $this->name;
}
}
Reference.php
<?php declare(strict_types=1);
namespace Six\Rpc\Client\Annotation\Mapping;
use Doctrine\Common\Annotations\Annotation\Attribute;
use Doctrine\Common\Annotations\Annotation\Attributes;
use Doctrine\Common\Annotations\Annotation\Required;
use Doctrine\Common\Annotations\Annotation\Target;
use Swoft\Rpc\Protocol;
/**
* Class Reference
*
* @since 2.0
*
* @Annotation
* @Target("PROPERTY")
* @Attributes({
* @Attribute("event", type="string"),
* })
*/
class Reference
{
/**
* @var string
*
* @Required()
*/
private $pool;
/**
* @var string
*/
private $version = Protocol::DEFAULT_VERSION;
private $fallback='';
/**
* Reference constructor.
*
* @param array $values
*/
public function __construct(array $values)
{
if (isset($values['value'])) {
$this->pool = $values['value'];
} elseif (isset($values['pool'])) {
$this->pool = $values['pool'];
}
if (isset($values['version'])) {
$this->version = $values['version'];
}
if (isset($values['fallback'])) {
$this->fallback = $values['fallback'];
}
}
/**
* @return string
*/
public function getVersion(): string
{
return $this->version;
}
/**
* @return string
*/
public function getPool(): string
{
return $this->pool;
}
/**
* @return string
*/
public function getFallback(): string
{
return $this->fallback;
}
}
FallbackParser.php
<?php declare(strict_types=1);
namespace Six\Rpc\Client\Annotation\Parser;
use PhpDocReader\AnnotationException;
use PhpDocReader\PhpDocReader;
use ReflectionException;
use ReflectionProperty;
use Six\Rpc\Client\Proxy;
use Six\Rpc\Client\ReferenceRegister;
use Six\Rpc\Client\Route;
use Swoft\Annotation\Annotation\Mapping\AnnotationParser;
use Swoft\Annotation\Annotation\Parser\Parser;
use Swoft\Bean\Annotation\Mapping\Bean;
use Swoft\Proxy\Exception\ProxyException;
use Six\Rpc\Client\Annotation\Mapping\Fallback;
use Swoft\Rpc\Client\Exception\RpcClientException;
/**
* @since 2.0
*
* @AnnotationParser(Fallback::class)
*/
class FallbackParser extends Parser
{
/**
* @param int $type
* @param Reference $annotationObject
*
* @return array
* @throws RpcClientException
* @throws AnnotationException
* @throws ReflectionException
* @throws ProxyException
*/
public function parse(int $type, $annotationObject): array
{
foreach ($this->reflectClass->getMethods() as $method ){
$method_name=$method->name;
//var_dump($method);
Route::registerRoute($annotationObject->getName(),
$annotationObject->getVersion(),$method_name,$this->className);
}
//返回当前类名注册到框架的bean容器当中
return [$this->className,$this->className,Bean::SINGLETON,''];
}
}
RefenceParser.php
<?php declare(strict_types=1);
namespace Six\Rpc\Client\Annotation\Parser;
use PhpDocReader\AnnotationException;
use PhpDocReader\PhpDocReader;
use ReflectionException;
use ReflectionProperty;
use Six\Rpc\Client\Proxy;
use Six\Rpc\Client\ReferenceRegister;
use Swoft\Annotation\Annotation\Mapping\AnnotationParser;
use Swoft\Annotation\Annotation\Parser\Parser;
use Swoft\Proxy\Exception\ProxyException;
use Six\Rpc\Client\Annotation\Mapping\Reference;
use Swoft\Rpc\Client\Exception\RpcClientException;
/**
* Class ReferenceParser
*
* @since 2.0
*
* @AnnotationParser(Reference::class)
*/
class ReferenceParser extends Parser
{
/**
* @param int $type
* @param Reference $annotationObject
*
* @return array
* @throws RpcClientException
* @throws AnnotationException
* @throws ReflectionException
* @throws ProxyException
*/
public function parse(int $type, $annotationObject): array
{
// Parse php document
$phpReader = new PhpDocReader();
$reflectProperty = new ReflectionProperty($this->className,
$this->propertyName);
$propClassType = $phpReader->getPropertyClass($reflectProperty);
if (empty($propClassType)) {
throw new RpcClientException(
sprintf('`@Reference`(%s->%s) must to define `@var xxx`',
$this->className, $this->propertyName)
);
}
$className = Proxy::newClassName($propClassType);
$this->definitions[$className] = [
'class' => $className,
];
//注册服务信息
ReferenceRegister::register($className,
$annotationObject->getPool(),
$annotationObject->getVersion(),
$annotationObject->getFallback());
return [$className, true];
}
}
AutoLoader.php
<?php declare(strict_types=1);
namespace Six\Rpc\Client;
use Swoft\Rpc\Packet;
use Swoft\SwoftComponent;
/**
* Class AutoLoader
*
* @since 2.0
*/
class AutoLoader extends SwoftComponent
{
/**
* @return array
*/
public function getPrefixDirs(): array
{
return [
__NAMESPACE__ => __DIR__,
];
}
/**
* @return array
*/
public function metadata(): array
{
return [];
}
/**
* @return array
*/
public function beans(): array
{
return [
'rpcClientPacket' => [
'class' => Packet::class
],
'circuit'=>[
'class'=>CircuitBreak::class
]
];
}
}
ServiceTrait.php
<?php declare(strict_types=1);
namespace Six\Rpc\Client\Concern;
use function Couchbase\basicEncoderV1;
use ReflectionException;
use SebastianBergmann\CodeCoverage\Report\PHP;
use Six\Rpc\Client\CircuitBreak;
use Six\Rpc\Client\Connection;
use Six\Rpc\Client\ReferenceRegister;
use Six\Rpc\Client\Route;
use Swoft\Bean\BeanFactory;
use Swoft\Bean\Exception\ContainerException;
use Swoft\Connection\Pool\Exception\ConnectionPoolException;
use Swoft\Log\Debug;
use Swoft\Redis\Redis;
use Swoft\Rpc\Client\Exception\RpcClientException;
use Swoft\Rpc\Protocol;
use Swoft\Stdlib\Helper\JsonHelper;
use Swoole\Exception;
/**
* Class ServiceTrait
*
* @since 2.0
*/
trait ServiceTrait
{
/**
* @param string $interfaceClass
* @param string $methodName
* @param array $params
*
* @return mixed
* @throws ReflectionException
* @throws ContainerException
* @throws ConnectionPoolException
* @throws RpcClientException
*/
protected function __proxyCall(string $interfaceClass,
string $methodName,
array $params)
{
$poolName = ReferenceRegister::getPool(__CLASS__);
$version = ReferenceRegister::getVersion(__CLASS__);
//获取降级类名称
$fallback = ReferenceRegister::getFallback(__CLASS__);
$fallbackName = BeanFactory::getBean(Route::match($fallback, $version,
$methodName));
$circuit = bean('circuit');
try {
/* @var Pool $pool */
$pool = BeanFactory::getBean($poolName);
/* @var Connection $connection */
$connection = $pool->getConnection();
$address = $connection->getAddress();
$connection->setRelease(true);
//获取服务状态
$state = $circuit->getState($address['host'] . ":" . $address['port']);
echo '当前状态:' . $state . PHP_EOL;
//如果熔断开启,直接降级
if ($state == CircuitBreak::StateOpen)
throw new RpcClientException("Rpc CircuitBreak:" .
$fallbackName->$methodName()[0]);
//半开状态,是允许访问后台服务的
if ($state == CircuitBreak::StateHalfOpen) {
//满足一定条件之后才允许调用
if (mt_rand(0, 100) % 2 == 0) {
$result = $this->getResult($connection, $version,
$interfaceClass, $methodName, $params, $address);
//记录成功的次数,大于设定的成功次数的值,熔断就会自动切换成关闭状态
$score = $circuit->add($address);
if ($score >= 0) Redis::zRem(CircuitBreak::FAILKEY,
$address['host'] . ":" . $address['port']);
return $result;
}
throw new RpcClientException("Rpc CircuitBreak:" .
$fallbackName->$methodName()[0]);
}
//关闭状态直接调用
return $this->getResult($connection, $version, $interfaceClass,
$methodName, $params, $address);
} catch (\Exception $e) {
//记录在redis当中
//如何区分服务,正则匹配ip+port
$message = $e->getMessage();
//用于重置连接,因为意外的bug导致错误无法得到信息,或者超时,但是连接正常建立
if(stristr($message,"Rpc call") || stristr($message,"Rpc CircuitBreak") )
{
var_dump("重置连接");
$connection->setRelease(true);
$connection->release();
}
//第一种情况是调用失败
//第二种创建连接失败
//第三种连接池里的连接意外断开了
if (stristr($message, "Rpc call") ||
stristr($message, "Create connection error") ||
stristr($message,"Connect failed host"))
{
preg_match("/host=(\d+.\d+.\d+.\d+)\sport=(\d+)/", $message, $mach);
$address = $mach[1] . ":" . $mach[2];
$state = $circuit->getState($address);
//当前状态是关闭状态
if (CircuitBreak::StateClose == $state) {
//记录当前的ip+port所对应方服务的失败次数,失败次数大于允许熔断次数则开启熔断器
$score = $circuit->add($address);
if ($score >= CircuitBreak::FAILCOUNT) {
$circuit->OpenBreaker($address);//开启熔断器,记录了延迟时间
echo "打开熔断器" . PHP_EOL;
}
throw new RpcClientException("Rpc CircuitBreak:" .
$fallbackName->$methodName()[0]."--正常熔断");
}
//当前状态是半开状态只要出现异常,就熔断
if (CircuitBreak::StateHalfOpen == $state) {
//次数重置,重置成熔断次数
$circuit->add($address, CircuitBreak::FAILCOUNT);
//重新打开熔断器
$circuit->OpenBreaker($address);//开启熔断器,记录了延迟时间
echo "半开状态重置" . PHP_EOL;
throw new RpcClientException("Rpc CircuitBreak:" .
$fallbackName->$methodName()[0]."---半开熔断");
}
//如果当前熔断是开启状态并且时连接失败的异常
if (CircuitBreak::StateOpen == $state && stristr($message,
"Create connection error")) {
throw new RpcClientException("Rpc CircuitBreak:" .
$fallbackName->$methodName()[0]."---连接熔断");
}
}
throw new Exception($e->getMessage()."正常熔断");
}
}
public function getResult($connection, $version, $interfaceClass,
$methodName, $params, $address)
{
$packet = $connection->getPacket();
// Ext data
$ext = $connection->getClient()->getExtender()->getExt();
$protocol = Protocol::new($version, $interfaceClass,
$methodName, $params, $ext);
$data = $packet->encode($protocol);
$message = sprintf('Rpc call failed. host=%s port=%d
interface=%s method=%s', $address['host'], $address['port'],
$interfaceClass, $methodName);
$result = $this->sendAndRecv($connection, $data, $message);
$connection->release(); //连接放入到连接池
$response = $packet->decodeResponse($result);
if ($response->getError() !== null) {
$code = $response->getError()->getCode();
$message = $response->getError()->getMessage();
$errorData = $response->getError()->getData();
throw new RpcClientException(
sprintf('Rpc call error! host=%s port=%d code=%d
message=%s data=%s', $address['host'], $address['port'],
$code, $message, JsonHelper::encode($errorData))
);
}
return $response->getResult();
}
/**
* @param Connection $connection
* @param string $data
* @param string $message
* @param bool $reconnect
*
* @return string
* @throws RpcClientException
* @throws ReflectionException
* @throws ContainerException
*/
private function sendAndRecv(Connection $connection, string $data,
string $message, bool $reconnect = false): string
{
//Reconnect
if ($reconnect) {
$connection->reconnect();
}
if (!$connection->send($data)) {
if ($reconnect) {
throw new RpcClientException($message);
}
//重发一次
return $this->sendAndRecv($connection, $data, $message, true);
}
$result = $connection->recv();
if ($result === false || empty($result)) {
if ($reconnect) {
throw new RpcClientException($message);
}
return $this->sendAndRecv($connection, $data, $message, true);
}
return $result;
}
}
最后熔断的异常会被这个类捕获
<?php declare(strict_types=1);
namespace App\Exception\Handler;
use const APP_DEBUG;
use function get_class;
use ReflectionException;
use function sprintf;
use Swoft\Bean\Exception\ContainerException;
use Swoft\Error\Annotation\Mapping\ExceptionHandler;
use Swoft\Http\Message\Response;
use Swoft\Http\Server\Exception\Handler\AbstractHttpErrorHandler;
use Throwable;
/**
* Class HttpExceptionHandler
*
* @ExceptionHandler(\Throwable::class)
*/
class HttpExceptionHandler extends AbstractHttpErrorHandler
{
/**
* @param Throwable $e
* @param Response $response
*
* @return Response
* @throws ReflectionException
* @throws ContainerException
*/
public function handle(Throwable $e, Response $response): Response
{
//捕获rpc的某些异常,自定义返回数据
if(stristr($e->getMessage(),"Rpc CircuitBreak:")){
return $response->withData($e->getMessage());
}
// Debug is false
if (!APP_DEBUG) {
return $response->withStatus(500)->withContent(
sprintf(' %s At %s line %d', $e->getMessage(),
$e->getFile(), $e->getLine())
);
}
$data = [
'code' => $e->getCode(),
'error' => sprintf('(%s) %s', get_class($e), $e->getMessage()),
'file' => sprintf('At %s line %d', $e->getFile(), $e->getLine()),
'trace' => $e->getTraceAsString(),
];
// Debug is true
return $response->withData($data);
}
}
在创建连接的时候,加入ip和端口
/**
* @throws RpcClientException
*/
public function create(): void
{
$connection = new \Co\Client(SWOOLE_SOCK_TCP);
[$host, $port] = $this->getHostPort();
$setting = $this->client->getSetting();
//赋值属性用于区分服务
$this->host=$host;
$this->port=$port;
if (!empty($setting)) {
$connection->set($setting);
}
if (!$connection->connect($host, (int)$port)) {
throw new RpcClientException(
sprintf('Connect failed host=%s port=%d', $host, $port)
);
}
$this->connection = $connection;
}
ServiceTrait.php中从连接池取出ip和端口
$connection = $pool->getConnection();
$address = $connection->getAddress();
记录熔断状态的类
<?php
/**
* Created by PhpStorm.
* User: Sixstar-Peter
* Date: 2019/6/15
* Time: 22:23
*/
namespace Six\Rpc\Client;
use Swoft\Bean\Annotation\Mapping\Inject;
use Swoft\Redis\Redis;
class CircuitBreak
{
const FAILKEY = 'circuit';//记录服务失败次数的key
const OpenBreaker='circuit_open';
const FAILCOUNT = 3; //允许失败的次数
const SuccessCount = 3; //成功多少次之后熔断器关闭
const StateOpen = 1;//熔断器开启的状态
const StateClose = 2;//关
const StateHalfOpen = 3;//半开
const OpenTime=5; //多久时间切换到半开状态
/**
* @Inject("redis.pool")
* @var \Swoft\Redis\Pool
*/
public $redis;
public function __construct()
{
//$this->redis=Redis::connection();
}
/**
* 记录服务失败次数
* @param $address
* @return float
*/
public function add($address,$count=null)
{
if($count!=null){
return Redis::zAdd(self::FAILKEY, [$count=>$address]);
}
return Redis::zIncrBy(self::FAILKEY, 1, $address);
}
/**
* 开启服务熔断,并且设置当前服务半开启的时间
* @param $address
* @return int
*/
public function OpenBreaker($address){
return Redis::zAdd(self::OpenBreaker,[(time()+self::OpenTime)=>$address]);
}
/**
* 获取服务状态
* @param $address
* @return float
*/
public function getState($address)
{
$score = Redis::zScore(self::FAILKEY, $address);
var_dump($score."成绩");
if ($score >= self::FAILCOUNT) return self::StateOpen; //返回开启状态
if ($score<0) return self::StateHalfOpen; //返回半开启状态
return self::StateClose; //返回的是关闭状态
}
}
定义监听器,定时找出那些熔断的服务,变为半熔断,有一定几率去尝试连接,如果还出现异常就直接熔断,如果成功就加次数,成功到达一定次数,熔断关闭
<?php
namespace Six\Rpc\Client\Listener;
use Six\Rpc\Client\CircuitBreak;
use Swoft\Event\Annotation\Mapping\Listener;
use Swoft\Event\EventHandlerInterface;
use Swoft\Event\EventInterface;
use Swoft\Redis\Redis;
use Swoft\Server\Swoole\SwooleEvent;
/**
* Class RegisterServer
* @package App\Listener
* @Listener(SwooleEvent::START)
*/
class BreakerTick implements EventHandlerInterface
{
public function handle(EventInterface $event): void
{
swoole_timer_tick(2000, function () {
//查询小于我当前时间的任务
$service = Redis::zRangeByScore(CircuitBreak::OpenBreaker,
"-inf", (string)time());
//修改
if (!empty($service)) {
foreach ($service as $s) {
//把失败次数重置成负数,改变成半开启状态
Redis::zAdd(CircuitBreak::FAILKEY,
[-CircuitBreak::SuccessCount=>$s]);
//删掉延迟时间,避免重复处理
Redis::zRem(CircuitBreak::OpenBreaker,$s);
echo '修改了'.$s."为半开启状态".PHP_EOL;
}
}
});
}
}
测试方法
<?php declare(strict_types=1);
namespace App\Rpc\Service;
use App\Rpc\Lib\PayInterface;
use App\Rpc\Lib\UserInterface;
use Swoft\Co;
use Swoft\Rpc\Server\Annotation\Mapping\Service;
/**
* Class UserService
*
* @since 2.0
*
* @Service()
*/
class PayService implements PayInterface
{
/**
* @param int $id
* @param mixed $type
* @param int $count
*
* @return array
*/
public function pay(): array
{
return ['result' => ['ok123']];
}
}