商品客户端和服务端的服务注册、发现、注销、健康检查

一、概述

 

二、实现商品服务的注册

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
  }
]

到这里,就得到了一个可用的商品微服务的服务列表,即完成了服务发现。

当然,这是原生写法,如果在框架内,可能会有封装好的方法,使用哪种均可。