Swoft 2.x 微服务(Consul、RPC 服务发现、限流与熔断器)

1. Swoft 服务注册与发现;

1.1 Consul 概况;

服务的发现与注册

  • 配置共享:调用端里面有数据库或者 Redis 的链接,单机开发的时候写在配置文件里,但是多台服务器或者通过 docker 部署,配置一旦发生改变(服务器没了,地址改了),这些改变无法让调用端获取到。这时候需要一个统一的配置中心,配置发生改变的时候要有一些即时的机制让调用端不需要大量的修改配置

下载安装 Consul

  • 下载地址:https://www.consul.io/downloads.html,解压出来直接是一个可执行文件
  • 下载 1.5.3 版本,最新的 1.7.0 版本有 bug
  • 拷贝到 /usr/local/consul,执行 ./consul -v 出现版本号,即可正常使用

启动 Consul

# 参数配置
# -data-dir 数据目录
# -bind  指定机器
# -server 以服务器模式进行启动
# -bootstrap 指定自己为 leader,而不需要选举
# -ui 启动一个内置管理 web 界面(浏览器访问:http://192.168.60.221:8500)
# -client 指定客户端可以访问的 IP。设置为 0.0.0.0 则任意访问,否则默认本机可以访问
./consul agent -data-dir=/home/hua/consul -bind=192.168.60.221 -server -bootstrap -client 0.0.0.0 -ui -client=0.0.0.0

基本操作

# 1. 服务端模式:负责保存信息、集群控制、与客户端通信、与其它数据中心通信
## 新开一个终端,查看当前多少个节点
./consul members
# 返回
Node    Address              Status  Type    Build  Protocol  DC   Segment
hua-PC  192.168.60.221:8301  alive   server  1.5.3  2         dc1  <all>

## 1.1 通过 API 的方式来调用并查看:https://www.consul.io/api/index.html
# 查看节点:
curl http://192.168.60.221:8500/v1/agent/members

## 1.2 注册服务
# 服务注册好之后,是会通过一定的方式保存到服务端
# 有服务注册,就会检查(比如服务挂了,就会发出警告)
# 列出当前所有注册好的服务(目前为空):https://www.consul.io/api/agent/service.html
curl http://192.168.60.221:8500/v1/agent/services
# 1.2.1 注册一个服务
# 参考 1:https://www.consul.io/api/agent/service.html#register-service
# 参考 2 格式:https://www.consul.io/api/agent/service.html#sample-payload
curl http://192.168.60.221:8500/v1/agent/service/register \
--request PUT \
--data '{"ID":"testservice","Name":"testservice","Tags":["test"],"Address":"192.168.60.221","Port":18306,"Check":{"HTTP":"http://192.168.60.221:18306/consul/health","Interval":"5s"}}'

# 1.2.2 反注册
curl http://192.168.60.221:8500/v1/agent/service/deregister/testservice \
--request PUT

## 1.3 检查服务状态是否健康:https://www.consul.io/api/health.html
curl http://192.168.60.221:8500/v1/health/checks/testservice

# 2. 客户端模式:无状态,将请求转发服务器或者集群,此时服务器或集群并不保存任何内容

# 3. 基于 Agent 守护进程

1.2 在 Consul 注册服务、反注册;

1.2.1 注册服务;

# 添加如下代码
'consul'            => [
        'host'      => '192.168.60.221',
        'port'      => '8500'
    ],
  • 新建 Swoft\App\consul\RegService.php
<?php

namespace app\consul;

use Swoft\Bean\Annotation\Mapping\Inject;
use Swoft\Consul\Agent;
use Swoft\Event\Annotation\Mapping\Listener;
use Swoft\Event\EventHandlerInterface;
use Swoft\Event\EventInterface;
use Swoft\Log\Helper\CLog;
use Swoft\Server\SwooleEvent;

/**
 * @Listener(event=SwooleEvent::START)
 * Swoole 服务启动的的时候执行 handle(
 */
class RegService implements EventHandlerInterface {

    /**
     * @Inject()
     * 注入 agent 操作 consul
     * @var Agent
     */
    private $agent;

    /**
     * @param EventInterface $event
     */
    public function handle(EventInterface $event): void
    {
        $service = [
            'ID'                => 'prodservice-id-1',
            'Name'              => 'prodservice',
            'Tags'              => [
                'http'
            ],
            'Address'           => '192.168.60.221',
            'Port'              => 18306,   //$httpServer->getPort(),
            'Meta'              => [
                'version' => '1.0'
            ],
            'EnableTagOverride' => false,
            'Weights'           => [
                'Passing' => 10,
                'Warning' => 1
            ],
            // 健康检查方法 1: 注册服务的时候,加入健康检查器
//            "checks" => [
//                [
//                    "name"  => "prod-check",
//                    "http"  => "http://192.168.60.221:18306/consul/health",
//                    "interval" => "10s",
//                    "timeout" => "5s"
//                ]
//            ]
        ];


        // Register
        $this->agent->registerService($service);

        // 注册第二个服务
        $service2 = $service;
        $service2["ID"] = 'prodservice-id-2';
        $this->agent->registerService($service2);

        // 注册第三个服务
        $service3 = $service;
        $service3["ID"] = 'prodservice-id-3';
        $this->agent->registerService($service3);


        // 健康检查方法 2:代码
        $this->agent->registerCheck([
                    "name"  => "prod-check1",
                    "http"  => "http://192.168.60.221:18306/consul/health",
                    "interval" => "10s",
                    "timeout" => "5s",
                    "serviceid" => "prodservice-id-1"
                ]);

        $this->agent->registerCheck([
            "name"  => "prod-check2",
            "http"  => "http://192.168.60.221:18306/consul/health2",
            "interval" => "10s",
            "timeout" => "5s",
            "serviceid" => "prodservice-id-2"
        ]);

        $this->agent->registerCheck([
            "name"  => "prod-check3",
            "http"  => "http://192.168.60.221:18306/consul/health3",
            "interval" => "10s",
            "timeout" => "5s",
            "serviceid" => "prodservice-id-3"
        ]);

        CLog::info('Swoft http register service success by consul!');
    }
}

