本文将带你从基础的微服务架构设计、网络协议、注册中心、配置中心、网关层面 渐进式讲解其微服务。

一、微服务架构设计方案

架构演进

微服务概念

 拆分

​​​​​​三个火枪手原则

AKF原则

二、微服务注册中心和配置中心

为什么要使用服务发现与注册

为什么要使用配置中心

官方下载地址

设置环境变量

Server配置

单机配置

集群配置

命令解析

ThinkPHP接入Consul

配置信息中心 

三、微服务API网关设计

为什么需要网关

API网关对比

Kong与Konga的关联

konga关键概念词

​​​​​​​下载镜像

​​​​​​​安装

​​​​​​​创建网络

​​​​​​​安装postgres,kong依赖于postgres

​​​​​​​初始化kong数据表信息

​​​​​​​启动kong

​​​​​​​初始化konga数据信息

​​​​​​​创建连接节点

​​​​​​​实现负载

创建Upstreams

​​​​​​​创建服务

​​​​​​​创建路由

​​​​​​​实现Basic Auth 与 JWT 认证

​​​​​​​添加Basic Auth插件

​​​​​​​添加用户信息

​​​​​​​添加JWT 插件

​​​​​​​添加JWT 认证信息

​​​​​​​实现Oauth2 认证

​​​​​​​添加Oauth2 插件

添加用户信息

​​​​​​​获取token

​​​​​​​API限流

​​​​​​​API黑白名单

​​​​​​​延伸

​​​​​​​jwt异常

​​​​​​​虚拟机环境 IP黑白名单失效

​​​​​​​服务恢复

四、结合swoole、swoft微服务化搭建

​​​​​​​网络基础

​​​​​​​OSI七层模型与TCP/IP模型

​​​​​​​ARP 地址解析协议

​​​​​​​经典的TCP三次握手与四次挥手

​​​​​​​抓包

​​​​​​​Swoft框架介绍

​​​​​​​协程

​​​​​​​下载框架代码

​​​​​​​RPC 服务

​​​​​​​使用swoft  实现RPC 服务

​​​​​​​前期准备工作

​​​​​​​修改配置文件

​​​​​​​启动RPC服务

​​​​​​​RPC客户端实现

五、整合API与Kong网关以及consul注册中心

​​​​​​​consul服务注册

​​​​​​​新增注册中心相关配置

​​​​​​​添加监听注册

​​​​​​​创建Consul控制器

​​​​​​​启动服务

​​​​​​​Swoft 服务发现

​​​​​​​新增注册新增相关配置

​​​​​​​RPC服务发现

​​​​​​​服务限流

​​​​​​​熔断与降级

​​​​​​​consul与kong网关结合

​​​​​​​加入dns_resolver 信息

添加对应的SERVER信息

​​​​​​​后记


按照模块划分,公用一个数据库


垂直拆分架构

按业务功能划分单独的子系统,公用一个数据库

将数据库进行分拆分离,如下图:

 

项目工期短且要求是微服务的架构

细颗粒度拆分

  1. 服务划分过细,服务间关系复杂
  2. 服务数量太多,团队效率急剧下降
  3. 没有自动化部署支撑,无法快速交付
  4. 没有服务治理,微服务达到一定数量,后台管理混乱

纵向拆分和横向拆分

纵向拆分:从业务纬度进行拆分。标准是按照业务的关联程度来决定,关系比较密切的业务适合拆分为一个微服务,而功能相对比较独立的业务适合单独拆分为一个微服务。

横向拆分:从公共且独立功能纬度拆分。标准是按照是否有公共的被多个其他服务调用,且依赖的资源独立不予其他业务耦合。

划分例子

  1. 第一种分为商品、帖子、新闻3个服务
  2. 第二种分为商品、订单、帖子、回帖、新闻、评论6个服务

如何选择呢?

如果你的团队只有9个人,那么分为3个是合理的,如果有18个人。那么6个服务是合理的。

