一、概述
二、实现商品服务的注册
1、构建Consul集群
因为对于整个微服务系统来说,服务的注册与发现是至关重要的,因此为了保证服务的注册与发现的高可用,就需要搭建一个consul集群。
容器名 | 节点名 | 属性 | IP地址 | port |
---|---|---|---|---|
consul_10 | consul_10 | Server、Master | 170.200.7.10 | 8510 |
consul_20 | consul_20 | Serve | 170.200.7.20 | 8520 |
consul_30 | consul_30 | Serve | 170.200.7.30 | 8530 |
consul_40 | consul_40 | client | 170.200.7.40 | 8540 |
consul_50 | consul_50 | client | 170.200.7.50 | 8550 |
宿主机IP:192.168.78.200
搭建consul集群的步骤内容太多,另开一篇文章,详见 搭建consul集群。
2、当前商品微服务的服务架构
容器名 | 使用的镜像 | 映射端口号 | IP地址 | 映射文件夹 |
---|---|---|---|---|
swoft_110 | swoft | 18110:18306 | 172.22.22.110 | /var/lib/docker/volumes/swoft_volume/_data : /var/www/swoft |
swoft_120 | swoft | 18120:18306 | 172.22.22.120 | /var/lib/docker/volumes/swoft_volume/_data : /var/www/swoft |
swoft_130 | swoft | 18130:18306 | 172.22.22.113 | /var/lib/docker/volumes/swoft_volume/_data : /var/www/swoft |
nginx1.21 | nginx1.21 | 81:80 | 172.22.22.20 | /var/lib/docker/volumes/nginx_volume/_data : /etc/nginx |
宿主机:192.168.78.104
备注:
搭建商品微服务的服务架构详见文章 使用docker-compose 编排基础分布式架构
后面还将添加订单微服务、库存微服务、用户微服务、活动微服务等多个微服务的服务架构在其他IP主机。
当前仅创建了商品微服务架构和用于注册发现、动态负载均衡的consul集群,还只是一个雏形。
3、修改swoft的配置文件
(1)说明
当前有三台swoft服务容器,它们的文件是共享的,都在192.168.78.104主机上的var/lib/docker/volumes/swoft_volume/_data 目录下。
(2)修改配置文件 app/bean.php
(2.1)在数组中添加consul配置
'consul' => [
// consul集群中的consul_40(client_2)https://www.haveyb.com/article/2058
'host' => '192.168.78.200',
'port' => 8550
]
(2.2)修改数组中‘rpcServer’的值
'rpcServer' => [
'class' => ServiceServer::class,
'port' => 18308
],
rpc有两种方式,一种是集成在http中,另一种是单独运行。
这里改为单独运行,因为我们使用docker-compose构建时加了一个http服务自启动,因此如果还按照默认的将rpc和http集成在一起,就会出现端口冲突而报错。
4、修改swoft服务注册文件 app/Listener/RegisterServiceListener.php
修改handle方法为:
public function handle(EventInterface $event): void
{
$rpcServer = $event->getTarget();
$service = [
'ID' => 'swoft_goods_server_110',
'Name' => 'swoft_goods_server_110',
'Tags' => [
'rpc'
],
'Address' => '127.0.0.1',
'Port' => $rpcServer->getPort(),
'Meta' => [
'version' => '1.0'
],
'EnableTagOverride' => false,
'Weights' => [
'Passing' => 10,
'Warning' => 1
]
];
// Register
$this->agent->registerService($service);
echo '哈哈哈Swoft rpc register service success by consul哈哈哈!'.PHP_EOL;
}
5、开启rpc服务
[root@v4 ~]# docker exec -it swoft_110 bash
cd /var/www/swoft/
./bin/swoft rpc:start
可以看到输出:
____ _____ ____ __ ___ ___
/ __/ _____ / _/ /_ / __/______ ___ _ ___ _ _____ ____/ /__ |_ | / _ \
_\ \| |/|/ / _ \/ _/ __/ / _// __/ _ `/ ' \/ -_) |/|/ / _ \/ __/ '_/ / __/_/ // /
/___/|__,__/\___/_/ \__/ /_/ /_/ \_,_/_/_/_/\__/|__,__/\___/_/ /_/\_\ /____(_)___/
SERVER INFORMATION(v2.0.10)
**************************************************************
* RPC | Listen: 0.0.0.0:18308, Mode: Process, Worker: 2
**************************************************************
RPC Server Start Success!
2021/09/09-15:36:01 [INFO] Swoft\Server\Server:startSwoole(499) Swoole\Runtime::enableCoroutine
2021/09/09-15:36:01 [INFO] Swoft\Listener\BeforeStartListener:handle(35) Server extra info: pidFile @runtime/swoft-rpc.pid
2021/09/09-15:36:01 [INFO] Swoft\Listener\BeforeStartListener:handle(36) Registered swoole events:
start, shutdown, managerStart, managerStop, workerStart, workerStop, workerError, connect, close, receive
哈哈哈Swoft rpc register service success by consul哈哈哈!
Server start success (Master PID: 66, Manager PID: 73)
6、查看consul集群ui页面,看是否注册成功
访问192.168.78.200:8550
这时,到consul集群中的任意一台中查看,会发现,Services列表中多了一个名为swoft_goods_server_110的服务
7、本质
其实本质就是swoft有一个onstart监听事件,swoft启动时,就会触发该事件,然后执行app/Listener/RegisterServiceListener.php中的handle方法,对consul执行请求
curl -X PUT -d '{"id": "swoft_goods_server_110","name": "swoft_goods_server_110","address": "127.0.0.1","port": 8500,"tags": ["rpc"]}' http://192.168.78.200:8550/v1/agent/service/register
8、将ID替换成动态的
如果不换成动态的,三台swoft的ID将都一样,那在consul里只有一个service,而非三个service,就不对了。
这里,采用根据容器IP来区分,IP的获取可参考文章 在代码同步下,将ip写入.env,实现动态服务注册
现在,每台swoft容器内都有一个.env文件了,里面都有一个key是HOST,我们就采用该key进行区分
修改文件app/Listener/RegisterServiceListener.php 中的handle方法:
public function handle(EventInterface $event): void
{
$rpcServer = $event->getTarget();
$host = env('HOST') ? : '127.0.0.1';
$ipSign = explode('.', $host)[3];
$service = [
'ID' => 'swoft_goods_server'.'_'.$ipSign,
'Name' => 'swoft_goods_server',
'Tags' => [
'rpc'
],
'Address' => env('HOST'),
'Port' => $rpcServer->getPort(),
'Meta' => [
'version' => '1.0'
],
'EnableTagOverride' => false,
'Weights' => [
'Passing' => 10,
'Warning' => 1
]
];
// Register
$this->agent->registerService($service);
}
9、最终效果
我们是通过docker-compose编排的一组服务,所以通过 docker-compose restart
命令可以直接重启这组服务内的所有容器。
又因为我们在swoft中设置了服务自动启动,启动时就会自动,触发了swoft的onstart监听事件,进而执行了RegisterServiceListener.php中的handle方法,所以正常来说,重启这组服务,consul中会多出来一组服务注册。
docker-compose restart
Consul 界面:
点进去:
10、服务注册总结
到这里,就实现了服务的自动注册。
三、服务的健康检查
1、引入健康检查机制
当前虽然完成了服务自动注册,但有一个问题是如果容器停掉了或主机宕机了,是没有办法触发服务自动注销的(服务注销在下面会讲),因此这里引入健康检查机制。
该机制实现起来比较简单,实际上就是在注册时,添加一些参数。
修改文件 app/Listener/RegisterServiceListener.php 中的handle方法为:
public function handle(EventInterface $event): void
{
$rpcServer = $event->getTarget();
$host = env('HOST') ? : '127.0.0.1';
$ipSign = explode('.', $host)[3];
$service = [
'ID' => 'swoft_goods_server'.'_'.$ipSign,
'Name' => 'swoft_goods_server',
'Tags' => [
'http'
],
'Address' => env('HOST'),
'Port' => $rpcServer->getPort(),
'Meta' => [
'version' => '1.0'
],
'EnableTagOverride' => false,
'Weights' => [
'Passing' => 10,
'Warning' => 1
],
'Check' => [
// 定义检测名称
'name' => 'swoft.goods.server',
// 定义consul检测swoft的ip及端口号
'tcp' => '192.168.78.104:18'.$ipSign,
// 检测频率
'interval' => '5s',
// 超时时间(超过多长时间无响应,判定为服务挂掉)
'timeout' => '2s'
]
];
// Register
$this->agent->registerService($service);
echo '哈哈哈Swoft rpc register service success by consul哈哈哈!'.PHP_EOL;
}
可以看到,我们在参数中添加了key为Check 的元素,添加完该元素后,consul会根据这些值来进行健康检查,如果检测到服务不能正常提供了,则会下掉该服务信息。
不要忘记修改完swoft代码后重启swoft,这里就重启这整组服务了
docker-compose restart
然后我们停掉swoft_110容器进行测试:
docker stop swoft_110
然后查看consul界面:
发现110在consul service列表里不见了,说明健康检查机制引入成功。
2、健康检查的本质
健康检查是consul自带的功能,只要在服务注册时,将相应参数传给consul,consul就会对该服务按照传过来的参数进行健康检查,比如上面,我们传递的就是:
'Check' => [
// 定义检测名称
'name' => 'swoft.goods.server',
// 定义consul检测swoft的ip及端口号
'tcp' => '192.168.78.104:18'.$ipSign,
// 检测频率
'interval' => '5s',
// 超时时间(超过多长时间无响应,判定为服务挂掉)
'timeout' => '2s'
]
四、商品服务的注销
其实有了consul的健康检查,服务的自动注销也就可有可无了,不过这里还是写一下,尽管实际上已经用不到了。
1、修改 app/Listener/DeregisterServiceListener.php 中的handle方法
public function handle(EventInterface $event): void
{
$host = env('HOST') ? : '127.0.0.1';
$ipSign = explode('.', $host)[3];
$rpcServer = $event->getTarget();
// 这里参数是在RegisterServiceListener.php中的handle方法中填写的ID
$this->agent->deregisterService('swoft_goods_server'.'_'.$ipSign);
}
其实本质就是swoft有一个shudown监听事件,当停止服务时,就会触发该事件,然后执行app/Listener/DeregisterServiceListener.php中的handle方法,对consul执行了请求
curl --request PUT http://192.168.78.200:8550/v1/agent/service/deregister/swoft_goods_server_110
2、查看效果
我们发现,当control + c 停掉运行中的rpc服务时,刚刚注册的service已经消失了,说明swoft已经把取消服务的请求发送给consul,并执行完成了。
3、温馨提示
每次修改完代码,不要忘记重启容器或重新 start swoft服务哦 ( ̄▽ ̄)"。
因为swoole、swoft、hyperf这类框架都是常驻内存的,修改完代码,并不是刷新一下页面就会请求新代码的,只有重新启动服务才会加载新代码。
五、服务发现
1、概述
服务发现也是基于consul来实现的,当然也可以有其他方式来实现,但consul当前算得上是最主流的。
本质上是concul会对注册的服务按照注册时的参数设置进行健康检查,然后调用者进行服务发现时,通过请求指定接口获取当前健康服务的服务列表。
2、使用consul做服务发现应该请求什么接口
http://192.168.78.200:8550/v1/health/service/swoft_goods_server
该接口可以获取到所有指定服务组名称的健康服务的列表。
3、接口返回数据的数据结构
# 请求http://192.168.78.200:8550/v1/health/service/swoft_goods_server接口的返回数据
[
{
"Node": {
"ID": "54590f5f-e1c0-6ccd-d4ac-88d3e29a42b3",
"Node": "consul_50",
"Address": "170.200.7.50",
"Datacenter": "dc1",
"TaggedAddresses": {
"lan": "170.200.7.50",
"lan_ipv4": "170.200.7.50",
"wan": "170.200.7.50",
"wan_ipv4": "170.200.7.50"
},
"Meta": {
"consul-network-segment": ""
},
"CreateIndex": 11,
"ModifyIndex": 14
},
"Service": {
"ID": "swoft_goods_server_110",
"Service": "swoft_goods_server",
"Tags": [
"http"
],
"Address": "172.22.22.110",
"TaggedAddresses": {
"lan_ipv4": {
"Address": "172.22.22.110",
"Port": 18306
},
"wan_ipv4": {
"Address": "172.22.22.110",
"Port": 18306
}
},
"Meta": {
"version": "1.0"
},
"Port": 18306,
"Weights": {
"Passing": 10,
"Warning": 1
},
"EnableTagOverride": false,
"Proxy": {
"Mode": "",
"MeshGateway": {},
"Expose": {}
},
"Connect": {},
"CreateIndex": 418,
"ModifyIndex": 418
},
"Checks": [
{
"Node": "consul_50",
"CheckID": "serfHealth",
"Name": "Serf Health Status",
"Status": "passing",
"Notes": "",
"Output": "Agent alive and reachable",
"ServiceID": "",
"ServiceName": "",
"ServiceTags": [],
"Type": "",
"Interval": "",
"Timeout": "",
"ExposedPort": 0,
"Definition": {},
"CreateIndex": 11,
"ModifyIndex": 11
},
{
"Node": "consul_50",
"CheckID": "service:swoft_goods_server_110",
"Name": "swoft.goods.server",
"Status": "passing",
"Notes": "",
"Output": "TCP connect 192.168.78.104:18110: Success",
"ServiceID": "swoft_goods_server_110",
"ServiceName": "swoft_goods_server",
"ServiceTags": [
"http"
],
"Type": "tcp",
"Interval": "",
"Timeout": "",
"ExposedPort": 0,
"Definition": {},
"CreateIndex": 418,
"ModifyIndex": 420
}
]
},
{
"Node": {
"ID": "54590f5f-e1c0-6ccd-d4ac-88d3e29a42b3",
"Node": "consul_50",
"Address": "170.200.7.50",
"Datacenter": "dc1",
"TaggedAddresses": {
"lan": "170.200.7.50",
"lan_ipv4": "170.200.7.50",
"wan": "170.200.7.50",
"wan_ipv4": "170.200.7.50"
},
"Meta": {
"consul-network-segment": ""
},
"CreateIndex": 11,
"ModifyIndex": 14
},
"Service": {
"ID": "swoft_goods_server_130",
"Service": "swoft_goods_server",
"Tags": [
"http"
],
"Address": "172.22.22.130",
"TaggedAddresses": {
"lan_ipv4": {
"Address": "172.22.22.130",
"Port": 18306
},
"wan_ipv4": {
"Address": "172.22.22.130",
"Port": 18306
}
},
"Meta": {
"version": "1.0"
},
"Port": 18306,
"Weights": {
"Passing": 10,
"Warning": 1
},
"EnableTagOverride": false,
"Proxy": {
"Mode": "",
"MeshGateway": {},
"Expose": {}
},
"Connect": {},
"CreateIndex": 59,
"ModifyIndex": 59
},
"Checks": [
{
"Node": "consul_50",
"CheckID": "serfHealth",
"Name": "Serf Health Status",
"Status": "passing",
"Notes": "",
"Output": "Agent alive and reachable",
"ServiceID": "",
"ServiceName": "",
"ServiceTags": [],
"Type": "",
"Interval": "",
"Timeout": "",
"ExposedPort": 0,
"Definition": {},
"CreateIndex": 11,
"ModifyIndex": 11
},
{
"Node": "consul_50",
"CheckID": "service:swoft_goods_server_130",
"Name": "swoft.goods.server",
"Status": "passing",
"Notes": "",
"Output": "TCP connect 192.168.78.104:18130: Success",
"ServiceID": "swoft_goods_server_130",
"ServiceName": "swoft_goods_server",
"ServiceTags": [
"http"
],
"Type": "tcp",
"Interval": "",
"Timeout": "",
"ExposedPort": 0,
"Definition": {},
"CreateIndex": 59,
"ModifyIndex": 61
}
]
}
]
4、经过PHP原生处理,得到可用服务ip及端口号列表
public function getHealthServerList()
{
// $serviceList是请求http://192.168.78.200:8550/v1/health/service/swoft_goods_server返回的json数据
$serviceList = '';
$serviceName = 'swoft_goods_server';
$serviceList = json_decode($serviceList, true);
$address = [];
foreach ($serviceList as $k => $v) {
//判断当前的服务是否是活跃的,并且是当前想要去查询服务
foreach ($v['Checks'] as $c) {
if ($c['ServiceName'] == $serviceName && $c['Status'] == "passing") {
$address[$k]['address'] = $v['Service']['Address'] . ":" . $v['Service']['Port'];
$address[$k]['weight'] = $v['Service']['Weights']['Passing'];
}
}
}
return $address;
}
它会变成这个样子:
[
{
"address": "172.22.22.110:18306",
"weight": 10
},
{
"address": "172.22.22.120:18306",
"weight": 10
},
{
"address": "172.22.22.130:18306",
"weight": 10
}
]
到这里,就得到了一个可用的商品微服务的服务列表,即完成了服务发现。
当然,这是原生写法,如果在框架内,可能会有封装好的方法,使用哪种均可。