在这里插入图片描述

1.2.2 反注册;

<?php

namespace app\consul;

use Swoft\Bean\Annotation\Mapping\Inject;
use Swoft\Consul\Agent;
use Swoft\Event\Annotation\Mapping\Listener;
use Swoft\Event\EventHandlerInterface;
use Swoft\Event\EventInterface;
use Swoft\Server\SwooleEvent;

/**
 * Class DeregisterServiceListener
 *
 * @since 2.0
 *
 * @Listener(SwooleEvent::SHUTDOWN)
 */
class UnregService implements EventHandlerInterface {

    /**
     * @Inject()
     *
     * @var Agent
     */
    private $agent;

    /**
     * @param EventInterface $event
     */
    public function handle(EventInterface $event): void
    {
        $this->agent->deregisterService('prodservice-id-1');
        $this->agent->deregisterService('prodservice-id-2');
        $this->agent->deregisterService('prodservice-id-3');
    }
}
  • 控制台 Ctrl + C 退出,完成反注册

1.3 健康检查;

  • 新建 Swoft\App\Http\Controller\Consul.php
<?php

namespace App\Http\Controller;

use Swoft\Http\Server\Annotation\Mapping\Controller;
use Swoft\Http\Server\Annotation\Mapping\RequestMapping;
use Swoft\Http\Server\Annotation\Mapping\RequestMethod;

/**
 * Class Consul
 * @Controller(prefix="/consul")
 */
class Consul{

    /**
     * @RequestMapping(route="health",method={RequestMethod::GET})
     */
    public function health(){
        return ["status" => "ok"];
    }

    /**
     * @RequestMapping(route="health2",method={RequestMethod::GET})
     */
    public function health2(){
        return ["status" => "ok"];
    }

}

1.4 服务发现;

<?php

namespace App\Http\Controller;

use App\consul\ServiceClient;
use App\consul\ServiceHelper;
use App\consul\ServiceSelector;
use Swoft\Bean\Annotation\Mapping\Inject;
use Swoft\Consul\Agent;
use Swoft\Consul\Health;
use Swoft\Http\Message\Request;
use Swoft\Http\Server\Annotation\Mapping\Controller;
use Swoft\Http\Server\Annotation\Mapping\RequestMapping;
use Swoft\Http\Server\Annotation\Mapping\RequestMethod;

/**
 * Class MyClient
 * @Controller(prefix="/client")
 */
class MyClient{

    /**
     * @Inject()
     *
     * @var Agent
     */
    private $agent;

    /**
     * @Inject()
     *
     * @var Health
     */
    private $health;

    /**
     * @Inject()
     *
     * @var ServiceHelper
     */
    private $serviceHelper;

    /**
     * @Inject()
     *
     * @var ServiceSelector
     */
    private $selector;

    /**
     * @Inject()
     *
     * @var ServiceClient
     */
    private $serviceClient;

    /**
     * @RequestMapping(route="services",method={RequestMethod::GET})
     * 获取当前服务
     * 访问:http://192.168.60.221:18306/client/services
     * 等同于:http://192.168.60.221:8500/v1/health/checks/prodservice
     */
    public function getService(){
        // $service = $this->agent->services();
        // return $service->getResult();
        // 返回随机服务
//        return  $this->selector->selectByRandom(
//            // 获取正常服务列表
//            $this->serviceHelper->getService("prodservice")
//        );
        // ip_hash 算法获取服务
        // return $this->selector->selectByIPHash(ip(), $this->serviceHelper->getService("prodservice"));

        // 轮询获取
        return $this->selector->selectByRoundRobin(
            $this->serviceHelper->getService("prodservice")
        );

    }

    /**
     * @RequestMapping(route="health",method={RequestMethod::GET})
     * 健康检查:https://www.consul.io/api/health.html#list-checks-for-service
     * 访问:http://192.168.60.221:18306/client/health
     * 等同于:http://192.168.60.221:8500/v1/health/checks/prodservice?filter=Status==passing
     */
    public function getHealth(){
        // checks() 方法需要修改 Swoft\vendor\swoft\consul\src\Health.php
        // 'query' => OptionsResolver::resolve($options, ['dc', "filter"]),
        $service = $this->health->checks("prodservice", ["filter" => "Status==passing" ]);    // 服务名
        return $service->getResult();
    }