为什么呢,这就是三个火枪手原则。

 上图 我们可以看到在没有服务注册发现的时候一个调用者需要维护多个服务的IP和端口,这是非常不友好的做法,当我们服务进行调整的时候就有可能导致服务调用失败,还有服务器更换服务器,上下新服务,都会受到影响。将来某一个服务节点出现问题,排查对应程序对运维人员来说都是一场很大的灾难,因为不知道哪一个节点出了问题,需要每一台服务器的去排查。

有服务注册发现的结构

 我们从上图可以发现,当我们有注册中心之后调用者不需要自己去维护所有服务的信息了,仅需要向注册中心请求获取服务,就可以拿到想要的服务信息。这样当我们的服务有所调整,或者上线下线服务,都要可以轻松操作,并且可以在注册中间检查到服务的健康情况,帮助运维人员快速定位到故障的服务器。

 使用配置中心

主流的配置中心

  1. Apollo是有协程开源的分布式配置中心
  2. Spring Cloud Config
  3. Consul

 在浏览器输入 http://192.168.5.189:8500 来查看信息

集群模式下  有三个实例

点击到Nodes 可以查看到如下图信息:

在Services中  有我们刚才注册的服务名为demoService服务啦 

当我们停到服务后,此刻consul注册中心也显示服务下线了

当我们请求 http://192.168.5.189:8000/consul/service/info

获取对应的配置 

访问接口   http://192.168.5.189:8000/consul/config/info?key=demoService/dev/mysql/host

返回一个数组

[
   {
        "LockIndex":0,
        "Key":"demoService/dev/mysql/host",
        "Flags":0,
        "Value":"MTI3LjAuMC4x",
        "CreateIndex":410,
        "ModifyIndex":410
   }
]

注:这里返回的Value是经过base64编码的加密信息,可以使用base64_decode 进行解码 

下面  我们以Kong+Konga 来搭建API网关

本地搭建konga 参考 https://blog.csdn.net/qq_31289187/article/details/127683144

 

推荐使用Docker 搭建konga 

docker相关知识  请查看此处  传送门

docker 离线安装 方法 https://blog.csdn.net/qq_54928486/article/details/127069180

docker  基础命令 https://blog.csdn.net/weixin_45630258/article/details/124681551

  1. ip是当前主机ip地址,端口是7001默认是8001,本文对外映射时设置成7001了)
  2. 此刻我们在浏览器 访问http://192.168.5.189:7001

 ​​​​​​​

  1. 输入name,然后提交
  2. 点击刚添加的upstream点击DETAILS添加targets,然后点击ADD TARGET输入target(ip+port)后点击SUBMIT TARGET即可,ip为本地电脑ip保证kong容器内可访问,端口为本地服务端口

注:这里我们最好使用调用API的方式进行添加

可以通过接口查看信息 如

在浏览器中访问 http://192.168.5.189:7001/upstreams  发现什么也没有

使用接口进行添加

路由地址  http://192.168.5.189:7001/upstreams

请求方式  POST

请求参数  name    

添加target 目标  

路由地址 http://192.168.5.189:7001/upstreams/NAME/targets

请求方式 POST

请求参数1  target     目标  IP+端口

请求参数2  weight     权重   数字越大  优先级越高

其中NAME   动态关联上面的upstreams添加的name 名称 

测试负载均衡

我们此刻访问http://192.168.5.189:7000/abc 

 再次刷新请求

 说明负载均衡配置好了

 查看对应的路由下的插件  我们可以看到刚才添加的basic-auth插件了 如下图所示:

此刻  我们访问之前的路由地址信息 如 http://192.168.5.189:7000/abc   会显示如下信息:

 说明插件已经生效   接下来我们需要配置添加用户信息

 点击用户名 进入到用户详情  选择credentials 添加对应的BASIC 信息 如下图:

此刻  我们在浏览器中 输入对应的Basic Auth信息 就可以正常访问接口了   也可以使用POSTMAN 工具来模拟认证。如图所示:

查看对应的服务下的插件  我们可以看到刚才添加的jwt插件了 如下图所示:

 此刻  我们访问之前的路由地址信息 如 http://192.168.5.189:7000/abc   会显示如下信息:

