服务的发现与注册
Docker 安装 Consul
# Consul 镜像地址:https://hub.docker.com/_/consul
# 拉取镜像
docker pull consul
# 启动
docker run -d --name=consul -p 8500:8500 \
consul agent -server -bootstrap -ui -client 0.0.0.0
# 查看版本
docker exec -it consul consul -v
# 开启防火墙
iptables -I INPUT -p tcp --dport 8500 -j ACCEPT
下载安装 Consul
/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 守护进程
Swoft\App\bean.php
# 添加如下代码
'consul' => [
'host' => '192.168.60.221',
'port' => '8500'
],
Swoft\App\consul\RegService.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!');
}
}
参考:https://www.swoft.org/documents/v2/microservice/register-find/#heading2
新建 Swoft\App\consul\UnregService.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');
}
}
Swoft\App\Http\Controller\Consul.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"];
}
}
Swoft\App\Http\Controller\MyClient.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
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));
}
}
Swoft\App\consul\ServiceSelector.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];
}
}
# 安装方法
# 进入到项目目录
composer require guzzlehttp/guzzle
Swoft\App\consul\ServiceClient.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();
}
}
概念:
基本原理:
关于 JSONRPC
{"jsonrpc": "2.0", "method": "add", "params": [1,2], "id": 1}
{"jsonrpc": "2.0", "result": 3, "id": 1}
Swoft_rpc\App\bean.php
# 添加如下代码
'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
// 修改 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"
]);
Swoft_rpc/App/Rpc/Lib/ProdInterface.php
(这是一个接口,拷贝给客户端 / 调用端)Swoft/App/Rpc/Lib/ProdInterface.php
namespace App\Rpc\Lib;
interface ProdInterface{
function getProdList();
}
Swoft_rpc/App/Rpc/Service/ProdService.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"]
];
}
}
Swoft/App/Http/Controller/ProdController.php
ProdService.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();
}
}
Swoft\App\rpcbean.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'),
]
];
Swoft\App\bean.php
# 修改一下内容(配置合并)
$rpcbeans = require("rpcbean.php");
$beans = [];
return array_merge($beans, $rpcbeans );
Swoft\App\Common\RpcProvider.php
Swoft\App\rpcbean.php
把 host 和 port 写死,这次操作需要注释掉,加入一个 providerSwoft\App\Rpc\RpcProvider.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
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
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;
}
}
限流功能
Swoft/App/Http/Controller/ProdController.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!"
}
模拟非付费用户限制接口请求
/prod/list?uid=666
,付费用户限流放宽,而免费用户则一秒只能一次Swoft/App/Http/Controller/ProdController.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();
}
}
Swoft/App/Http/Lib/ProdLib.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();
}
}
Swoft/App/Http/Controller/ProdController.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' => '降级内容'
];
}
}
Swoft/App/Http/Lib/ProdLib.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
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"]
];
}
}
// 参考: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)