    /**
     * @RequestMapping(route="call",method={RequestMethod::GET})
     * 1.6 调用封装后的方法,获取服务
     */
    public function call(Request $request){
        return $this->serviceClient->call("prodservice", "/prod/list");

    }

}
  • 封装以上代码,新建: Swoft\App\consul\ServiceHelper.php
<?php

namespace App\consul;


use Swoft\Bean\Annotation\Mapping\Bean;
use Swoft\Bean\Annotation\Mapping\Inject;
use Swoft\Consul\Agent;
use Swoft\Consul\Health;

/**
 * Class ServiceHelper
 * @Bean()
 */
class ServiceHelper{

    /**
     * @Inject()
     *
     * @var Agent
     */
    private $agent;

    /**
     * @Inject()
     *
     * @var Health
     */
    private $health;

    /**
     * @param string $serviceName
     * @return array
     * 根据服务名 获取健康服务列表
     */
    public function getService(string $serviceName) : array {
        $service = $this->agent->services()->getResult();
        $checks = $this->health->checks($serviceName, ["filter" => "Status==passing"])->getResult();

        $passingNode = [];  // [0=>'s1', 1=>'s2', 2=>'s3']
        foreach ($checks as $check){
            $passingNode[] = $check['ServiceID'];
        }

        if(count($passingNode) == 0) return [];

        return array_intersect_key($service, array_flip($passingNode));

    }

}

1.5 算法获取服务;

  • 新建:Swoft\App\consul\ServiceSelector.php
<?php

namespace App\consul;

use Swoft\Bean\Annotation\Mapping\Bean;

/**
 * Class ServiceSelector
 * @package App\consul
 * @Bean()
 * 如果不用 @Bean() 注入,可以把方法都写成 static 方法
 */
class ServiceSelector{

    private $nodeIndex = 0;

    /**
     * @param array $serviceList
     *  1.5.1 随机获取一个服务
     */
    public function selectByRandom(array $serviceList){
        $getIndex = array_rand($serviceList);   // ['prod-1' => 'xxx']
        return $serviceList[$getIndex];
    }

    /**
     * @param string $ip
     * @param array $serviceList
     * @return mixed
     *  1.5.2 ip_hash 获取一个服务
     */
    public function selectByIPHash(string $ip, array $serviceList){
        $getIndex = crc32($ip)%count($serviceList);
        $getKey = array_keys($serviceList)[$getIndex];
        return $serviceList[$getKey];
    }

    /**
     * @param array $serviceList
     *  1.5.3 轮询算法 获取一个服务
     */
    public function selectByRoundRobin(array $serviceList){
//        if($this->nodeIndex >= count($serviceList)){
//            $this->nodeIndex = 0;
//        }
//
//        $getKey = array_keys($serviceList)[$this->nodeIndex];
//        $this->nodeIndex++;

        $getKey = array_keys($serviceList)[$this->nodeIndex];
        $this->nodeIndex = ($this->nodeIndex + 1) % count($serviceList);
        return $serviceList[$getKey];

    }

}

1.6 封装 client 类、调用 http api;

# 安装方法
# 进入到项目目录
composer require guzzlehttp/guzzle
  • 新建:Swoft\App\consul\ServiceClient.php
<?php

namespace App\consul;

use Swoft\Bean\Annotation\Mapping\Bean;
use Swoft\Bean\Annotation\Mapping\Inject;

/**
 *
 * @Bean()
 */
class ServiceClient{

    const SELECT_RAND = 1;
    const SELCET_IPHASH = 2;
    const SELECT_ROUNDROBIN = 3;

    /**
     * @Inject()
     *
     * @var ServiceHelper
     */
    private $serviceHelper;
    /**
     * @Inject()
     *
     * @var ServiceSelector
     */
    private $selector;

    /**
     * @param string $service
     * @param int $selectType
     * @return mixed
     * 以某种算法获取服务
     */
    private function loadService(string $service, int $selectType){
        $serviceList = $this->serviceHelper->getService($service);

        switch ($selectType){
            case self::SELECT_RAND:
                return $this->selector->selectByRandom($serviceList);
            case self::SELCET_IPHASH:
                return $this->selector->selectByIPHash(ip(), $serviceList);
            default:
                return$this->selector->selectByRoundRobin($serviceList);
        }
    }

    /**
     * @param $service
     * @param $endpoint 端点,地址
     * @param string $method
     * @param int $selectType
     * @return mixed
     */
    public function call($service, $endpoint, $method = "GET", $selectType = ServiceClient::SELECT_ROUNDROBIN){
        // 从 consul 获取服务
        $getService = $this->loadService($service, $selectType);

        $client = new \GuzzleHttp\Client();
        // endpoint,好比:/prod/list  就是 path
        // 目前是 HTTP 方式
        // 如果调用的是 RPC 服务,需要修改
        $url = "http://" . $getService["Address"] . ":" . $getService["Port"] . $endpoint;
        $response = $client->request($method, $url);
        return $response->getBody();
    }

}

2. RPC 和服务发现;

2.1 RPC 服务的基本配置;

2.1.1 基本概念;