路由地址 http://192.168.5.189:7001/consumers/USERNAME/jwt

注:consumers  代表需要在consumers层操作   USERNAME  与上面创建的CONSUMERS

名保持一致

请求方式 POST

请求参数1   algorithm  算法类型  我们这里填写HS256

请求参数2   key    这里默认在JWT荷载信息中  ,加入iss 做关联

请求参数3  secret  密钥  这里我们填写刚才生成的密钥 如 RSY16m4MuSsU3yJXpXzUGdRPmhHm9gni

为了不影响认证  将之前的Basic-Auth 删除掉

访问接口  需要在访问接口时,加入Authorization   内容为bearer+空格+JWT    如下图:

此JWT 为

 左边计算出的JWT 

 将得到client_id  与 client_secret   请保存下来   下面获取token时,需要传入该值

 直接访问  提示需要填写token

 访问接口  需要在访问接口时,加入Authorization   内容为token_type+空格+access_token    如下图:

我们将限流添加到Server层

路由地址 http://192.168.5.189:7001/services/SERVICES_NAME/plugins  

注:services  代表需要在Servvice层操作   SERVICES_NAME  与上面创建的服务名名保持一致

请求方式 POST

请求参数1  name    插件名称  我们这里填写rate-limiting

请求参数2 config.minute  每分钟访问次数  

请求参数3 config.limit_by  限制条件  

 

 此刻 直接访问接口  当访问6次后,提示访问失败了

 直接访问 

也可以使用docker container ls 查看容器列表

将得到的容器id  使用docker start 容器id 或者使用docker restart 容器id启动  如:

docker start 3fdc87f60d8b

这里需要启动三个容器  待三个容器全部正常启动后  可以正常访问http://127.0.0.1:1337  即可正常访问啦

若错误 docker driver failed programming  如下图  

问题分析:

首先理清一下我做了什么操作。我记得在我开启docker后,执行 docker-compose up -d 启动完容器后,发现无法连接 MySQL 容器,经查没有关闭防火墙,未开放3306端口,因此执行 systemctl stop firewalld.service 关闭防火墙,最后我 docker-compose restart 重启了一下容器就出现了上述错误。

那毫无疑问,肯定是我关防火墙导致重启容器失败。为什么会这样呢?

原因:docker 服务启动时定义的自定义链 docker 由于 防火墙 被清掉。防火墙 的底层是使用 iptables 进行数据过滤,建立在iptables之上,这可能会与 docker产生冲突。当 防火墙 启动或者关闭的时候,将会从 iptables 中移除 docker 的规则,从而影响了 docker的正常工作。当你使用的是 Systemd (我上面关闭防火墙用的 systemctl 就是 Systemd 的主命令, 用于管理系统) 的时候, 防火墙 会在 docker 之前启动,但是如果你在 docker 启动之后再启动 或者重启 防火墙 ,你就需要重启 docker进程了。重启 docker服务及可重新生成自定义链docker

问题解决:

重启 docker 即可:systemctl restart docker

systemctl restart docker

 OSI与TCP/IP模型对比

 在计算机的实际流程

 TCP 首部

 TCP三次握手

 TCP四次挥手

 

UDP协议

 UDP 中文名是用户数据报协议,是OSI参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务

 TCP与UDP的区别

课外延伸  

推荐抓包工具   Wireshark

tcpdump -i  -w  host

参数说明

  • -i  指定抓包的网卡名称
  • -w  抓包存放的路径
  • host  对方服务器IP。
  • -s 表示从一个包中截取的字节数。0表示包不截断,抓完整的数据包。默认的话 tcpdump 只显示部分数据包,默认68字节。
  • -v 输出一个稍微详细的信息,例如在ip包中可以包括ttl和服务类型的信息

命令如下:

tcpdump -s 0 -i any host 192.168.5.188 -v -w test.pcap

 我们可以看到  其默认绑定端口为18306

 这里为了方便  可以先把user微服务写好,然后再复制改一改vehicle

 swoft 有一个默认的RPC 的UserServer  我们为了区分 将其方法返回改动一下 如下图:

