背景
商品详情实际上没有太多的业务,就是查询如首页,以及商品分页查询,详情查询
例如
淘宝: https://www.taobao.com/ 如下图所示
我们可以基于京东,天猫,淘宝等商场可以查阅;对于商城来说它需要的数据来源很多,一个页面中就会需要有包含
1. 图片/视频
2. 价格
3. 店铺星级评价
4. 购买方式
...
也就是说需要获取以及展示的数据就非常的多,因此在根据不同的唯度拆分的情况下就需要从不同的服务获取相关的数据,
商品模块的特点
商品详情页是商城的核心访问模块之一,重要度很高。
因此高可用就必不可少;
i.搭配高可用,比如mongodb,consul等,我们都将搭建高可用集群,以consul为例:
ii.可以选择将商品详情单独拆分成一个服务,本次课中,我们采用将它放到商品模块,但实际场景中,商品详情可以单独部署成一个服务,这个实现起来也很简单,增加商品详情服务器然后注册到consul即可。
查询多,插入少
对于商品模块以及首页等查询多,几乎无插入的内容,我们可以考虑不同层面的缓存策略以便可以更好的提高查询的效率;
比如 浏览器缓存,客户端文件缓存, nginx静态资源缓存,服务端nosql缓存。
流量节点 | 缓存技术 |
---|---|
客户端 | 使用浏览器缓存 |
客户端应用缓存 | |
客户端网络 | 代理服务器开启缓存 |
广域网 | 使用代理服务器(含CDN) |
使用镜像服务器 | |
使用P2P技术 | |
源站及源站网络 | 使用接入层提供的缓存机制 |
使用应用层提供的缓存机制 | |
使用分布式缓存 | |
静态化、伪静态化 | |
使用服务器操作系统提供的缓存机制 |
我们以淘宝为例子
在多次打开页面之后可以看到有些是获取的时间为0(缓存资料-参考:https://blog.csdn.net/adley_app/article/details/102950695)
我们可以看到一些数据会以不同的缓存策略在浏览器中缓存(这里不谈浏览器的缓存策略选择等相关知识点)
在服务端也会运用到nosql作为利器,本项目中redis+mongodb均会一起运用
数据来源于多个服务
商品这块很多数据都来自于其它服务
我们可以从大体来看,就需要用到很多的数据;如价格,店铺,活动,商品推荐,评价等信息;最少会来源两个以上的服务信息; 因此如果说我们只是正常的通过当访问一次首页或者详情就去请求(mq,tcp)获取数据的话,频繁多次会资源的消耗同时也会存在这比较大的延时问题;
问题1:如果商品详情页出了bug,得拉上其它其它服务的同事一起联调。
问题2:如果商品模块访问压力大, 会导致其它服务性能受损造成严重污染。
针对于这个问题之前提过了一个方法:数据闭环
实施方案
我们采用了数据聚合,将其他服务和模块的数据聚合到商品详情下的mongodb,前端请求mongodb获得聚合完成数据。
下图是数据聚合的过程:
会出现的问题
问题1:有些维度的数据更新频繁,而有些维度的数据很少更新,买家对不同维度的数据的实时性也要求不一有些数据需要在买家请求后同步返回,而有些数据 则可以在主要数据加载完毕后,再通过浏览器异步加载
解决:我们采用了数据异构,将不同维度的数据存入不同redis地址。上图是数据异构的过程
注意:一个商品详情页的数据,异构后是多份redis数据,聚合后是一份mongodb数据。
问题2: 查询量波动很大,可能突然来了很多访问量,这些都不可预测
采用集群支持性能的线性拓展
所以本案中的consul mongodb等,我们也将搭建集群
设计
1.通过上面的分析,我们最终得到我们总体实施方案,大体包括一下内容:
2.部署consul集群,多个server+多个client。
3.并将商品服务,订单服务,秒杀服务等等注册到consul。
4.为了提高qps,有必要引入nosql (redis+mongodb), 这个将在下节课讲解
搭建Consul集群
前期准备
因consul注册中心是可以针对于整个项目进行运用因此我们可以把consul放到公共的服务器上
利用之前通过save打包的consul镜像
通过执行docker load < consul1.4.tar
加载再镜像
构建consul集群服务
consul的集群构建一般来说建议是采用奇数个数(这是官方给出的建议也就是,consul集群个数为3,5,7 但是官方并未细节说明原因)
然后就可以启动集群了,这里启动 5 个 Consul Agent,3 个 Server(会选举出一个 Leader),2 个 Client。
如下为构建的docker-compose内容
# 编排php,redis,nginx容器
version: "3.6" # 确定docker-composer文件的版本
services: # 代表就是一组服务 - 简单来说一组容器
# server
consul_master_server_172_30: # 这个表示服务的名称,课自定义; 注意不是容器名称
image: consul1.4 # 指定容器的镜像文件
networks: ## 引入外部预先定义的网段
swoft_consul:
ipv4_address: 172.200.7.30 #设置ip地址
container_name: consul_master_server_172_30 # 这是容器的名称
command: ./consul agent -server -bootstrap-expect 3 -data-dir /tmp/consul -node=consul_master_server_172_30 -bind=172.200.7.30 -ui - client=0.0.0.0
consul_slaves_server_172_20: # 这个表示服务的名称,课自定义; 注意不是容器名称
image: consul1.4 # 指定容器的镜像文件
networks: ## 引入外部预先定义的网段
swoft_consul:
ipv4_address: 172.200.7.20 #设置ip地址
container_name: consul_slaves_server_172_20 # 这是容器的名称
command: ./consul agent -server -data-dir /tmp/consul -node=consul_slaves_server_172_20 -bind=172.200.7.20 -ui -client=0.0.0.0 -join 172.200.7.30
consul_slaves_server_172_10: # 这个表示服务的名称,课自定义; 注意不是容器名称
image: consul1.4 # 指定容器的镜像文件
networks: ## 引入外部预先定义的网段
swoft_consul:
ipv4_address: 172.200.7.10 #设置ip地址
container_name: consul_slaves_server_172_20 # 这是容器的名称
command: ./consul agent -server -data-dir /tmp/consul -node=consul_slaves_server_172_10 -bind=172.200.7.10 -ui -client=0.0.0.0 -join 172.200.7.30
# 设置网络模块
networks:
# 自定义网络
swoft_consul:
driver: bridge
ipam: #定义网段
config:
- subnet: "172.200.7.0/24"
实际上对于consul的集群构建来说非常简单,并没有想象中的难;
然后执行 docker-compose up -d 构建容器
从上面可以看到, 生成了一台leader和二台follower。
构建consul客户端
构建--基于之前的docker-compose.yaml构建
# 编排php,redis,nginx容器
version: "3.6" # 确定docker-composer文件的版本
services: # 代表就是一组服务 - 简单来说一组容器
# server
# ....
# client
consul_client_172_50: # 这个表示服务的名称,课自定义; 注意不是容器名称
image: consul1.4 # 指定容器的镜像文件
ports: # 配置容器与宿主机的端口
- "8550:8500"
networks: ## 引入外部预先定义的网段
swoft_consul:
ipv4_address: 172.200.7.50 #设置ip地址
container_name: consul_client_172_50 # 这是容器的名称
command: ./consul agent -data-dir /tmp/consul -node=consul_client_172_50 -bind=172.200.7.50 -ui -client=0.0.0.0 -join 172.200.7.30
consul_client_172_40: # 这个表示服务的名称,课自定义; 注意不是容器名称
image: consul1.4 # 指定容器的镜像文件
ports: # 配置容器与宿主机的端口
- "8540:8500"
networks: ## 引入外部预先定义的网段
swoft_consul:
ipv4_address: 172.200.7.40 #设置ip地址
container_name: consul_client_172_40 # 这是容器的名称
command: ./consul agent -data-dir /tmp/consul -node=consul_client_172_40 -bind=172.200.7.40 -ui -client=0.0.0.0 -join 172.200.7.30
# 设置网络模块
networks: # 自定义网络
swoft_consul:
driver: bridge
ipam: #定义网段
config:
- subnet: "172.200.7.0/24"
到此,一主二从二客户端的consul高可用集群搭建完毕,如下所示:
构建consul部署架构理解
集群介绍
Consul 是一个分布式的解决方案,可以部署多个 Consul 实例,确保数据中心的持续稳定,在 Consul 集群中,内部采用投票的方式选举出 leader,然后才开始运 行整个集群,只有正确选举出 leader 后,集群才开始工作,当一个服务注册到 Consul 后,集群将该服务进行同步,确保 Consul 集群内的每个节点都存储了该服 务的信息;然后,Consul 集群将对该服务进行健康检查和投票,超过半数通过,即认为该服务为正常(或者异常);一旦被投票认定为异常的服务,该服务将不 会被外部发现(不可访问),在此过程中,Consul 将持续的对该异常的服务进行检查,一旦服务恢复,Consul 即刻将其加入正常服务。
server与client
Consul 支持两种运行的方式,即 server 和 client 模式,当一个 Consul 节点以 server 模式运行的时候,就表示该 Consul 节点会存储服务和配置等相关信息,并且 参与到健康检查、leader 选举等服务器事务中,与之相反的是,client 模式不会存储服务信息。
数据中心
每个Consul节点都需要加入一个命名的数据中心(DataCenter) , -个节点上,可以运行多个 数据中心,数据中心的作用在于应用隔离,相当于服务分组。可以简单理解 为, -个数据中心 域为一个二层联通的子网。
构建consul集群架构
下面这张图来源于 Consul 官网,很好的解释了 Consul 的工作原理,先大致看一下:
首先 Consul 支持多数据中心,在上图中有两个 DataCenter,他们通过 Internet 互联,同时请注意为了提高通信效率,只有 Server 节点才加入跨数据中心的通信。
在单个数据中心中,Consul 分为 Client 和 Server 两种节点(所有的节点也被称为 Agent),Server 节点保存数据,Client 负责健康检查及转发数据请求到 Server。
Server 节点有一个 Leader 和多个 Follower,Leader 节点会将数据同步到 Follower,Server 的数量推荐是 3 个或者 5 个,在 Leader 挂掉的时候会启动选举机制产生 一个新的 Leader。
集群内的 Consul 节点通过 gossip 协议(流言协议)维护成员关系,也就是说某个节点了解集群内现在还有哪些节点,这些节点是 Client 还是 Server。
单个数据中心的流言协议同时使用 TCP 和 UDP 通信,并且都使用 8301 端口。跨数据中心的流言协议也同时使用 TCP 和 UDP 通信,端口使用 8302。
集群内数据的读写请求既可以直接发到 Server,也可以通过 Client 使用 RPC 转发到 Server,请求最终会到达 Leader 节点。
在允许数据轻微陈旧的情况下,读请求也可以在普通的 Server 节点完成,集群内数据的读写和复制都是通过 TCP 的 8300 端口完成。
consul 服务发现原理
服务发现的完整流程,先大致看一下:
-
首先需要有一个正常的 Consul 集群,有 Server,有 Leader。这里在服务器 Server1、Server2、Server3 上分别部署了 Consul Server。
-
假设他们选举了 Server2 上的 Consul Server 节点为 Leader。
-
然后在服务器 Server4 和 Server5 上通过 Consul Client 分别注册 Service A、B、C,这里每个 Service 分别部署在了两个服务器上,这样可以避免 Service 的单点问 题。
-
服务注册到 Consul 可以通过 HTTP API(8500 端口)的方式,也可以通过 Consul 配置文件的方式。
-
Consul Client 将注册信息通过 RPC 转发到 Consul Server,服务信息保存在 Server 的各个节点中,并且通过 Raft 实现了强一致性。
-
最后在服务器 Server6 中 Program D 需要访问 Service B,这时候 Program D 首先访问本机 Consul Client 提供的 HTTP API,本机 Client 会将请求转发到 Consul Server。
-
Consul Server 查询到 Service B 当前的信息返回,最终 Program D 拿到了 Service B 的所有部署的 IP 和端口,然后就可以选择 Service B 的其中一个部署并向其发 起请求了。
将商品服务自动注册和服务发现
拆分说明
在之前有讲过,关于商品这一块我们会拆分为两个大的方面client与server;
为什么这么做呢?
client同比与传统框架为controller,而server则为业务+数据库处理;如果说是简单的微服务我们可以两者合一,但是如果说考虑到后期的扩展和维护则可以需要做 好拆分,client会负责请求的处理。
client与server之间采用 rpc通信
访问流程 用户-》nginx(负载)-》client(通过 consul 发现server) -》rpc-》server
client采用httpServer
server采用rpcServer
开发前的准备
首先就是需要注意consul是放在与public的服务器中的,而goods则是放在goods服务器上;考虑到后期的发展以及需要的系统容器增多所以目录结构也有调整
首先第一步构建的是goods-server
理解swoft的服务注册与发送功能的运用
https://www.swoft.org/documents/v2/microservice/register-find/
在官方的手册中有描述关于服务注册与发现的案例,并且采用的是consul并且也提供了相应的案例
快速上手
服务注册
我们可以先基于官方案例来完成注册与注销
首先我们需要修改 bean.php 的配置信息
<?php
return [
// ...
'consul' => [
'host' => '192.168.169.110',
'port' => '8540'
]
];
?>
注意这里我们选择的是consul的client
app\server\swoft\app\Listener\RegisterServiceListener.php
<?php
/**
* Class RegisterServiceListener
* @since 2.0
* @Listener(event=SwooleEvent::START)
*/
class RegisterServiceListener implements EventHandlerInterface {
/*** @Inject() ** @var Agent */
private $agent;
/*** @param EventInterface $event */
public function handle(EventInterface $event): void { $rpcServer = $event->getTarget();
$service = [
'ID' => 'swoft_goods_server_173_110',
'Name' => 'swoft_goods_server',
'Tags' => [
'RPC'
],
'Address' => '192.168.169.120',
'Port' => $rpcServer->getPort(),
'Meta' => [
'version' => '1.0'
],
'EnableTagOverride' => false,
'Weights' => [
'Passing' => 10,
'Warning' => 1
]
];
// Register
$this->agent->registerService($service);
echo "Swoft http register service success by consul!\n";
}
}
?>
docker-composer.yml
# 编排php,redis,nginx容器
version: "3.6" # 确定docker-composer文件的版本
services: # 代表就是一组服务 - 简单来说一组容器 swoft_goods_server_173_110: # 这个表示服务的名称,课自定义; 注意不是容器名称
image: swoft # 指定容器的镜像文件
container_name: swoft_goods_server_173_110 # 这是容器的名称
networks: ## 引入外部预先定义的网段
app_swoft:
ipv4_address: 173.200.7.110 #设置ip地址
volumes: # 配置数据挂载 - /www/wwwroot/2007_SRM/app/server/swoft:/www/swoft
working_dir: /www/swoft #工作目录
command: php bin/swoft rpc:start # 设置网络模块
networks: # 自定义网络
app_swoft:
driver: bridge
ipam: #定义网段
config:
- subnet: "173.200.7.0/24"
服务注销
服务启动注册服务,服务关闭或者退出则需要取消服务注册,此时这里和注册一样监听一个 SwooleEvent::SHUTDOWN 事件即可,本章这里还是以 取消 Http server 服务为例:
app\server\swoft\app\Listener\DeregisterServiceListener.php
<?php declare(strict_types=1);
/**
* Class DeregisterServiceListener
* @since 2.0
* @Listener(SwooleEvent::SHUTDOWN)
*/
class DeregisterServiceListener implements EventHandlerInterface {
/**
* @Inject()
*
* @var Agent
*/
private $agent;
public function handle(EventInterface $event): void {
$httpServer = $event->getTarget();
$this->agent->deregisterService('swoft_goods_server_173_110');
echo "Swoft rpc register service deregisterService\n";;
}
}
?>
演示就跳过了....
理解运行
在监听中,主要是通过 Swoft\Consul\Agent 实现服务的注册和注销的功能
同时利用在对象上增加的 @Listener(SwooleEvent::SHUTDOWN) 指定运行的周期地址;
回顾swoole声明周期简化版:
通过new 创建服务 -》进入start事件初始化-》managerStar -》worker/task start -》往后交互都在中间的监听如(request,message,receive) 等事件 -》 worker/task stop-》managerStop-》结束swoole进程Shutdown
而swoft的事件监听就是在这些节点增加的事件监听方法;通过 SHUTDOWN确定执行节点;
关于方法的封装主要是在 app\server\swoft\vendor\swoft\consul\src\Consul.php
与 app\server\swoft\vendor\swoft\consul\src\Agent.php
其中注释:@Bean() 当一个对象中标有这个注解代表在启动的时候会注册到容器中,然后使用的时候我们无需手动的new 只需要在属性上运用@Inject()注解可以创 建实例;
注意注意!!! 注 @Inject()注解是需要配合@var一起运用负责报错;
在 app\server\swoft\vendor\swoft\consul\src\Consul.php
发送数据实际底层是运用了协程客户端处理的