概念:

  • 中文名称是“远程过程调用”。现在做的网站里面提交表单就是一个远程调用,它的过程是在服务端执行的。PRC 是一个更广泛的概念。如果只局限在 HTTP 这个范畴,现在做的网站和 RESTful API 都是 RPC

基本原理:

  • 建立在客户端和服务端,网络是相通的,两者之间能够建立 TCP、UDP 连接等基础之上,能够传输一些内容,这就是 RPC
  • 这里面的内容需要双方进行约定。客户端连上 TCP 之后,传一个 abc,服务端立马就知道客户端要它执行 abc 这个方法。所以 abc 就是协议,协议都是约定好的(比如 JSONRPC),系统做好之后,第三方才可以根据协议接入

关于 JSONRPC

  • 文档:http://wiki.geekdream.com/Specification/json-rpc_2.0.html
  • 好比创建一个 JSON 对象,里面包含一些固定的格式
  • 用户建立好 TCP 的过程当中,发送如下数据(请求),客户端只要拼凑如下数据发送给服务端就可以了
  • {"jsonrpc": "2.0", "method": "add", "params": [1,2], "id": 1}
  • 服务端直接获取数据进行 JSON decode,分别解析参数,在进行执行,执行的过程自己决定
  • 响应结果:{"jsonrpc": "2.0", "result": 3, "id": 1}
  • 现在只要写代码完成以上数据的“接收”和“响应”,然后再把过程封装,使其更加的人性化(本地化)
  • 实际开发有各种框架支持

2.1.2 基本配置;

# 添加如下代码
'rpcServer'         => [
   'class' => ServiceServer::class,
],
  • 实例操作
# 启动
# RPC 默认监听端口 18307
# HTTP 监听 18306 端口
php ./bin/swoft rpc:start
# 返回
 SERVER INFORMATION(v2.0.8)
  **************************************************************
  * RPC      | Listen: 0.0.0.0:18307, Mode: Process, Worker: 2
  **************************************************************

RPC Server Start Success!
  • 修改 App\consul\regService.php
<?php
// 修改 1:
 $service = [
    'ID'                => 'prodservice-id-1',
     'Name'              => 'prodservice',
     'Tags'              => [
         'rpc'
     ],
     'Address'           => '192.168.60.221',
     // 端口号改成 RPC 服务的 18307
     'Port'              => 18307, 
     'Meta'              => [
         'version' => '1.0'
     ],
     'EnableTagOverride' => false,
     'Weights'           => [
         'Passing' => 10,
         'Warning' => 1
     ],
 ];
 // 修改 2:连接方式修改成 tcp
 // 健康检查方法 2:代码
 $this->agent->registerCheck([
   "name"  => "prod-check1",
    //"http"  => "http://192.168.60.221:18306/consul/health",
    "tcp" => "192.168.60.221:18307",
    "interval" => "10s",
    "timeout" => "5s",
    "serviceid" => "prodservice-id-1"
]);

2.2 创建 RPC 服务,客户端直连调用;

  • 拆分成两个项目:Swoft(HTTP)和 Swoft_rpc(RPC),Swoft 仅仅是用来调用 RPC,只是为了完成和客户端(浏览器)的响应,并不完成具体的取数据等等。RPC Server 可以部署在不同的点上,作为和前端(用户)交互的入口。这只是一种做法,不代表 Swoft(HTTP)不可以写业务逻辑
  • 参考:https://www.swoft.org/documents/v2/core-components/rpc-server/#-rpc-client
  • RPC 端新建:Swoft_rpc/App/Rpc/Lib/ProdInterface.php
  • HTTP 端新建:Swoft/App/Rpc/Lib/ProdInterface.php
<?php

namespace App\Rpc\Lib;

interface ProdInterface{

    function getProdList();
}
  • RPC 端新建实现类:Swoft_rpc/App/Rpc/Service/ProdService.php
<?php

namespace App\Rpc\Service;

use App\Rpc\Lib\ProdInterface;
use Swoft\Rpc\Server\Annotation\Mapping\Service;

/**
 * Class ProdService
 * @package App\Rpc\Service
 * @Service()
 */
class ProdService implements ProdInterface{

    function getProdList()
    {
        return [
            ["prod_id" => 101, "prod_name" => "testprod1"],
            ["prod_id" => 102, "prod_name" => "testprod2"]
        ];
    }
}
<?php

namespace App\Http\Controller;

use App\Rpc\Lib\ProdInterface;
use Swoft\Http\Server\Annotation\Mapping\Controller;
use Swoft\Http\Server\Annotation\Mapping\RequestMapping;
use Swoft\Http\Server\Annotation\Mapping\RequestMethod;
use Swoft\Rpc\Client\Annotation\Mapping\Reference;

/**
 * Class ProdController
 * @Controller(prefix="/prod")
 */
class ProdController{

    /**
     * @Reference(pool="prod.pool")
     *
     * @var ProdInterface
     */
    private $prodService;


    /**
     * @RequestMapping(route="list",method={RequestMethod::GET})
     */
    public function prod(){
        // return ["prod_list"];
        // 接口实现部分在 RPC 端,并不在 HTTP 端
        return $this->prodService->getProdList();
    }
}
  • HTTP 端新建文件:Swoft\App\rpcbean.php
<?php