vim app/Rpc/Service/UserService.php
#我们将其getList返回的 略微修改一下   这里我们方便测试 改成我们想要的  后续需要根据业务逻辑去改造了 

此时user 用户的RPC服务已经弄好了,且启动成功正常  接下来我们看看vehicle 车辆的RPC服务

部署vehicle RPC服务

与上面一样 切换目录到vehicle项目目录

cd vehicle

 修改一下RPC服务的端口号  因为都在一台服务器上,刚才user的RPC端口是18308   此刻,我们vehicle服务的RPC 改成18309

vim app/bean.php

这里改成18309

将/vehicle/app/Rpc/Lib/UserInterface.php 案例修改成对应的接口   这里我们在vehicle服务中,故此实现的也是Vehicle的接口  命令如下:

cd app/Rpc/Lib
mv UserInterface.php VehicleInterface.php

修改VehicleInterface.php 内容 

修改注解信息与对应的class类名  如图所示:

修改对应的Service 并删除无用的服务    如下:

cd app/Rpc/Service
mv UserService.php VehicleService.php
rm UserServiceV2.php

 

 修改VehicleService.php内容  将服务接口换成VehicleInterface  与红框保持一致  如下图:

 此刻我们启动RPC 服务  运行命令:

./bin/swoft rpc:start

 这里报错了    为什么呢   是因为我们刚才删除了那个userService的服务  这里我们定位到这个报错的路径   可以看看

vim app/Http/Controller/RpcController.php

 可以看到  红框的地方  我们已经删除此文件了  故此他会启动报错  这里我们删除该控制器

rm app/Http/Controller/RpcController.php

再次运行  可以看到正常启动啦

 bean.php 配置RPC 连接信息  根据实际情况 填写对应的RPC服务信息   红框的内容根据实际情况填写  这里我们填写上user和vehicle上面两个RPC服务的具体信息  如图所示

'vehicle'=> [
    'class'    => ServiceClient::class,
    'host'     => '127.0.0.1',
    'port'     => 18309,
    'settting' => [
          'timeout'     => 0.5,
          'connect_timout' => 1.0,
          'write_timeout'  => 10.0,
          'read_timeout'   => 0.5,
    ],
    'packet' => bean('rpcClientPacket')
],
'vehicle.pool' => [
   'class'  => ServicePool::class,
   'client' => bean('vehicle'),
],

​​​​​​​拷贝Lib 接口文件

两个RCP服务端下的lib下的接口文件拷贝到http服务目录下   命令如下:

cd /http/app/Rpc/Lib
cp /user/app/Rpc/Lib/UserInterface.php ./
cp /vehicle/app/Rpc/Lib/VehicleInterface.php ./

如下图:

 ​​​​​​​​​​​​​​调用RPC服务

我们还是在http 目录下操作   编写对应的控制器   这里我们使用他默认的一个控制器案例 RpcController.php

cd http/app/Http/Controller
vim RpcController.php

添加对应的VehicleInterface 命名空间 等信息  这里我用红框标记出来了  这里仅做参考,后续需要根据实际情况填写。 如下图:

 这里需要注意的是@Reference 这个注解 与 @var   这个与上面填写的bean.php中的RPC信息保持一致。

​​​​​​​​​​​​​​启动HTTP服务 

运行下面命令:

./bin/swoft http:start

说明启动正常

此时 访问 http://192.168.5.189:18406  显示如下:

说明HTTP服务正常

此后 我们在访问http://192.168.5.189:18406/rpc/getList  结果如下图:

​​​​​​​​​​​​​​非 Swoft 框架调用

默认消息协议是 json-rpc, 所以我们按照这个格式就可以了,需要注意的是,默认消息协议是以 \r\n\r\n 结尾的。

这里 method 的格式为 "{version}::{class_name}::{method_name}"。

{
    "jsonrpc": "2.0",
    "method": "{version}::{class_name}::{method_name}",
    "params": [],
    "id": "",
    "ext": []
}

示例: 如果使用默认消息协议,可以按照如下方式进行封装

