5.微服務-熔斷降級組件

熔斷組件封裝

熔斷技術可以說是一種“智能化的容錯”,當調用滿足失敗次數,失敗比例就會觸發熔斷器打開,有程序自動切斷當前的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']];
    }

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