use Swoft\Rpc\Client\Client as ServiceClient;
use Swoft\Rpc\Client\Pool as ServicePool;

$settings = [
    'timeout'         => 0.5,
    'connect_timeout' => 1.0,
    'write_timeout'   => 10.0,
    'read_timeout'    => 0.5,
];

return [
    'prod' => [
        'class'   => ServiceClient::class,
        // 2.3 操作时,注释掉
        // 'host'    => '192.168.60.221',
        // 'port'    => 18307,
        'setting' => $settings,
        'packet'  => bean('rpcClientPacket'),
        // 2.3 添加
        'provider' => bean(\App\Rpc\RpcProvider::class)
    ],
    'prod.pool' => [
        'class'  => ServicePool::class,
        'client' => bean('prod'),
    ]
];
  • HTTP 端修改文件:Swoft\App\bean.php
# 修改一下内容(配置合并)
$rpcbeans = require("rpcbean.php");

$beans =  [];

return array_merge($beans, $rpcbeans );

2.3 客户端通过 Consul 服务发现调用 RPC;

<?php

namespace App\Rpc;

use App\consul\ServiceHelper;
use Swoft\Bean\Annotation\Mapping\Bean;
use Swoft\Bean\Annotation\Mapping\Inject;
use Swoft\Rpc\Client\Client;
use Swoft\Rpc\Client\Contract\ProviderInterface;

/**
 * Class RpcProvider
 * @Bean()
 */
class RpcProvider implements ProviderInterface {

    /**
     * @Inject()
     *
     * @var ServiceHelper
     */
    private $serviceHelper;

    /**
     * @param Client $client
     *
     * @return array
     *
     * @example
     * [
     *     'host:port',
     *     'host:port',
     *     'host:port',
     * ]
     */
    public function getList(Client $client): array
    {
        $services = $this->serviceHelper->getService("prodservice");    // ["id" => []]
        // 参考 \vendor\swoft\rpc-client\src\Connection.php line 162

        $ret = [];
        foreach ($services as $key=>$value) {
            $ret[$key] = $value["Address"] . ":" . $value["Port"];
        }
        //print_r($ret);
        return $ret;

    }
}

另:通过服务名参数的注入方式,调用 RPC 服务

  • 修改文件:Swoft\App\rpcbean.php
<?php

use App\Rpc\RpcProvider;
use Swoft\Rpc\Client\Client as ServiceClient;
use Swoft\Rpc\Client\Pool as ServicePool;

$settings = [
    'timeout'         => 0.5,
    'connect_timeout' => 1.0,
    'write_timeout'   => 10.0,
    'read_timeout'    => 0.5,
];

return [
    'prodProvider' => [
        'class' => RpcProvider::class,
        'service_name' => 'prodservice',    // 服务名
        'serviceHelper' => bean(\App\consul\ServiceHelper::class)
    ],
    'prod' => [
        'class'   => ServiceClient::class,
        // 2.3 操作时,注释掉
        // 'host'    => '192.168.60.221',
        // 'port'    => 18307,
        'setting' => $settings,
        'packet'  => bean('rpcClientPacket'),
        // 2.3 添加
        'provider' => bean("prodProvider")
    ],
    'prod.pool' => [
        'class'  => ServicePool::class,
        'client' => bean('prod'),
    ]

];
  • 修改:Swoft\App\Rpc\RpcProvider.php
<?php

namespace App\Rpc;

use App\consul\ServiceHelper;
use Swoft\Bean\Annotation\Mapping\Bean;
use Swoft\Bean\Annotation\Mapping\Inject;
use Swoft\Rpc\Client\Client;
use Swoft\Rpc\Client\Contract\ProviderInterface;

/**
 * Class RpcProvider
 * @Bean()
 */
class RpcProvider implements ProviderInterface {

    protected $service_name;    // 服务名

    /**
     *
     * @var ServiceHelper
     */
    protected $serviceHelper;

    /**
     * @param Client $client
     *
     * @return array
     *
     * @example
     * [
     *     'host:port',
     *     'host:port',
     *     'host:port',
     * ]
     */
    public function getList(Client $client): array
    {
        $services = $this->serviceHelper->getService($this->service_name);    // ["id" => []]

        $ret = [];
        foreach ($services as $key=>$value) {
            $ret[$key] = $value["Address"] . ":" . $value["Port"];
        }
        return $ret;

    }
}

2.4 限流功能的使用、令牌桶;