<?phpconst RPC_EOL = "\r\n\r\n";function request($host, $class, $method, $param, $version = '1.0', $ext = []) {
    $fp = stream_socket_client($host, $errno, $errstr);
    if (!$fp) {
        throw new Exception("stream_socket_client fail errno={$errno} errstr={$errstr}");
    }    $req = [
        "jsonrpc" => '2.0',
        "method" => sprintf("%s::%s::%s", $version, $class, $method),
        'params' => $param,
        'id' => '',
        'ext' => $ext,
    ];
    $data = json_encode($req) . RPC_EOL;
    fwrite($fp, $data);    $result = '';
    while (!feof($fp)) {
        $tmp = stream_socket_recvfrom($fp, 1024);        if ($pos = strpos($tmp, RPC_EOL)) {
            $result .= substr($tmp, 0, $pos);
            break;
        } else {
            $result .= $tmp;
        }
    }    fclose($fp);
    return json_decode($result, true);
}$ret = request('tcp://127.0.0.1:18307', \App\Rpc\Lib\UserInterface::class, 'getList',  [1, 2], "1.0");
var_dump($ret);

这里我们改成具体的调用 

 调用后  发现RPC的调用正常

 因为我们配置了HTTP方式的健康检查  故此  我们需要在user项目服务下 同时开启一个HTTP服务 端口号为18318 修改配置如下:

vim app/bean.php

 这里的端口与刚才配置的健康健康端口保持一致,并打开listener监听,这里是启动http服务后,同时启动rpc 里面的这个rpcServer服务 

 此刻 我们在consul 中查看刚才的注册信息

上次我们已经实现的User项目的注册服务  接下来我们实现Vehicle下的 

刚才的步骤一样,因为都在一个服务器上  故此 端口号请勿重复,否则起不来

这里我们就不多做讲解

启动服务后,我们在consul注册中心可以看到如下信息:

并在bean.php中  新增RpcProvider 命名空间  对应下面的128行,要不然会报错

use App\Common\RpcProvider;

 修改bean.php 下面的配置信息

 网上说  这里在 user 服务上,通过 provider 参数注入了一个服务提供者 RpcProvider::class (bean 名称)

红色框的地方  需要添加 而这里面的swoft-user 根据实际情况填写

一下是getList方法代码

public function getList(Client $client) : array 
{  
      //Get health service from consul
     $services = $this->ServiceHelper->getService('swoft-user');
     $ret = [];
     foreach ($services as $key=>$value) {
           $ret[$key] = $value['Address'] . ':' . $value['Port'];
     }
     return $ret;
}

上面调整完后  我们复制一个RpcProvider.php  命令如下:

cp app/Common/RpcProvider.php app/Common/VehicleRpcProvider.php

VehicleRpcProvider.php 内容如下:

修改具体的bean.php 配置     

新增命名空间等  如图所示:

 

启动服务 

./bin/swoft http:start

访问之前写的RPC客户端调用  http://192.168.5.189:18406/rpc/getList

访问正常  说明consul服务发现成功了

 操作RPC服务控制器

vim app/Rpc/Service/UserService.php

新增 命名空间

use Swoft\Limiter\Annotation\Mapping\RateLimiter;

在getList方法前  加入对应的注解信息 

 * @RateLimiter(rate=1,max=2,fallback="limiterFallback")
  • name  缓存前缀
  • rate 允许多大的请求访问,请求数/秒
  • max 最大的请求数
  • default 初始化请求数
  • fallback 降级函数,和 breaker 一样

 * @RateLimiter(rate=1, max=2,fallback="limiterFallback") 

添加降级函数limiterFallback

改变的地方 使用红色框表示 如下图所示:

改完后 记得重启服务 要不然不生效

此刻我们访问http://192.168.5.189:18406/rpc/getList  多访问几次 就会限流了  如下图:

限流如此简单  更多 请查阅swoft官方手册  https://www.swoft.org/documents/v2/microservice/limit/  

我们连续访问 http://192.168.5.189:18406/rpc/getList 

 

创建路由 名字 swoft-route1  paths 为/ap   如图所示:

添加完服务后  我们在http服务上 改造一下Home控制器的index方法 代码如下图:

