本文将带你从基础的微服务架构设计、网络协议、注册中心、配置中心、网关层面 渐进式讲解其微服务。
一、微服务架构设计方案
架构演进
微服务概念
拆分
三个火枪手原则
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信息
后记
按照模块划分,公用一个数据库
垂直拆分架构
按业务功能划分单独的子系统,公用一个数据库
将数据库进行分拆分离,如下图:
项目工期短且要求是微服务的架构
细颗粒度拆分
- 服务划分过细,服务间关系复杂
- 服务数量太多,团队效率急剧下降
- 没有自动化部署支撑,无法快速交付
- 没有服务治理,微服务达到一定数量,后台管理混乱
纵向拆分和横向拆分
纵向拆分:从业务纬度进行拆分。标准是按照业务的关联程度来决定,关系比较密切的业务适合拆分为一个微服务,而功能相对比较独立的业务适合单独拆分为一个微服务。
横向拆分:从公共且独立功能纬度拆分。标准是按照是否有公共的被多个其他服务调用,且依赖的资源独立不予其他业务耦合。
划分例子
- 第一种分为商品、帖子、新闻3个服务
- 第二种分为商品、订单、帖子、回帖、新闻、评论6个服务
如何选择呢?
如果你的团队只有9个人,那么分为3个是合理的,如果有18个人。那么6个服务是合理的。
为什么呢,这就是三个火枪手原则。
上图 我们可以看到在没有服务注册发现的时候一个调用者需要维护多个服务的IP和端口,这是非常不友好的做法,当我们服务进行调整的时候就有可能导致服务调用失败,还有服务器更换服务器,上下新服务,都会受到影响。将来某一个服务节点出现问题,排查对应程序对运维人员来说都是一场很大的灾难,因为不知道哪一个节点出了问题,需要每一台服务器的去排查。
有服务注册发现的结构
我们从上图可以发现,当我们有注册中心之后调用者不需要自己去维护所有服务的信息了,仅需要向注册中心请求获取服务,就可以拿到想要的服务信息。这样当我们的服务有所调整,或者上线下线服务,都要可以轻松操作,并且可以在注册中间检查到服务的健康情况,帮助运维人员快速定位到故障的服务器。
使用配置中心
主流的配置中心
- Apollo是有协程开源的分布式配置中心
- Spring Cloud Config
- 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
- ip是当前主机ip地址,端口是7001(默认是8001,本文对外映射时设置成7001了)
- 此刻我们在浏览器 访问http://192.168.5.189:7001
- 输入name,然后提交
- 点击刚添加的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方式 。整个流程走完,我相信你一定对微服务架构有了一定的认识。
本文中涉及的网关以及注册中心对应的服务软件都不是一定的,可以根据实际情况来选择适合自己的项目,学习是一件枯燥且有趣的事情,学海无涯,学会站在巨人的肩膀上,本次学习就此告一段落。谢谢!
发表评论 取消回复