限流功能

  • 官方文档:https://www.swoft.org/documents/v2/microservice/limit/
  • Swoft 在这部分做的比较人性化,基本上不需要了解算法,就可以直接来配置
  • 目前结构如下
    在这里插入图片描述
  • 限流功能不一定全部在程序里面实现,因为有时候有很多程序,在每一个程序里配置限流会很麻烦。部分限流功能可以使用网关来搞定(但是网关的限流的可控性没有程序强)。
  • 如果项目只使用到反向代理,没有使用到网关,很可能就没有限流功能,或者觉得限流功能不满意,那在程序里面就需要加入限流。限流在实际开发中是一定要加入的,不管是使用单机还是微服务
  • 限流的主要算法是令牌桶算法:https://www.swoft.org/documents/v2/microservice/limit/#heading3
  • 上图,用户和服务端交互的过程时候使用浏览器和 HTTP API,不会直接和 RPC 进行交互,所以限流功能可以放在网关,也可以放在程序端 HTTP API(Nginx 仅仅为反向代理),RPC 可放可不放(因为 HTTP 限制住了,RPC 也限制住了)
  • 但是有的时候 RPC 对应的 HTTP API 是没有的,是通过一定的第三方库来生成出来的 HTTP API 反向代理。假设做项目全部写在 RPC,让用户去访问就得再开发一个 HTTP API,这个比较麻烦,所以第三方库就有一个专门的网关,来自动生成 HTTP API 的访问规则,这时候就要在 RPC 这块也写上限流。实际开发时候大多数都在网关设置,也不会在 RPC 里设置
  • 以下在 HTTP API 写限流:只要在所需要的方法里面。因为实际开发,有些业务是不限流的(有些 URL 获取静态信息)。有些秒杀抢购商品,不限流,服务器就崩了。
  • HTTP 端部分修改 Swoft/App/Http/Controller/ProdController.php
<?php
// 导入
use Swoft\Limiter\Annotation\Mapping\RateLimiter;

// 修改方法
/**
 * @RequestMapping(route="list",method={RequestMethod::GET})
 * @RateLimiter(key="request.getUriPath()", rate=1, max=5)
 */
public function prod(Request $request){
    // 接口实现部分在 RPC 端,并不在 HTTP 端

    // 假设这里有多个 URL(路由),RateLimiter 里都配置的限流
    // 那这里会有一个 key 的区分,key 必须是一致的
    // 官方 Swoft 的限流,它不是纯内存写的(在很多第三方库, JAVA,GO 里,有纯内存代码完成限流算法)
    // 也有一些使用外部第三方(redis)来完成令牌的输出
    // 这里需要配置 Redis,因为是使用 Redis 来完成 RateLimiter 的

    // name:限流器名称,默认 swoft:limiter
    // rate:允许多大的请求访问,请求数:秒
    // max:最大的请求数
    return $this->prodService->getProdList();
}

// 由于没有加降级方法和异常处理,1 秒内快速刷新 5 次,会报以下错误
{
	message: "Rate(App\Http\Controller\ProdController->prod) to Limit!"
}
  • 关于令牌桶的过程:桶就好比一个数组,令牌就是元素(数字),每隔一秒把一个令牌放进桶,放到满为止(@RateLimiter 设置的 max 值),如果外面还有令牌,也不能放了,就在外面等着
  • 请求之前会做一个拦截,首先去桶里面看,如果有令牌,就取出来一个,然后给与访问。如果没有令牌,就不能访问,等待下一个令牌放进去,才能访问。快速刷新页面 5 次,会把桶里的令牌全部的消耗掉,桶就空了,不给访问了(代表限流了)。每个一秒还会往桶里放一个,因为设置了 mate 为 1(每秒访问一次)。所以等一段时间就又能访问了
  • Swoft 是使用异步的方式去完成的,可以在更新令牌的时候效率更高一些。传统的方式(定时任务,定时协程来持续生成令牌)生成令牌的时候开销有些大
  • 这种开销也是在一定场景内的,比如说要对每个用户做访问限制,根据用户的 IP 或者用户名做访问限制,不同的用户访问的速率不一样,比如会员制,付费了接口可以一秒调用几次,没有付费接口几秒调用一次,在这种场景下,使用延迟计算,效率会更高一些

模拟非付费用户限制接口请求

  • 实际请求时 URL 为 /prod/list?uid=666,付费用户限流放宽,而免费用户则一秒只能一次
  • 以上需求,如果在 controller 里直接加 @RateLimiter 就不方便了
  • 参考:https://www.swoft.org/documents/v2/microservice/limit/#heading9
  • HTTP 端完整修改 Swoft/App/Http/Controller/ProdController.php
<?php

namespace App\Http\Controller;

use App\Http\Lib\ProdLib;
use Swoft\Bean\Annotation\Mapping\Inject;
use Swoft\Http\Message\Request;
use Swoft\Http\Server\Annotation\Mapping\Controller;
use Swoft\Http\Server\Annotation\Mapping\RequestMapping;
use Swoft\Http\Server\Annotation\Mapping\RequestMethod;

/**
 * Class ProdController
 * @Controller(prefix="/prod")
 */
class ProdController{

    /**
     * @Inject()
     * @var ProdLib
     */
    protected $prodLib;