重启服务后

此刻我们访问http://192.168.5.189:7000/ap/home/index  访问结果如下:

说明此刻接口通过网关kong 找寻路由 转发到了对应的服务上了 

但 我们后续服务地址 包括端口变化了  此时这种方法就不行了

在实验前  我们将Http项目加入到consul注册中心,步骤就不在此赘述,详情请查看第五章第1小点

结果如下图:
 

注册信息

我们kong和consul 来结合 需要DNS 这个端口配合实现  如上图 我们可以看到DNS 为8600

我们先删除容器  删除之前我们先停掉容器  如下:

docker stop 容器id

删除容器

docker rm 容器id

重新启动kong  并加入相关DNS配置   命令如下:

docker run -d --name kong-ee --network=kong_net \
  -e "KONG_DATABASE=postgres" \
  -e "KONG_PG_HOST=172.17.0.1" \
  -e "KONG_PG_PASSWORD=kong" \
  -e "KONG_PROXY_ACCESS_LOG=/dev/stdout" \
  -e "KONG_ADMIN_ACCESS_LOG=/dev/stdout" \
  -e "KONG_PROXY_ERROR_LOG=/dev/stderr" \
  -e "KONG_ADMIN_ERROR_LOG=/dev/stderr" \
  -e "KONG_DNS_RESOLVER=192.168.5.189:8600" \
  -e "KONG_ADMIN_LISTEN=0.0.0.0:8001" \
  -e "KONG_ADMIN_GUI_URL=http://192.168.5.189:7002" \
    -p 7000:8000 \
    -p 7001:8001 \
    -p 7443:8443 \
    -p 7444:8444 \
    -p 7002:8002 \
  kong/kong-gateway:2.4.1.0-alpine

这里与之前的启动  多了一个配置  -e "KONG_DNS_RESOLVER=192.168.5.189:8600"

如果创建失败  可以看看容器名是否被占用

docker container ls 
#或者运行下面命令
docker container ls -a 

 然后删除不需要的容器 

#删除指定容器
docker rm -f <containerid>

若报网络不存在,如下图:

请查询具体的docker 网络  如下命令:

docker network ls

 

然后再重新运行启动命令 当运行成功后,我们打开konga 可以在info中查询对应的DNS信息

如果有该值  说明我们配置成功​​​​​​​

然后我们创建一个路由

访问http://192.168.5.189:7000/aa/home/index

完美通过 kong ->访问注册信息 ->访问实际服务 啦

 我们来实验一下当consul中的服务IP变化后,我们kong网关还是否可以访问正常。

当前swoft-http 服务端口为18406  我们改成18407 如下图:

首先我们回到http项目  修改bean.php 中的HTTP启动端口,然后将监听中的健康检查也改成对应的接口

vim app/Listener/RegisterServiceListener.php

 然后重新启动HTTP服务 在查看consul 如下图:

接下来验证一下:

我们访问之前在konga中新建的服务  采用IP+端口方式  如图所示:

接口已经不通了 

我们在访问kong+consul 新建的服务 如图所示:

依旧正常访问  

故此:我们只需要将kong与consul做关联,后续改服务,不需要修改kong网关啦 这样前端业务也不需要变动。

​​​​​​​后记

作者的话:

       学习完整个微服务框架综合实战,我们了解了什么是微服务,为什么使用微服务,以及拆分原则。对注册中心与服务发现有了一定的了解,也知道了kong与konga的关系,以及一些网络知识,基本上按照前端用户使用网关kong调用接口,网关关联注册中心consul,而内部接口进行服务注册,调用采用RPC方式 。整个流程走完,我相信你一定对微服务架构有了一定的认识。

       本文中涉及的网关以及注册中心对应的服务软件都不是一定的,可以根据实际情况来选择适合自己的项目,学习是一件枯燥且有趣的事情,学海无涯,学会站在巨人的肩膀上,本次学习就此告一段落。谢谢!

点赞(4) 打赏

评论列表 共有 0 条评论

暂无评论

微信小程序

微信扫一扫体验

立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部