    /**
     * @RequestMapping(route="list",method={RequestMethod::GET})
     * RateLimiter(key="request.getUriPath()", rate=1, max=5)
     */
    public function prod(Request $request){
        // 接口实现部分在 RPC 端,并不在 HTTP 端

        // 2.4.1 限流功能
        // 假设这里有多个 URL(路由),RateLimiter 里都配置的限流
        // 那这里会有一个 key 的区分,key 必须是一致的
        // 官方 Swoft 的限流,它不是纯内存写的(在很多第三方库, JAVA,GO 里,有纯内存代码完成限流算法)
        // 也有一些使用外部第三方(redis)来完成令牌的输出
        // 这里需要配置 Redis,因为是使用 Redis 来完成 RateLimiter 的

        // name:限流器名称,默认 swoft:limiter
        // rate:允许多大的请求访问,请求数:秒
        // max:最大的请求数

        // 2.4.2 模拟非付费用户限制接口请求
        // 以上需求,如果在 controller 里直接加 @RateLimiter 就不方便了
        // 需要传递 uid 参数,这样 @RateLimiter 的 key 就很难写出表达式
        // 官方的表达式可以在源码 \vendor\swoft\limiter\src\RateLimiter.php
        // 根据 https://www.swoft.org/documents/v2/microservice/limit/#heading8
        // key 表达式内置 CLASS(类名) 和 METHOD(方法名称) 两个变量,方便开发者使用
        // 也就是说在写 @RateLimiter 的 key 写表达式的时候,可以直接写 CLASS,取的就是类名
        // 第二点,源码里面通过反射取出当前方法里面的 params
        // 在 prod() 注入一个 Request 参数,但是也不能随便注入,否则会报一个空错误
        // 除了控制器方法还可以写在普通方法,可以限制任何 bean 里面的方法,实现方法限速
        // 效果和在控制器里面写一样,而且更加灵活

        $uid = $request->get("uid", 0);
        if($uid <= 0)
            throw new \Exception("error token");

        return $this->prodLib->getProds($uid);

        // 写入 ProdLib.php
        //return $this->prodService->getProdList();
    }

}

<?php

namespace App\Http\Lib;

use App\Rpc\Lib\ProdInterface;
use Swoft\Bean\Annotation\Mapping\Bean;
use Swoft\Limiter\Annotation\Mapping\RateLimiter;
use Swoft\Rpc\Client\Annotation\Mapping\Reference;

/**
 * Class ProdLib
 * @package App\Http\Lib
 * @Bean()
 */
class ProdLib {

    /**
     * @Reference(pool="prod.pool")
     *
     * @var ProdInterface
     */
    private $prodService;


    public function getProds(int $uid){
        if($uid > 10) {
            // 假设满足此条件为 vip 客户
            return $this->getProdsByVIP();
        } else {
            // 普通用户
            return $this->getProdsByNormal();

        }

    }


    public function getProdsByVIP() {
        // VIP 用户不限流
        return $this->prodService->getProdList();
    }


    /**
     * RateLimiter(key="'ProdLib'~'getProdsByNormal'~'Normal'")
     * @RateLimiter(key="CLASS~METHOD~'Normal'", rate=1, max=5)
     * // 语法参照:http://www.symfonychina.com/doc/current/components/expression_language/syntax.html#catalog11
     */
    public function getProdsByNormal() {
        return $this->prodService->getProdList();
    }

}

2.5 熔断器基本使用;

  • 在熔断这一块,外部也有一些第三方的库,包括一些开源的网关,可以直接在网关里面设置,程序里也可以不设置。但有的时候控制的颗粒度需要细一些,就需要在程序里面控制。之前的服务限流也是同样的道理
  • 现在的结构是 HTTP API 来访问 RPC API,把业务全部现在 RPC 里面。那么 HTTP API 专门给用户通过 AJAX 等等方式来进行请求的,完成用户的响应以及基本逻辑。常规的做法都是以前的单机做法,直接在 HTTP API 里面把代码都写在 Controller 里面。现在进入微服务年代,以及一些逼格要求,会把业务封装到 RPC 里面,那这时候的结构就相对复杂一些,一旦复杂,就会有一些问题需要解决,比如熔断,比如限流
  • 熔断,就是 HTTP API 调用 RPC API 的时候,出现了异常或者超时,那这时候是否要直接报错(当然报错也是一种方式)?还有一种方式是进行服务降级,就是在本地(HTTP API)部分去写死或者写一个业务逻辑很简单、很少会出错的一个方法,返回一个默认的数据,这就是最简单的一个服务降级。当用户(浏览器)去访问 HTTP API 的时候,如果反复报错,那用户体验就不是很美丽,所以要在 HTTP API 部分做服务降级,返回一个其它内容(比如推荐商品)
  • 降级在做微服务这一块是必须得有的,尤其是有多个服务,服务和服务之间进行调用
  • 尽量写在业务类里面,Controller 类里面尽量写的纯净一些,限流、熔断写在业务类里面,Controller 最终调用的是业务类方法
    在这里插入图片描述

2.5.1 Break 注解、降级函数的使用;

  • 设置一个超时时间,如果 RPC 调用多少秒没响应,则进行服务降级(不要让它直接报错)
  • HTTP 端完整修改 Swoft/App/Http/Controller/ProdController.php
<?php

namespace App\Http\Controller;

use App\Http\Lib\ProdLib;
use Swoft\Bean\Annotation\Mapping\Inject;
use Swoft\Http\Message\Request;
use Swoft\Http\Server\Annotation\Mapping\Controller;
use Swoft\Http\Server\Annotation\Mapping\RequestMapping;
use Swoft\Http\Server\Annotation\Mapping\RequestMethod;

/**
 * Class ProdController
 * @Controller(prefix="/prod")
 */
class ProdController{

    /**
     * @Inject()
     * @var ProdLib
     */
    protected $prodLib;


    /**
     * @RequestMapping(route="list",method={RequestMethod::GET})
     * Breaker 必须打在有 Bean 的类里面(Controller 就是一个 Bean)
     * Breaker 也没必要一定要写在 Controller 里面,一般都是写在业务类里面
     * Controller 类搞的纯净一些,清晰一些,避免写一些稀奇古怪的方法
     * 就像之前的 RateLimiter() 写在 ProdLib.php 的最终函数里
     * Breaker(timeout=2.0,fallback="defaultProds")
     */
    public function prod(Request $request){
        $uid = $request->get("uid", 0);
        if($uid <= 0)
            throw new \Exception("error token");

        return $this->prodLib->getProds($uid);
    }


    public function defaultProds() {
        return [
            'prod_id' => 900, 'prod_name' => '降级内容'
        ];
    }

}
  • 修改 HTTP Swoft/App/Http/Lib/ProdLib.php
<?php

namespace App\Http\Lib;

use App\Rpc\Lib\ProdInterface;
use Swoft\Bean\Annotation\Mapping\Bean;
use Swoft\Breaker\Annotation\Mapping\Breaker;
use Swoft\Limiter\Annotation\Mapping\RateLimiter;
use Swoft\Rpc\Client\Annotation\Mapping\Reference;

/**
 * Class ProdLib
 * @package App\Http\Lib
 * @Bean()
 */
class ProdLib {

    /**
     * @Reference(pool="prod.pool")
     *
     * @var ProdInterface
     */
    private $prodService;

    /**
     * @param int $uid
     * @return mixed
     * @Breaker(timeout=2.0,fallback="defaultProds")
     */
    public function getProds(int $uid){
        if($uid > 10) {
            // 假设满足此条件为 vip 客户
            return $this->getProdsByVIP();
        } else {
            // 普通用户
            return $this->getProdsByNormal();

        }

    }


    public function getProdsByVIP() {
        // VIP 用户不限流
        return $this->prodService->getProdList();
    }


    /**
     * RateLimiter(key="'ProdLib'~'getProdsByNormal'~'Normal'")
     * @RateLimiter(key="CLASS~METHOD~'Normal'", rate=1, max=5)
     * // 语法参照:http://www.symfonychina.com/doc/current/components/expression_language/syntax.html#catalog11
     */
    public function getProdsByNormal() {
        return $this->prodService->getProdList();
    }


    public function defaultProds() {
        return [
            'prod_id' => 900, 'prod_name' => '降级内容'
        ];
    }

}
  • 修改 Swoft_rpc/app/Rpc/Service/ProdService.php
<?php

namespace App\Rpc\Service;

use App\Rpc\Lib\ProdInterface;
use Swoft\Rpc\Server\Annotation\Mapping\Service;

/**
 * Class ProdService
 * @package App\Rpc\Service
 * @Service()
 */
class ProdService implements ProdInterface{

    function getProdList()
    {   
        // 延时 3 秒,用来测试降级
        sleep(3);
        return [
            ["prod_id" => 101, "prod_name" => "testprod101"],
            ["prod_id" => 102, "prod_name" => "testprod102"]
        ];
    }
}

2.5.2 Break 熔断器参数设置、状态。

  • 熔断器已经在使用,只不过参数没有设置到想要的内容,所以看不出熔断器是否打开或者关闭
  • 在官方文档有清晰的解释,到底有哪些状态,在默认状态下,熔断器是关闭的
  • 之前请求,如果超时或者发生异常,会执行降级方法。这很可能每次请求或者点击都会产生等待。一旦发生超时,很多时候是由于网络原因、或者网络不稳定造成的,这时候降级一次没有关系。如果确实是后台服务完全挂掉了,那现在每次去请求还是要等待,会造成访问的性能比较慢和卡
  • 所以熔断器起到这样的作用,一旦失败超时达到一定的次数就打开,一旦打开之后,有一个很大的区别在于,它不会去执行真实的调用方法,而直接去执行降级方法,这是一个基本的概念
  • 如果没有机会让熔断器关闭(后台服务恢复但是仍然调用降级方法),这也是不对的。需要去设置一定的参数,让熔断器去关掉。去设置熔断器状态为半开,就是有一定的几率去请求真实服务的。一旦服务回恢复了,就会把熔断器关闭,去请求真实服务。如果半开状态去请求服务,发现还是不行,就又把熔断器打开了,再次去等待一定时间
<?php
// 参考:https://www.swoft.org/documents/v2/microservice/blown-downgraded/#breaker
// 查看 vendor\swoft\breaker\src\Annotation\Mapping\Breaker
// private $failThreshold = 3; 连续失败多少次后打开熔断器
// private $sucThreshold = 3; 连续成功多少次去切换熔断器状态
// private $retryTime = 3; 从开启到半开尝试切换时间
// 开启状态一直请求的是降级方法,一旦进入半开,就会尝试切换请求真实的服务,一旦成功,把熔断器关掉
// 关掉,才是访问真实的服务

// 修改 Swoft/App/Http/Lib/ProdLib.php
@Breaker(timeout=2.0, fallback="defaultProds", failThreshold=3, sucThreshold=3, retryTime=5)
发布了125 篇原创文章 · 获赞 13 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/hualaoshuan/article/details/104413354
今日推荐