SpringCloud
认识微服务
- 随着互联网行业的发展,对服务的要求也越来越高,服务架构也从单体架构逐渐演变为现在流行的微服务架构。那这些架构之间有怎样的区别呢?
单体架构
分布式架构
分布式架构优缺点如下
```
优点1
2
3
4
5
6
- 降低服务耦合
- 有利于服务升级和拓展
- ```
缺点- 服务调用关系错综复杂
分布式架构虽然降低了服务耦合,但是服务拆分时也有很多问题需要思考
- 服务拆分的细粒度如何界定?
- 服务之间如何调用?
- 服务的调用关系如何管理?
人们需要指定一套行之有效的标准来约数分布式架构
微服务
- 微服务的架构特征
- 微服务的上述特性其实是在给分布式架构制定一个标准,进一步降低服务之间的耦合度,提供服务的独立性和灵活性。做到高内聚,低耦合
- 因此,可以认为微服务是一种经过良好架构设计的分布式架构方案
- 但方案该怎么落地?选用什么样的技术栈?全球的互联网公司都在积极尝试自己的微服务落地方案
- 其中在Java领域最引人瞩目的就是SpringCloud 提供的方案了
SpringCloud
- SpringCloud 是目前国内使用最广泛的微服务架构。官网地址:https://spring.io/projects/spring-cloud
- SpringCloud 集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验。
- 其中常见的组件包括
- 微服务注册与发现
- Eureka
- Nacos
- Consul
- 服务远程调用
- OpenFeign
- Dubbo
- 服务链路监控
- Zipkin
- Sleuth
- 统一配置管理
- SpringCloudConfig
- Nacos
- 统一网关路由
- SpringCloudGateway
- Zuul
- 流控、降级、保护
- Hystix
- Sentinel
- 微服务注册与发现
- 另外,SpringCloud 底层是依赖于SpringBoot的,并且有版本的兼容关系,如下
Release Train | Boot Version |
---|---|
2020.0.x aka llford | 2.4.x |
Hoxton | 2.2.x,2.3.x (Starting with SR5) |
Greenwich | 2.1.x |
Finchley | 2.0.x |
Edgware | 1.5.x |
Dalston | 1.5.X |
- 本文的学习版本是Hoxton.SR10,因此对应的是SpringBoot版本是2.3.x
总结
- 单体架构:简单方便,高度耦合,扩展性差,适合小型项目。例如:学生管理系统
- 分布式架构:松耦合,扩展性好,但架构复杂,难度大。适合大型互联网项目。例如:京东、淘宝
- 微服务:一种更好的分布式架构方案
- 优点:拆分力度更小、服务更独立、耦合度更低
- 缺点:架构非常复杂,运维、监控、部署难度提高
- SpringCloud 是微服务架构的一站式解决方案,集成了各种优秀的微服务功能组件
服务拆分和远程调用
- 任何分布式架构都离不开服务的拆分,微服务也是一样
服务拆分原则
服务拆分示例
- cloud-demo:父工程,管理依赖
- order-service:订单微服务,负责订单相关业务
- user-service:用户微服务,负责用户相关业务
- 需求
- 订单微服务和用户微服务必须有各自的数据库,相互独立
- 订单服务和用户服务都对外暴露Restful的接口
- 订单服务如果需要查询用户信息,只能调用用户服务的Restful接口,不能查询用户数据库
导入Sql语句
- Order表
- User表
1 | CREATE DATABASE cloud_order; |
导入demo
- 导入黑马提供好的demo,里面包含了
order-service
和user-service
,将其配置文件中的数据库修改为自己的配置,随后将这两个服务启动,开始我们的调用案例
实现远程调用案例
在order-service中的web包下,有一个OrderController,是根据id查询订单的接口
1
2
3
4
5
6
7
8
9
10
11
12
13
public class OrderController {
private OrderService orderService;
public Order queryOrderByUserId( { Long orderId)
// 根据id查询订单并返回
return orderService.queryOrderById(orderId);
}
}我们打开浏览器,访问
,是可以查询到数据的,但此时的user是null
1 | { |
在user-service中的web包下,也有一个UserController,其中包含一个根据id查询用户的接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class UserController {
private UserService userService;
/**
* 路径: /user/110
*
* @param id 用户id
* @return 用户
*/
public User queryById( { Long id)
return userService.queryById(id);
}
}我们打开浏览器,访问
,查询到的数据如下
1 | { |
案例需求
- 修改order-service中的根据id查询订单业务,要求在查询订单的同时,根据订单中包含的userId查询出用户信息,一并返回
- 因此,我们需要在order-service 中向user-service 发起一个http 请求,调用http://localhost:8081/user/{userId} 这个接口。
- 大概步骤如下
- 注册一个RestTemplate 的实例到Spring 容器
- 修改order-service 服务中的OrderService 类中的queryOrderById 方法,根据Order 对象中的userId 查询User
- 将查询到的User 填充到Order 对象,一并返回
注册RestTemplate
首先我们在order-service服务中的OrderApplication启动类中,注册RestTemplate实例
1
2
3
4
5
6
7
8
9
10
11
12
13
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
实现远程调用
修改order-service服务中的queryById方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class OrderService {
private OrderMapper orderMapper;
private RestTemplate restTemplate;
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
// 2. 远程查询User
// 2.1 url地址,这里的url是写死的,后面会改进
String url = "http://localhost:8081/user/" + order.getUserId();
// 2.2 发起调用
User user = restTemplate.getForObject(url, User.class);
// 3. 存入order
order.setUser(user);
// 4.返回
return order;
}
}再次访问http://localhost:8080/order/101, 这次就能看到User数据了
1
2
3
4
5
6
7
8
9
10
11
12{
"id": 101,
"price": 699900,
"name": "Apple 苹果 iPhone 12 ",
"num": 1,
"userId": 1,
"user": {
"id": 1,
"username": "柳岩",
"address": "湖南省衡阳市"
}
}
提供者与消费者
- 在服务调用关系中,会有两个不同的角色
服务提供者
:一次业务中,被其他微服务调用的服务(提供接口给其他微服务)服务消费者
:一次业务中,调用其他微服务的服务(调用其他微服务提供的接口)
- 但是,服务提供者与服务消费者的角色并不是绝对的,而是相对于业务而言
- 如果服务A调用了服务B,而服务B又调用的服务C,那么服务B的角色是什么?
- 对于A调用B的业务而言:A是服务消费者,B是服务提供者
- 对于B调用C的业务而言:B是服务消费者,C是服务提供者
- 因此服务B既可以是服务提供者,也可以是服务消费者
Eureka注册中心
- 假如我们的服务提供者user-service提供了三个实例,占用的分别是8081、8082、8083端口
- 那我们来思考几个问题
问题一
:order-service在发起远程调用的时候,该如何得知user-service实例的ip地址和端口?问题二
:有多个user-service实例地址,order-service调用时,该如何选择?问题三
:order-service如何得知某个user-service实例是否健康,是不是已经宕机?
Eureka的结构和作用
那么现在来回答之前的各个问题
```
问题一1
2
3
4
5
6
7
8
9
10
:order-service如何得知user-service实例地址?
- 获取地址信息流程如下
1. user-service服务实例启动后,将自己的信息注册到eureka-server(Eureka服务端),这个叫服务注册
2. eureka-server保存服务名称到服务实例地址列表的映射关系
3. order-service根据服务名称,拉取实例地址列表,这个叫服务发现或服务拉取
- ```
问题二:order-service如何从多个user-service实例中选择具体的实例?
- order-service从实例列表中利用负载均衡算法选中一个实例地址
- 向该实例地址发起远程调用
```
问题三1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
:order-service如何得知某个user-service实例是否依然健康,是不是已经宕机?
- user-service会每隔一段时间(默认30秒)向eureka-server发起请求,报告自己的状态,成为心跳
- 当超过一定时间没有发送心跳时,eureka-server会认为微服务实例故障,将该实例从服务列表中剔除
- order-service拉取服务时,就能将该故障实例排除了
注意:一个微服务,即可以是服务提供者,又可以是服务消费者,因此eureka将服务注册、服务发现等功能统一封装到了eureka-client端
- 因此,我们接下来动手实践的步骤包括
1. 搭建注册中心
- 搭建EurekaServer
2. 服务注册
- 将user-service、order-service都注册到eureka
3. 服务发现
- 在order-service中完成服务拉取,然后通过负载均衡挑选一个服务,实现远程调用
## 搭建eureka-server
- 首先我们注册中心服务端:eureka,这必须是一个独立的微服务
### 创建eureka-server服务
- 在cloud-demo父工程下,创建一个子模块,这里就直接创建一个maven项目就好了,然后填写服务信息
### 引入eureka依赖
- 引入SpringCloud为eureka提供的starter依赖:
```XML
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
编写启动类
给eureka-server服务编写一个启动类,一定要添加一个@EnableEurekaServer注解,开启eureka的注册中心功能
1
2
3
4
5
6
7
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class);
}
}
编写配置文件
编写一个application.yml文件,内容如下
为什么也需要配置eureka的服务名称呢?
eureka也会将自己注册为一个服务
1
2
3
4
5
6
7
8
9server:
port: 10086 # 服务端口
spring:
application:
name: eureka-server # eureka的服务名称
eureka:
client:
service-url: # eureka的地址信息
defaultZone: http://127.0.0.1:10086/eureka
启动服务
启动微服务,然后在浏览器访问 http://localhost:10086/, 看到如下结果就是成功了
从图中我们也可以看出eureka确实是将自己注册为了一个服务,这里的
Kyle
是主机名,也就是127.0.0.1UP (1) - Kyle:eureka-server:10086
服务注册
- 下面,我们将user-service注册到eureka-server中去
引入依赖
在user-service的pom.xml文件中,引入下面的eureka-client依赖
1
2
3
4
5<!-- eureka-client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
配置文件
在user-service中,修改application.yml文件,添加服务名称、eureka地址
1
2
3
4
5
6
7spring:
application:
name: order-service
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
启动多个user-service实例
- 为了演示一个服务有多个实例的场景,我们添加一个SpringBoot的启动配置,再启动一个user-service,其操作步骤就是复制一份user-service的配置,name配置为UserApplication2,同时也要配合VM选项,修改端口号
-Dserver.port=8082
,点击确定之后,在IDEA的服务选项卡中,就会出现两个user-service启动配置,一个端口是8081,一个端口是8082 - 之后我们按照相同的方法配置order-service,并将两个user-service和一个order-service都启动,然后查看eureka-server管理页面,发现服务确实都启动了,而且user-service有两个
服务发现
- 下面,我们将order-service的逻辑修改:向eureka-server拉取user-service的信息,实现服务发现
引入依赖
之前说过,服务发现、服务注册统一都封装在eureka-client依赖,因此这一步与服务注册时一致
在order-service的pom.xml文件中,引入eureka-client依赖
1
2
3
4<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
配置文件
服务发现也需要知道eureka地址,因此第二步与服务注册一致,都是配置eureka信息
在order-service中,修改application.yml文件,添加服务名称、eureka地址
1
2
3
4
5
6
7spring:
application:
name: orderservice
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
服务拉取和负载均衡
最后,我们要去eureka-server中拉取user-service服务的实例列表,并实现负载均衡
不过这些操作并不需要我们来做,是需要添加一些注解即可
在order-service的OrderApplication中,给RestTemplate这个Bean添加一个
1
注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
public RestTemplate restTemplate() {
return new RestTemplate();
}
}修改order-service服务中的OrderService类中的queryOrderById方法,修改访问路径,用服务名来代替ip、端口
1
2
3
4
5
6
7
8
9
10
11
12
13public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
// 2. 远程查询User
// 2.1 url地址,用user-service替换了localhost:8081
String url = "http://user-service/user/" + order.getUserId();
// 2.2 发起调用
User user = restTemplate.getForObject(url, User.class);
// 3. 存入order
order.setUser(user);
// 4.返回
return order;
}Spring会自动帮我们从eureka-server端,根据user-service这个服务名称,获取实例列表,然后完成负载均衡
小结
- 搭建EurekaServer
- 引入eureka-server依赖
- 添加@EnableEurekaServer注解
- 在application.yml中配置eureka地址
- 服务注册
- 引入eureka-client依赖
- 在application.yml中配置eureka地址
- 服务发现
- 引入eureka-client依赖
- 在application.yml中配置eureka地址
- 在RestTemplate添加
@LoadBalanced
注解 - 用服务提供者的服务名称远程调用
Ribbon负载均衡
- 在这个小节,我们来说明@LoadBalanced注解是怎么实现的负载均衡功能
负载均衡原理
- SpringCloud底层其实是利用了一个名为Ribbon的组件,来实现负载均衡功能的
- 那么我们明明发出的请求是http://userservice/user/1, 怎么变成了http://localhost:8080/user/1 的呢
源码跟踪
- 为什么我们只输入了service名称就可以访问了呢?之前还得获取ip和端口
- 答案显然是有人帮我们根据service名称,获取到了服务实例的ip和端口。它就是LoadBalancerInterceptor,这个类会第RestTemplate的请求进行拦截,然后从Eureka根据服务id获取服务列表,随后利用负载均衡算法,得到真实的服务地址信息,替换服务id
- 那下面我们来进行源码跟踪
LoadBalancerInterceptor
代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
private LoadBalancerClient loadBalancer;
private LoadBalancerRequestFactory requestFactory;
public LoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerRequestFactory requestFactory) {
this.loadBalancer = loadBalancer;
this.requestFactory = requestFactory;
}
public LoadBalancerInterceptor(LoadBalancerClient loadBalancer) {
this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer));
}
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException {
URI originalUri = request.getURI();
String serviceName = originalUri.getHost();
Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
return (ClientHttpResponse)this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution));
}
}可以看到这里的intercept方法,拦截了用户的HTTPRequest请求,然后做了几件事
- request.getURI():获取请求uri,本利中就是http://user-service/user/1
- originalUri.getHost():获取uri路径的主机名,其实就是服务id,user-service
- this.loadBalancer.execute:处理服务id和用户请求
这里的this.loadBalancer是LoadBalancerClient类型,我们继续跟入
LoadBalancerClient
继续跟入execute方法
1
2
3
4
5
6
7
8
9
10public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint) throws IOException {
ILoadBalancer loadBalancer = this.getLoadBalancer(serviceId);
Server server = this.getServer(loadBalancer, hint);
if (server == null) {
throw new IllegalStateException("No instances available for " + serviceId);
} else {
RibbonServer ribbonServer = new RibbonServer(serviceId, server, this.isSecure(server, serviceId), this.serverIntrospector(serviceId).getMetadata(server));
return this.execute(serviceId, (ServiceInstance)ribbonServer, (LoadBalancerRequest)request);
}
}代码是这样的
- getLoadBalancer(serviceId):根据服务id获取ILoadBalancer,而ILoadBalancer会拿着服务id去eureka中获取服务列表并保存起来
- getServer(loadBalancer, hint):利用内置的负载均衡算法,从服务列表中选择一个,本例中,可以看到获取到的是8081端口
负载均衡策略IRule
在刚才的代码中,可以看到获取服务是通过一个getServer的方法来做负载均衡,我们继续跟入,会发现这样一段代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public Server chooseServer(Object key) {
if (this.counter == null) {
this.counter = this.createCounter();
}
this.counter.increment();
if (this.rule == null) {
return null;
} else {
try {
return this.rule.choose(key);
} catch (Exception var3) {
logger.warn("LoadBalancer [{}]: Error choosing server for key {}", new Object[]{this.name, key, var3});
return null;
}
}
}这里的rule默认值是一个RoundRobinRule,也就是轮询
那么到这里,整个负载均衡的流程我们就清楚了
总结
- SpringCloudRibbon的底层采用了一个拦截器,拦截了RestTemplate发出的请求,对地址做了修改,用一幅图来总结一下
- 整个流程如下
- 拦截我们的RestTemplate请求:http://user-service/user/1
- RibbonLoadBalancerClient会从请求url中获取服务名称,也就是user-service
- DynamicServerListLoadBalancer根据user-service到eureka拉取服务列表
- eureka返回列表,localhost:8081、localhost:8082
- IRule利用内置负载均衡规则,从列表中选择一个,例如localhost:8081
- RibbonLoadBalancerClient修改请求地址,用localhost:8081替代user-service,得到http://localhost:8081/user/1, 发起真实请求
- SpringCloudRibbon的底层采用了一个拦截器,拦截了RestTemplate发出的请求,对地址做了修改,用一幅图来总结一下
负载均衡策略
负载均衡策略
内置负载均衡规则类 | 规则描述 |
---|---|
RoundRobinRule | 简单轮询服务列表来选择服务器。它是Ribbon默认的负载均衡规则。 |
AvailabilityFilteringRule | 对以下两种服务器进行忽略: (1)在默认情况下,这台服务器如果3次连接失败,这台服务器就会被设置为“短路”状态。短路状态将持续30秒,如果再次连接失败,短路的持续时间就会几何级地增加。 (2)并发数过高的服务器。如果一个服务器的并发连接数过高,配置了AvailabilityFilteringRule规则的客户端也会将其忽略。并发连接数的上限,可以由客户端的..ActiveConnectionsLimit属性进行配置。 |
WeightedResponseTimeRule | 为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择服务器,这个权重值会影响服务器的选择。 |
ZoneAvoidanceRule | 以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询。 |
BestAvailableRule | 忽略那些短路的服务器,并选择并发数较低的服务器。 |
RandomRule | 随机选择一个可用的服务器。 |
RetryRule | 重试机制的选择逻辑 |
- 默认的实现就是ZoneAvoidanceRule,是一种轮询方案
自定义负载均衡策略
通过定义IRule实现,可以修改负载均衡规则,有两种方式
代码方式:在order-service中的OrderApplication类中,定义一个IRule,此种方式定义的负载均衡规则,对所有微服务均有效
1
2
3
4
public IRule randomRule(){
return new RandomRule();
}配置文件方式:在order-service中的application.yml文件中,添加新的配置也可以修改规则
1
2
3user-service: # 给某个微服务配置负载均衡规则,这里是user-service服务
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则注意:一般使用没人的负载均衡规则,不做修改
饥饿加载
Ribbon默认是采用懒加载,即第一次访问时,才回去创建LoadBalanceClient,请求时间会很长
而饥饿加载在则会在项目启动时创建,降低第一次访问的耗时,通过下面配置开启饥饿加载
1
2
3
4ribbon:
eager-load:
enabled: true # 开启饥饿加载
clients: user-service # 指定对user-service这个服务进行饥饿加载,可以指定多个服务
小结
Ribbon负载均衡规则
- 规则接口是IRule
- 默认实现是ZoneAvoidanceRule,根据zone选择服务列表,然后轮询
负载均衡自定义方式
- 代码方式:配置灵活,但修改时需要重新打包发布
- 配置方式:直观,方便,无需重新打包发布,但是无法做全局配置(只能指定某一个微服务)
饥饿加载
开启饥饿加载
1
enable: true
指定饥饿加载的微服务名称,可以配置多个
1
2
3clients:
- user-service
- xxx-service
Nacos注册中心
- 国内公司一般都推崇阿里巴巴的技术,比如注册中心,
SpringCloud Alibaba
也推出了一个名为Nacos
的注册中心
认识和安装Nacos
Nacos是阿里巴巴的产品,现在是SpringCloud中的一个组件,相比于Eureka,功能更加丰富,在国内受欢迎程度较高
在Nacos的GitHub页面,提供有下载链接,可以下载编译好的Nacos服务端或者源代码:
- GitHub主页:https://github.com/alibaba/nacos
- GitHub的Release下载页:https://github.com/alibaba/nacos/releases
下载好了之后,将文件解压到非中文路径下的任意目录,目录说明:
- bin:启动脚本
- conf:配置文件
Nacos的默认端口是8848,如果你电脑上的其它进程占用了8848端口,请先尝试关闭该进程。
- 如果无法关闭占用8848端口的进程,也可以进入nacos的conf目录,修改配置文件application.properties中的server.port
Nacos的启动非常简单,进入bin目录,打开cmd窗口执行以下命令即可
1
startup.cmd -m standalone
之后在浏览器访问http://localhost:8848/nacos 即可,默认的登录账号和密码都是nacos
服务注册到Nacos
- Nacos是SpringCloudAlibaba的组件,而
SpringCloud Alibaba
也遵循SpringCloud中定义的服务注册、服务发现规范。因此使用Nacos与使用Eureka对于微服务来说,并没有太大区别 - 主要差异在于
- 依赖不同
- 服务地址不同
引入依赖
在cloud-demo父工程的pom.xml文件中引入SpringCloudAlibaba的依赖
1
2
3
4
5
6
7<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.6.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>然后在user-service和order-service中的pom文件引入nacos-discovery依赖
1
2
3
4<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>注意:同时也要将eureka的依赖注释/删除掉
配置Nacos地址
在user-service和order-service的application.yml中添加Nacos地址
1
2
3
4spring:
cloud:
nacos:
server-addr: localhost:8848注意:同时也要将eureka的地址注释掉
重启服务
服务分级存储模型
- 一个服务可以有多个实例,例如我们的user-service,可以有
- 127.0.0.1:8081
- 127.0.0.1:8082
- 127.0.0.1:8083
- 假如这些实例分布于全国各地的不同机房,例如
- 127.0.0.1:8081,在杭州机房
- 127.0.0.1:8082,在杭州机房
- 127.0.0.1:8083,在上海机房
- Nacod就将在同一机房的实例,划分为一个
集群
- 也就是说,user-service是服务,一个服务可以包含多个集群,例如在杭州,上海,每个集群下可以有多个实例,形成分级模型
- 微服务相互访问时,应该尽可能访问同集群实例,因为本地访问速度更快,房本集群内不可用时,才去访问其他集群
- 例如:杭州机房内的order-service应该有限访问同机房的user-service,若无法访问,则去访问上海机房的user-service
给user-service配置集群
修改user-service的application.yml文件,添加集群配置
1
2
3
4
5
6spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HZ # 集群名称,杭州重启两个user-service实例
之后我们再复制一个user-service的启动配置,端口号设为8083,之后修改application.yml文件,将集群名称设为上海,之后启动该服务
1
2
3
4
5
6spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: SH # 集群名称,上海那么我们现在就启动了两个集群名称为HZ的user-service,一个集群名称为SH的user-service,在Nacos控制台看到如下结果
Nacos服务分级存储模型
- 一级是服务,例如user-service
- 二级是集群,例如杭州或上海
- 三级是实例,例如杭州机房的某台部署了user-service的服务器
如何设置实例的集群属性
- 修改application.yml文件,添加spring.cloud.nacos.discovery.cluster-name属性即可
同集群优先的负载均衡
默认的ZoneAvoidanceRule并不能根据同集群优先来实现负载均衡
因此Nacos中提供了一个NacosRule的实现,可以优先从同集群中挑选实例
给order-service配置集群信息,修改其application.yml文件,将集群名称配置为HZ
1
2
3
4
5
6spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HZ # 集群名称,杭州修改负载均衡规则
1
2
3user-service: # 给某个微服务配置负载均衡规则,这里是user-service服务
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 负载均衡规则
那我们现在访问http://localhost:8080/order/101 ,同时观察三个user-service的日志输出,集群名称为HZ的两个user-service可以看到日志输出,而集群名称为SH的user-service则看不到日志输出
那我们现在将集群名称为HZ的两个user-service服务停掉,那么现在访问http://localhost:8080/order/101, 则集群名称为SH的user-service会输出日志
NacosRule负载均衡策略
- 优先选择统计群服务实例列表
- 本地鸡群找不到提供者,才去其他集群寻找,并且会报警告
- 确定了可用实例列表后,再采用随机负载均衡挑选实例
权重配置
实际部署中肯定会出现这样的场景
- 服务器设备性能由沙溢,部分实例所在的机器性能较好,而另一些较差,我么你希望性能好的机器承担更多的用户请求
- 但默认情况下NacosRule是统计群内随机挑选,不会考虑机器性能的问题
因此Nacos提供了权重配置来控制访问频率,权重越大则访问频率越高
在Nacos控制台,找到user-service的实例列表,点击编辑,即可以修改权重
注意:若权重修改为0,则该实例永远不会被访问
我们可以将某个服务的权重修改为0,然后进行更新,然后也不会影响到用户的正常访问别的服务集群,之后我们可以给更新后的该服务,设置一个很小的权重,这样就会有一小部分用户来访问该服务,测试该服务是否稳定(类似于灰度测试)
环境隔离
- Nacos提供了namespace来实现环境隔离功能
- nacos中可以有多个namespace
- namespace下可以由group、service等
- 不同的namespace之间相互隔离,例如不同的namespace的服务互相不可见
创建namespace
给微服务配置namespace
给微服务配置namespace只能通过修改配置来实现
例如,修改order-service的application.yml文件
1
2
3
4
5
6
7spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HZ
namespace: ea980a8c-c886-4a2c-8653-d29c62d518bb # 命名空间,填上图中的命名空间ID重启order-service后,访问Nacos控制台,可以看到下面的结果,此时访问order-service,因为namespace不同,会导致找不到user-service,若访问http://localhost:8080/order/101 则会报错
Nacos和Eureka的区别
Nacos的服务实例可以分为两种类型
- 临时实例:如果实例宕机超过一定时间,会从服务列表剔除,默认的类型
- 非临时实例:如果实例宕机,不会从服务列表剔除,也可以叫永久实例
配置一个服务实例为永久实例
1
2
3
4
5spring:
cloud:
nacos:
discovery:
ephemeral: false # 设置为非临时实例Nacos与Eureka的共同点
- 都支持服务注册和服务拉取
- 都支持服务提供者心跳方式做健康监测
Nacos与Eureka的区别
- Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式(但是对服务器压力比较大,不推荐)
- 临时实例心跳不正常会被剔除,非临时实例则不会被剔除
- Nacos支持服务列表变更的消息推送模式,服务列表更新更及时
- Nacos集群默认采用AP方式,当急群众存在非临时实例时,采用CP模式;Eureka采用AP方式
Nacos配置管理
- Nacos除了可以做注册中心,同样还可以做配置管理来使用
统一配置管理
- 当微服务部署的实例越来越多,达到数十、数百时,诸葛修改微服务配置就会让人抓狂,而且容易出错,所以我们需要一种统一配置管理方案,可以集中管理所有实例的配置
- Nacos一方面可以将配置集中管理,另一方面可以在配置变更时,及时通知微服务,实现配置的热更新
在Nacos中添加配置文件
如何在Nacos中管理配置呢
配置列表
->点击右侧加号
在弹出的表单中,填写配置信息
1
2pattern:
dateformat: yyyy-MM-dd HH:mm:ss注意:只有需要热更新的配置才有放到Nacos管理的必要,基本不会变更的一些配置,还是保存到微服务本地比较好(例如数据库连接配置等)
从微服务拉取配置
微服务要拉取Nacos中管理的配置,并且与本地的application.yml配置合并,才能完成项目启动
但如果上位读取application.yml,又如何得知Nacos地址呢?
Spring引入了一种新的配置文件:bootstrap.yml文件,会在application.yml之前被读取,流程如下
- 项目启动
- 加载bootstrap.yml文件,获取Nacos地址,配置文件id
- 根据配置文件id,读取Nacos中的配置文件
- 读取本地配置文件application.yml,与Nacos拉取到的配置合并
- 创建Spring容器
- 加载bean
引入nacos-config依赖
首先在user-service服务中,引入nacos-config的客户端依赖
1
2
3
4
5<!--nacos配置管理依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
添加bootstrap.yml
然后在user-service中添加一个bootstrap.yml文件,内容如下
1
2
3
4
5
6
7
8
9
10spring:
application:
name: user-service # 服务名称
profiles:
active: dev #开发环境,这里是dev
cloud:
nacos:
server-addr: localhost:8848 # Nacos地址
config:
file-extension: yaml # 文件后缀名
这里会根据spring.cloud.nacos.server-addr获取Nacos地址,再根据
${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
作为文件id,来读取配置。在本例中,就是读取user-service-dev.yaml
测试是否真的读取到了,我们在user-service的UserController中添加业务逻辑,读取nacos中的配置信息pattern.dateformat配置
1
2
3
4
5
6
7
private String dateformat;
public String test() {
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
}打开浏览器,访问http://localhost:8081/user/test, 看到如下结果,则说明确实读取到了配置信息
配置热更新
- 我们最终的目的,是修改Nacos中的配置后,微服务中无需重启即可让配置生效,也就是配置热更新
- 要实现配置热更新,可以使用两种方式
方式一
在@Value注入的变量类上添加注解@RefreshScope(刷新作用域)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class UserController {
private String dateformat;
public String test() {
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
}
}测试是否热更新
启动服务,打开浏览器,访问http://localhost:8081/user/test, 由于我们之前配置的dateformat是yyyy-MM-dd MM:hh:ss,所以看到的日期格式为
2023-5-12 22:11:03
那我们现在直接在Nacos中编辑配置信息,并保存
1
2pattern:
dateformat: yyyy年MM月dd日 HH:mm:ss无需重启服务器,直接刷新页面,看到的日期格式为
2023年5月12日 22:16:13
,说明确实是热更新
方式二
使用@ConfigurationProperties注解代替
@Value
注解在user-service服务中,添加一个类,读取
1
pattern.dateformat
属性
1
2
3
4
5
6
public class PatternProperties {
private String dateformat;
}在UserController中用这个类来代替
1
1
2
3
4
5
6
7
8
9
10
11
12
13
public class UserController {
private PatternProperties patternProperties;
public String test() {
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(patternProperties.getDateformat()));
}
}使用同样的方法进行测试,这里就不赘述了
配置共享
- 其实微服务启动时,回去Nacos读取多个配置文件,例如
[spring.application.name]-[spring.profiles.active].yaml
,例如:user-service-dev.yaml[spring.application.name].yaml
,例如:userservice.yaml
- 而
[spring.application.name].yaml
不包含环境,因此可以被多个环境共享 - 那下面我们通过案例来测试配置共享
添加一个环境共享配置
我们在Nacos中添加一个
1
Data ID
为
1
user-service.yml
文件,编写的配置内容如下
1
2pattern:
envSharedValue: 多环境共享属性值修改user-service-dev.yml文件
1
2
3pattern:
dateformat: yyyy/MM/dd HH:mm:ss
env: user-service开发环境配置
在user-service中读取共享配置
修改我们的PatternProperties类,添加envSharedValue和env属性
1
2
3
4
5
6
7
8
public class PatternProperties {
private String dateformat;
private String envSharedValue;
private String env;
}同时修改UserController,添加一个方法
1
2
3
4
5
6
7
8
9
10
11
12
13
public class UserController {
private PatternProperties patternProperties;
public PatternProperties prop(){
return patternProperties;
}
}修改UserApplication2的启动项,改变其profile值为test(改变环境),同时新建一个user-service-test.yml配置
1
2
3pattern:
dateformat: yyyy-MM-dd HH:mm:ss
env: user-service测试环境配置那现在,我们的UserApplication加载的是user-service-dev.yml和user-service.yml这两个配置文件
我们的UserApplication2加载的是user-service-test.yml和user-service.yml这两个配置文件
启动这两个服务,打开浏览器分别访问
和
http://localhost:8082/user/prop,看到的结果如下
- UserApplication(dev环境)
UserApplication2(test环境)
1
2
3
4
5{
"dateformat": "yyyy/MM/dd HH:mm:ss",
"envSharedValue": "多环境共享属性值",
"env": "user-service开发环境配置"
}
- 可以看出,不管是dev还是test环境,都读取到了envSharedValue这个属性的值,且dev和test也都有自己特有的属性值
配置共享的优先级
- 当Nacos、服务笨蛋同时出现相同属性时,优先级也有高低之分
- 服务名-profile.yaml > 服务名.yaml > 本地配置
- user-service-dev.yaml > user-service.yaml > application.yaml
搭建Nacos集群
集群结构图
Nacos生产环境下一定要部署为集群状态
其中包含3个Nacos节点,然后一个负载均衡器代理3个Nacos。这里的负载均衡器可以使用Nginx,关于Nginx的基本使用,我在前面的一篇文章也做过介绍
瑞吉外卖项目优化
https://w-zx.cc/posts/ruiji02.html
节点 | ip | port |
---|---|---|
nacos1 | 192.168.150.1 | 8845 |
nacos2 | 192.168.150.1 | 8846 |
nacos3 | 192.168.150.1 | 8847 |
搭建集群
搭建集群的基本步骤
搭建数据库,初始化数据库表结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200CREATE DATABASE IF NOT EXISTS nacos_config;
USE nacos_config;
CREATE TABLE `config_info` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` VARCHAR(255) NOT NULL COMMENT 'data_id',
`group_id` VARCHAR(255) DEFAULT NULL,
`content` LONGTEXT NOT NULL COMMENT 'content',
`md5` VARCHAR(32) DEFAULT NULL COMMENT 'md5',
`gmt_create` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
`src_user` TEXT COMMENT 'source user',
`src_ip` VARCHAR(50) DEFAULT NULL COMMENT 'source ip',
`app_name` VARCHAR(128) DEFAULT NULL,
`tenant_id` VARCHAR(128) DEFAULT '' COMMENT '租户字段',
`c_desc` VARCHAR(256) DEFAULT NULL,
`c_use` VARCHAR(64) DEFAULT NULL,
`effect` VARCHAR(64) DEFAULT NULL,
`type` VARCHAR(64) DEFAULT NULL,
`c_schema` TEXT,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_info_aggr */
/******************************************/
CREATE TABLE `config_info_aggr` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` VARCHAR(255) NOT NULL COMMENT 'data_id',
`group_id` VARCHAR(255) NOT NULL COMMENT 'group_id',
`datum_id` VARCHAR(255) NOT NULL COMMENT 'datum_id',
`content` LONGTEXT NOT NULL COMMENT '内容',
`gmt_modified` DATETIME NOT NULL COMMENT '修改时间',
`app_name` VARCHAR(128) DEFAULT NULL,
`tenant_id` VARCHAR(128) DEFAULT '' COMMENT '租户字段',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfoaggr_datagrouptenantdatum` (`data_id`,`group_id`,`tenant_id`,`datum_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='增加租户字段';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_info_beta */
/******************************************/
CREATE TABLE `config_info_beta` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` VARCHAR(255) NOT NULL COMMENT 'data_id',
`group_id` VARCHAR(128) NOT NULL COMMENT 'group_id',
`app_name` VARCHAR(128) DEFAULT NULL COMMENT 'app_name',
`content` LONGTEXT NOT NULL COMMENT 'content',
`beta_ips` VARCHAR(1024) DEFAULT NULL COMMENT 'betaIps',
`md5` VARCHAR(32) DEFAULT NULL COMMENT 'md5',
`gmt_create` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
`src_user` TEXT COMMENT 'source user',
`src_ip` VARCHAR(50) DEFAULT NULL COMMENT 'source ip',
`tenant_id` VARCHAR(128) DEFAULT '' COMMENT '租户字段',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfobeta_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_beta';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_info_tag */
/******************************************/
CREATE TABLE `config_info_tag` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` VARCHAR(255) NOT NULL COMMENT 'data_id',
`group_id` VARCHAR(128) NOT NULL COMMENT 'group_id',
`tenant_id` VARCHAR(128) DEFAULT '' COMMENT 'tenant_id',
`tag_id` VARCHAR(128) NOT NULL COMMENT 'tag_id',
`app_name` VARCHAR(128) DEFAULT NULL COMMENT 'app_name',
`content` LONGTEXT NOT NULL COMMENT 'content',
`md5` VARCHAR(32) DEFAULT NULL COMMENT 'md5',
`gmt_create` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
`src_user` TEXT COMMENT 'source user',
`src_ip` VARCHAR(50) DEFAULT NULL COMMENT 'source ip',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfotag_datagrouptenanttag` (`data_id`,`group_id`,`tenant_id`,`tag_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_tag';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_tags_relation */
/******************************************/
CREATE TABLE `config_tags_relation` (
`id` BIGINT(20) NOT NULL COMMENT 'id',
`tag_name` VARCHAR(128) NOT NULL COMMENT 'tag_name',
`tag_type` VARCHAR(64) DEFAULT NULL COMMENT 'tag_type',
`data_id` VARCHAR(255) NOT NULL COMMENT 'data_id',
`group_id` VARCHAR(128) NOT NULL COMMENT 'group_id',
`tenant_id` VARCHAR(128) DEFAULT '' COMMENT 'tenant_id',
`nid` BIGINT(20) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`nid`),
UNIQUE KEY `uk_configtagrelation_configidtag` (`id`,`tag_name`,`tag_type`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_tag_relation';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = group_capacity */
/******************************************/
CREATE TABLE `group_capacity` (
`id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`group_id` VARCHAR(128) NOT NULL DEFAULT '' COMMENT 'Group ID,空字符表示整个集群',
`quota` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
`usage` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '使用量',
`max_size` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
`max_aggr_count` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数,,0表示使用默认值',
`max_aggr_size` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
`max_history_count` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
`gmt_create` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_group_id` (`group_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='集群、各Group容量信息表';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = his_config_info */
/******************************************/
CREATE TABLE `his_config_info` (
`id` BIGINT(64) UNSIGNED NOT NULL,
`nid` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
`data_id` VARCHAR(255) NOT NULL,
`group_id` VARCHAR(128) NOT NULL,
`app_name` VARCHAR(128) DEFAULT NULL COMMENT 'app_name',
`content` LONGTEXT NOT NULL,
`md5` VARCHAR(32) DEFAULT NULL,
`gmt_create` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`gmt_modified` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`src_user` TEXT,
`src_ip` VARCHAR(50) DEFAULT NULL,
`op_type` CHAR(10) DEFAULT NULL,
`tenant_id` VARCHAR(128) DEFAULT '' COMMENT '租户字段',
PRIMARY KEY (`nid`),
KEY `idx_gmt_create` (`gmt_create`),
KEY `idx_gmt_modified` (`gmt_modified`),
KEY `idx_did` (`data_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='多租户改造';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = tenant_capacity */
/******************************************/
CREATE TABLE `tenant_capacity` (
`id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`tenant_id` VARCHAR(128) NOT NULL DEFAULT '' COMMENT 'Tenant ID',
`quota` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
`usage` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '使用量',
`max_size` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
`max_aggr_count` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数',
`max_aggr_size` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
`max_history_count` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
`gmt_create` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_id` (`tenant_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='租户容量信息表';
CREATE TABLE `tenant_info` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`kp` VARCHAR(128) NOT NULL COMMENT 'kp',
`tenant_id` VARCHAR(128) DEFAULT '' COMMENT 'tenant_id',
`tenant_name` VARCHAR(128) DEFAULT '' COMMENT 'tenant_name',
`tenant_desc` VARCHAR(256) DEFAULT NULL COMMENT 'tenant_desc',
`create_source` VARCHAR(32) DEFAULT NULL COMMENT 'create_source',
`gmt_create` BIGINT(20) NOT NULL COMMENT '创建时间',
`gmt_modified` BIGINT(20) NOT NULL COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_info_kptenantid` (`kp`,`tenant_id`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='tenant_info';
CREATE TABLE `users` (
`username` VARCHAR(50) NOT NULL PRIMARY KEY,
`password` VARCHAR(500) NOT NULL,
`enabled` BOOLEAN NOT NULL
);
CREATE TABLE `roles` (
`username` VARCHAR(50) NOT NULL,
`role` VARCHAR(50) NOT NULL,
UNIQUE INDEX `idx_user_role` (`username` ASC, `role` ASC) USING BTREE
);
CREATE TABLE `permissions` (
`role` VARCHAR(50) NOT NULL,
`resource` VARCHAR(255) NOT NULL,
`action` VARCHAR(8) NOT NULL,
UNIQUE INDEX `uk_role_permission` (`role`,`resource`,`action`) USING BTREE
);
INSERT INTO users (username, PASSWORD, enabled) VALUES ('nacos', '$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu', TRUE);
INSERT INTO roles (username, role) VALUES ('nacos', 'ROLE_ADMIN');配置Nacos
我们进入Nacos的conf目录,修改配置文件cluster.conf.example,重命名为cluster.conf,然后添加内容,如果后面启动报错了,就把这里的127.0.0.1换成本机真实IP
1
2
3
4PLAINTEXT
127.0.0.1:8845
127.0.0.1:8846
127.0.0.1:8847然后修改application.properties文件,添加数据库配置
1
2
3
4
5
6
7spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=root
db.password.0=root
启动Nacos集群
将nacos文件夹复制3份,分别命名为:nacos1、nacos2、nacos3
然后分别修改这三个文件夹中的application.properties
nacos1
1
server.port=8845
nacos2
1
server.port=8846
nacos3
1
server.port=8847
Nginx反向代理
修改conf/nginx.conf文件,将下面的配置粘贴到http块中
1
2
3
4
5
6
7
8
9
10
11
12
13
14upstream nacos-cluster {
server 127.0.0.1:8845;
server 127.0.0.1:8846;
server 127.0.0.1:8847;
}
server {
listen 80;
server_name localhost;
location /nacos {
proxy_pass http://nacos-cluster;
}
}启动nginx,然后在浏览器访问http://localhost/nacos 即可
同时将bootstrap.yml中的Nacos地址修改为localhost:80,user-service和order-service中都改
1
2
3
4spring:
cloud:
nacos:
server-addr: localhost:80 # Nacos地址重启服务,在Nacos中可以看到管理的服务
若报错,请将前面的127.0.0.1换成本机ip,例如192.168.1.7这种的
Feign远程调用
先来看看我们以前利用RestTemplate发起远程调用的代码
1
2String url = "http://user-service/user/" + order.getUserId();
User user = restTemplate.getForObject(url, User.class);存在以下问题:
- 代码可读性差,编程体验不统一
- 参数复杂的URL难以维护(百度随便搜一个中文名词,然后看一下url有多长,有多少参数)
我们可以利用Feign来解决上面提到的问题
Feign是一个声明式的http客户端,官网地址https://github.com/OpenFeign/feign, 其作用就是帮助我们优雅的实现http请求的发送
Feign替代RestTemplate
Feign的使用步骤如下
引入依赖
我们在order-service服务的pom文件中引入Feign的依赖
1
2
3
4<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
添加注解
- 在order-service的启动类上添加
@EnableFeignClients
注解,开启Feign的功能
- 在order-service的启动类上添加
编写Feign客户端
在order-service中新建com.itcast.order.client包,然后新建一个接口,内容如下
1
2
3
4
5
public interface UserClient {
User findById(; Long id)
}这个客户端主要是基于SpringMVC的注解来声明远程调用的信息,比如
- 服务名称:user-service
- 请求方式:GET
- 请求路径:/user/{id}
- 请求参数:Long id
- 返回值类型:User
这样,Feign就可以帮助我们发送http请求,无需自己使用RestTemplate来发送了
测试
修改order-service中的OrderService类中的queryOrderById方法,使用Feign客户端代替RestTemplate
- DIFF
- 修改后的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
- @Autowired
- private RestTemplate restTemplate;
+ @Autowired
+ private UserClient userClient;
public Order queryOrderById(Long orderId) {
Order order = orderMapper.findById(orderId);
- String url = "http://user-service/user/" + order.getUserId();
- User user = restTemplate.getForObject(url, User.class);
+ User user = userClient.findById(order.getUserId());
order.setUser(user);
return order;
}
}
总结
- 使用Feign的步骤
- 引入依赖
- 主启动类添加@EnableFeignClients注解
- 编写FeignClient接口
- 使用FeignClient中定义的方法替代RestTemplate
- 使用Feign的步骤
自定义配置
- Feign可以支持很多的自定义配置,如下表所示
类型 | 作用 | 说明 |
---|---|---|
feign.Logger.Level | 修改日志级别 | 包含四种不同的级别:NONE、BASIC、HEADERS、FULL |
feign.codec.Decoder | 响应结果的解析器 | http远程调用的结果做解析,例如解析json字符串为java对象 |
feign.codec.Encoder | 请求参数编码 | 将请求参数编码,便于通过http请求发送 |
feign. Contract | 支持的注解格式 | 默认是SpringMVC的注解 |
feign. Retryer | 失败重试机制 | 请求失败的重试机制,默认是没有,不过会使用Ribbon的重试 |
- 一般情况下,默认值就能满足我们的使用,如果需要自定义,只需要创建自定义的@Bean覆盖默认的Bean即可,下面以日志为例来演示如何自定义配置
配置文件方式
基于配置文件修改Feign的日志级别可以针对单个服务
1
2
3
4
5feign:
client:
config:
userservice: # 针对某个微服务的配置
loggerLevel: FULL # 日志级别也可以针对所有服务
1
2
3
4
5feign:
client:
config:
default: # 这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
loggerLevel: FULL # 日志级别而日志的级别分为四种
- NONE:不记录任何日志信息,这是默认值
- BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
- HEADERS:在BASIC的基础上,额外记录了请求和响应头的信息
- FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据
Java代码方式
也可以基于Java代码修改日志级别,先声明一个类,然后声明一个Logger.Level的对象
1
2
3
4
5
6public class DefaultFeignConfiguration {
public Logger.Level feignLogLevel(){
return Logger.Level.BASIC; //日志级别设置为 BASIC
}
}如果要全局生效,将其放到启动类的@EnableFeignClients这个注解中
1
如果是局部生效,则把它放到对应的@FeignClient注解中
1
Feign使用优化
Feign底层发起http请求,依赖于其他框架,其底层客户端实现包括
- URLConnection:默认实现,不支持连接池
- Apache HttpClient:支持连接池
- OKHttp:支持连接池
因此提高Frign的性能主要手段就是使用连接池,代替默认的URLConnection
这里我们使用Apache的HttpClient来演示
引入依赖
在order-service的pom文件中引入Apache的HttpClient依赖
1
2
3
4
5<!--httpClient的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
配置连接池
在order-service的application.yml中添加配置
1
2
3
4
5
6
7
8
9feign:
client:
config:
default: # default全局的配置
logger-level: BASIC # 日志级别,BASIC就是基本的请求和响应信息
httpclient:
enabled: true # 开启feign对HttpClient的支持
max-connections: 200 # 最大的连接数
max-connections-per-route: 50 # 每个路径的最大连接数
小结,Feign的优化
- 日志级别尽量使用BASIC
- 使用HttpClient或OKHttp代替URLConnection
- 引入feign-httpclient依赖
- 配置文件中开启httpclient功能,设置连接池参数
最佳实践
所谓最佳实践,就是使用过程中总结的经验,最好的一种使用方式
仔细观察发现,Feign的客户端与服务提供者的controller代码十分相似
- Feign
- Controller
1
2
3
4
5
public interface UserClient {
User findById(; Long id)
}
- 除了方法名,其余代码几乎一模一样,那有没有一种方法简化这种重复的代码编写呢?
继承方式
这两部分相同的代码,可以通过继承来共享
定义一个API接口,利用定义方法,并基于SpringMVC注解做声明
1
2
3
4public interface UserAPI{
User findById(; Long id)
}Feign客户端和Controller都继承该接口
- Feign客户端
- Controller
1
2
public interface UserClient extends UserAPI{}
优点
- 简单
- 实现了代码共享
缺点
- 服务提供方、服务消费方紧耦合
- 参数列表中的注解映射并不会继承,所以Controller中必须再次声明方法、参数列表、注解
抽取方式
- 将Feign的Client抽取为独立模块,并且把接口有关的POJO、默认的Feign配置都放到这个模块中,提供给所有消费者使用
- 例如,将UserClient、User、Feign的默认配置都抽取到一个feign-api包中,所有微服务引用该依赖包,即可直接使用
实现基于抽取的最佳实践
抽取
在order-service中使用feign-api
首先,将order-service中的UserClient、User、DefaultFeignConfiguration等类或接口删除掉
然后在order-service中的pom文件中引入我们自己编写的feign-api依赖
1
2
3
4
5<dependency>
<groupId>cn.itcast.demo</groupId>
<artifactId>feign-api</artifactId>
<version>1.0</version>
</dependency>接着修改order-service中涉及到以上三个组件的代码爆红部分
解决包扫描问题
现在UserClient在cn.itcast.feign.clients包下,而order-service的@EnableFeignClients注解是在cn.itcast.order包下,不在同一个包,无法扫描到UserClient
方式一:指定Feign应该扫描的包
1
方式二:指定需要加载的Client接口
1
Gateway服务网关
- SpringCloudGateway是SpringCloud的一个全新项目,该项目是基于Spring 5.0,SpringBoot2.0和ProjectReactor等响应式办成和事件流技术开发的网关,它旨在为微服务框架提供一种简单有效的统一的API路由管理方式
为什么需要网关
- Gateway网关是我们服务的守门神,是所有微服务的统一入口
- 网关的核心功能特性
- 请求路由
- 权限控制
- 限流
- 架构图如下
- 路由和负载均衡:一切请求都必须先经过gateway,但网关不处理业务,而是根据某种规则,把请求转发到某个微服务,这个过程叫路由。当然路由的目标服务有多个时,还需要做负载均衡
- 权限控制:网关作为微服务的入口,需要校验用户是否有请求资格,如果没有则拦截
- 限流:当请求量过高时,在网关中按照微服务能够接受的速度来放行请求,避免服务压力过大
- 在SpringCloud中网关的实现包括两种
- gateway
- zuul
- Zuul是基于Servlet的实现,属于阻塞式编程。而SpringCloudGateway则是基于Spring5中提供的WebFlux,属于响应式编程的实现,具备更好的性能
gateway快速入门
下面,我们就来演示一下网关的基本路由功能,基本步骤如下
创建SpringBoot工程gateway,引入网关依赖
创建一个maven工程就行,引入依赖如下
1
2
3
4
5
6
7
8
9
10<!--网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos服务发现依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
编写启动类
1
2
3
4
5
6
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class,args);
}
}编写基础配置和路由规则
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19server:
port: 10010 # 网关端口
spring:
application:
name: gateway # 服务名称
cloud:
nacos:
server-addr: localhost:80 # nacos地址(我这里还是用的nginx反向代理,你们可以启动一个单体的nacos,用8848端口)
gateway:
routes:
- id: user-service # 路由id,自定义,只需要唯一即可
uri: lb://user-service # 路由的目标地址,lb表示负载均衡,后面跟服务名称
# uri: http://localhost:8081 # 路由的目标地址,http就是固定地址
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
- Path=/user/** # 这个是按照路径匹配,只要是以/user开头的,就符合规则
- id: order-service # 按照上面的写法,再配置一下order-service
uri: lb://order-service
predicates:
- Path=/order/**启动网关服务进行测试
重启网关,访问
时,符合/user/**规则,请求转发到
http://user-service/user/1,结果如下
1
2
3
4
5
{
"id": 1,
"username": "柳岩",
"address": "湖南省衡阳市"
}
- 访问
http://localhost:10010/order/101
时,符合/order/**规则,请求转发到
http://order-service/order/101,结果如下
1
2
3
4
5
6
7
8
9
10
11
12
{
"id": 101,
"price": 699900,
"name": "Apple 苹果 iPhone 12 ",
"num": 1,
"userId": 1,
"user": {
"id": 1,
"username": "柳岩",
"address": "湖南省衡阳市"
}
}
1. 网关陆游的流程图
[![img](https://pic1.imgdb.cn/item/6370a93e16f2c2beb1c23c09.jpg)](https://pic1.imgdb.cn/item/6370a93e16f2c2beb1c23c09.jpg)
总结
- 网关搭建的步骤
- 创建项目,引入nacos和gateway依赖
- 配置application.yml,包括服务基本信息,nacos地址、路由
- 路由配置包括
- 路由id:路由的唯一表示
- 路由目标(uri):路由的目标地址,http代表固定地址,lb代表根据服务名称负载均衡
- 路由断言(predicates):判断路由的规则
- 路由过滤器(filters):对请求或相应做处理
- 网关搭建的步骤
接下来我们就来重点学习路由断言和路由过滤器的详细知识
断言工厂
- 我们在配置文件中写的断言规则只是字符串,这些字符串会被
Predicate Factory
读取并处理,转变为路由判断的条件 - 例如
Path=/user/**
是按照路径匹配,这个规则是由org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory
类来处理的,像这样的断言工厂,在SpringCloudGatewway还有十几个
名称 | 说明 | 示例 |
---|---|---|
After | 是某个时间点后的请求 | - After=2037-01-20T17:42:47.789-07:00[America/Denver] |
Before | 是某个时间点之前的请求 | - Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai] |
Between | 是某两个时间点之前的请求 | - Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver] |
Cookie | 请求必须包含某些cookie | - Cookie=chocolate, ch.p |
Header | 请求必须包含某些header | - Header=X-Request-Id, \d+ |
Host | 请求必须是访问某个host(域名) | - Host=.somehost.org,.anotherhost.org |
Method | 请求方式必须是指定方式 | - Method=GET,POST |
Path | 请求路径必须符合指定规则 | - Path=/red/{segment},/blue/** |
Query | 请求参数必须包含指定参数 | - Query=name, Jack或者- Query=name |
RemoteAddr | 请求者的ip必须是指定范围 | - RemoteAddr=192.168.1.1/24 |
Weight | 权重处理 |
- 关于更详细的使用方法,可以参考官方文档:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gateway-request-predicates-factories
过滤器工厂
路由过滤器的种类
- Spring提供了31中不同的路由过滤器工厂,例如
名称 | 说明 |
---|---|
AddRequestHeader | 给当前请求添加一个请求头 |
RemoveRequestHeader | 移除请求中的一个请求头 |
AddResponseHeader | 给响应结果中添加一个响应头 |
RemoveResponseHeader | 从响应结果中移除有一个响应头 |
RequestRateLimiter | 限制请求的流量 |
官方文档的使用举例
1
2
3
4
5
6
7
8spring:
cloud:
gateway:
routes:
- id: add_request_header_route
uri: https://example.org
filters:
- AddRequestHeader=X-Request-red, blueThis listing adds X-Request-red:blue header to the downstream request’s headers for all matching requests.
关于更详细的使用方法,可以参考官方文档:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gateway-request-predicates-factories
请求头过滤器
下面我们以AddRequestHeader为例,作为讲解
需求:给所有进入user-service的请求都添加一个请求头:Truth=Welcome to Kyle’s Blog!
只需要修改gateway服务的application.yml文件,添加路由过滤即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16server:
port: 10010 # 网关端口
spring:
application:
name: gateway # 服务名称
cloud:
nacos:
server-addr: localhost:80 # nacos地址
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/user/**
filters:
- AddRequestHeader=Truth, Welcome to Kyle's Blog! # 添加请求头当前过滤器写在user-service路由下,因此仅仅对访问user-service的请求有效,我们在UserController中编写对应的方法来测试
1
2
3
4
public void test( { String tmp)
System.out.println(tmp);
}重启网关和user-service,打开浏览器访问http://localhost:10010/user/test, 控制台会输出
Welcome to Kyle's Blog!
,证明我们的配置已经生效
默认过滤器
如果要对所有的路由都生效,则可以将过滤器工厂写到default下,格式如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16server:
port: 10010 # 网关端口
spring:
application:
name: gateway # 服务名称
cloud:
nacos:
server-addr: localhost:80 # nacos地址
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/user/**
default-filters:
- AddRequestHeader=Truth, Welcome to Kyle's Blog! # 添加请求头重启网关服务,打开浏览器访问http://localhost:10010/user/test, 控制台依旧会输出
Welcome to Kyle's Blog!
,证明我们的配置已经生效
小结
- 过滤器的作用是什么?
- 对路由的请求或响应做加工处理,比如添加请求头
- 配置在路由下的过滤器只对当前路由请求生效
- default-filters的作用是什么?
- 对所有路由都生效的过滤器
全局过滤器
- 上面提到的31中过滤器的每一种的作用都是固定的,如果我们希望拦截请求,做自己的业务逻辑,则无法实现,这就要用到我们的全局过滤器了
全局过滤器的作用
全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与GatewayFilter的作用一样。区别在于GatewayFilter通过配置定义,处理的逻辑是固定的,而GlobalFilter的逻辑需要我们自己编写代码实现
定义的方式就是实现GlobalFilter接口
1
2
3
4
5
6
7
8
9
10public interface GlobalFilter {
/**
* 处理当前请求,有必要的话通过{@link GatewayFilterChain}将请求交给下一个过滤器处理
*
* @param exchange 请求上下文,里面可以获取Request、Response等信息
* @param chain 用来把请求委托给下一个过滤器
* @return {@code Mono<Void>} 返回标示当前过滤器业务结束
*/
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}在filter中编写自定义逻辑,可以实现下列功能
- 登录状态判断
- 权限校验
- 请求限流等
自定义全局过滤器
需求:定义全局过滤器,拦截请求,判断请求参数是否满足下面条件
- 参数中是否有authorization
- authorization参数值是否为admin
如果同时满足,则放行,否则拦截
具体实现如下
在gateway模块下新建cn.itcast.gateway.filter包,然后在其中编写AuthorizationFilter类,实现GlobalFilter接口,重写其中的filter方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public class AuthorizationFilter implements GlobalFilter {
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1. 获取请求参数
MultiValueMap<String, String> params = exchange.getRequest().getQueryParams();
// 2. 获取authorization参数
String authorization = params.getFirst("authorization");
// 3. 校验
if ("admin".equals(authorization)) {
// 4. 满足需求则放行
return chain.filter(exchange);
}
// 5. 不满足需求,设置状态码,这里的常量底层就是401,在restFul中401表示未登录
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
// 6. 结束处理
return exchange.getResponse().setComplete();
}
}
重启网关,测试我们的拦截器是否生效,打开浏览器访问
http://localhost:10010/user/1,无法正常访问;加上需要的请求参数访问http://localhost:10010/user/1?authorization=admin,
可以看到正常数据
1 | { |
过滤器执行顺序
请求进入网关会碰到三类过滤器:当前路由的过滤器、DefaultFilter、GlobalFilter
请求路由后,会将当前路由过滤器和DefaultFilter、GlobalFilter,合并到一个过滤器链(集合)中,排序后依次执行每个过滤器
那么排序的规则是什么呢?
每个过滤器都必须指定一个int类型的order值,order值越小,优先级越高,执行顺序越靠前(默认值为2147483647,即int最大值)
GlobalFilter通过实现
1
Ordered
接口,或者添加
1
注解来指定order值,需要我们自己指定
- 实现Ordered接口
- 使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public class AuthorizationFilter implements GlobalFilter, Ordered {
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1. 获取请求参数
MultiValueMap<String, String> params = exchange.getRequest().getQueryParams();
// 2. 获取authorization参数
String authorization = params.getFirst("authorization");
// 3. 校验
if ("admin".equals(authorization)) {
// 4. 满足需求则放行
return chain.filter(exchange);
}
// 5. 不满足需求,设置状态码,这里的常量底层就是401,在restFul中401表示未登录
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
// 6. 结束处理
return exchange.getResponse().setComplete();
}
public int getOrder() {
return -1;
}
}
路由过滤器和defaultFilter的order由Spring指定,默认是按照声明顺序从1递增
当过滤器的order值一样时,会按照defaultFilter > 路由过滤器 > GlobalFilter的顺序执行
例如下面这种情况下的order值就会相同,如果我们在自定义全局过滤器中设定的order也为1,那么也会冲突
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22server:
port: 10010 # 网关端口
spring:
application:
name: gateway # 服务名称
cloud:
nacos:
server-addr: localhost:80 # nacos地址
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/user/**
filters:
- AddRequestHeader=Truth, Welcome to Kyle's Blog! # 1
- AddRequestHeader=Truth, Welcome to Kyle's Blog! # 2
- AddRequestHeader=Truth, Welcome to Kyle's Blog! # 3
default-filters:
- AddRequestHeader=Truth, Welcome to Kyle's Blog! # 1
- AddRequestHeader=Truth, Welcome to Kyle's Blog! # 2
- AddRequestHeader=Truth, Welcome to Kyle's Blog! # 3
详细内容,可以查看源码:
org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator#getFilters()
方法是先加载defaultFilters,然后再加载某个route的filters,然后合并。org.springframework.cloud.gateway.handler.FilteringWebHandler#handle()
方法会加载全局过滤器,与前面的过滤器合并后根据order排序,组织过滤器链
跨域问题
什么是跨域问题
- 跨域:域名不一致就是跨域,主要包括
- 域名不同:
www.baidu.com
和www.baidu.org
,www.js.com
和miaosha.js.com
- 域名相同,端口不同:localhost:8080和localhost:8081
- 域名不同:
- 跨域问题:浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题
- 解决方案:CORS
- CORS是一个W3C标准,全称是”跨域资源共享”(Cross-origin resource sharing)。
- 它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。
解决跨域问题
在gateway服务的application.yml文件中,添加下面的配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18spring:
cloud:
gateway:
globalcors: # 全局的跨域处理
add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
corsConfigurations:
'[/**]':
allowedOrigins: # 允许哪些网站的跨域请求
- "http://localhost:9527"
allowedMethods: # 允许的跨域ajax的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许在请求中携带的头信息
allowCredentials: true # 是否允许携带cookie
maxAge: 360000 # 这次跨域检测的有效期
Docker容器化
一.引入
(1) 为什么需要Docker
微服务虽然具备各种各样的优势,但服务的拆分的非常多给部署带来了很大的麻烦。
- 分布式系统中,依赖的组件非常多,不同组件之间部署时往往会产生一些冲突。
- 在数百上千台服务中重复部署,环境不一定一致,会遇到各种问题
2.大型项目组件较多,运行环境也较为复杂,部署时会碰到一些问题:
依赖关系复杂,容易出现兼容性问题
开发、测试、生产环境有差异
例如一个项目中,部署时需要依赖于node.js、Redis、RabbitMQ、MySQL等,这些服务部署时所需要的函数库、依赖项各不相同,甚至会有冲突。给部署带来了极大的困难。
(2) Docker的作用
(2.1) 解决依赖兼容问题
Docker为了解决依赖的兼容问题的,采用了两个手段:
- 将应用的Libs(函数库)、Deps(依赖)、配置与应用一起打包
将每个应用放到一个隔离容器去运行,避免互相干扰
- 这样打包好的应用包中,既包含应用本身,也包含了应用所需要的Libs、Deps,无需在操作系统上安装这些,由此不再存在不同应用之间的兼容问题了。
虽然解决了不同应用的兼容问题,但是开发、测试等环境会存在差异,操作系统版本也会有差异,怎么解决这些问题呢?
(2.2) 解决操作系统环境差异
要解决不同操作系统环境差异问题,必须先了解操作系统结构。以一个Ubuntu操作系统为例,结构如下:
结构包括:
- 计算机硬件:例如CPU、内存、磁盘等
- 系统内核:所有Linux发行版的内核都是Linux,例如CentOS、Ubuntu、Fedora等。内核可以与计算机硬件交互,对外提供内核指令,用于操作计算机硬件。
- 系统应用:操作系统本身提供的应用、函数库。这些函数库是对内核指令的封装,使用更加方便。
应用于计算机交互的流程如下:
- 应用调用操作系统应用(函数库),实现各种功能
- 系统函数库是对内核指令集的封装,会调用内核指令
- 内核指令操作计算机硬件
Ubuntu和CentOSpringBoot都是基于Linux内核,无非是系统应用不同,提供的函数库有差异:
此时,如果将一个Ubuntu版本的MySQL应用安装到CentOS系统,MySQL在调用Ubuntu函数库时,会发现找不到或者不匹配,就会报错了:
Docker如何解决不同系统环境的问题?
Docker将用户程序与所需要调用的系统(比如Ubuntu)函数库一起打包
Docker运行到不同操作系统时,直接基于打包的函数库,借助于操作系统的Linux内核来运行
这样就不用再关注运行的系统本身。
如图:
(3) Docker和虚拟机的区别
Docker可以让一个应用在任何操作系统中非常方便的运行。而以前我们接触的虚拟机,也能在一个操作系统中,运行另外一个操作系统,保护系统中的任何应用。
两者有什么差异呢?
虚拟机(virtual machine)是在操作系统中模拟硬件设备,然后运行另一个操作系统,比如在 Windows 系统里面运行 Ubuntu 系统,这样就可以运行任意的Ubuntu应用了。
- 是在操作系统中的操作系统,体积大、启动速度慢、性能一般
Docker仅仅是封装函数库,并模拟部分操作系统,
- 是一个系统进程,体积小、启动速度快、性能好
如图:
对比来看:
(4) 小结
Docker如何解决大型项目依赖关系复杂,不同组件依赖的兼容性问题?
- Docker允许开发中将应用、依赖、函数库、配置一起打包,形成可移植镜像
- Docker应用运行在容器中,使用沙箱机制,相互隔离
Docker如何解决开发、测试、生产环境有差异的问题?
- Docker镜像中包含完整运行环境,包括系统函数库,仅依赖系统的Linux内核,因此可以在任意Linux操作系统上运行
Docker是一个快速交付应用、运行应用的技术,具备下列优势:
- 可以将程序及其依赖、运行环境一起打包为一个镜像,可以迁移到任意Linux操作系统
- 运行时利用沙箱机制形成隔离容器,各个应用互不干扰
- 启动、移除都可以通过一行命令完成,方便快捷
二.相关概念
(1) 镜像和容器
- 镜像(Image):Docker将应用程序及其所需的依赖、函数库、环境、配置等文件打包在一起,称为镜像。
- 即一个应用在硬盘上的文件、及其运行环境、部分系统函数库文件一起打包形成的文件包。这个文件包是只读的。
- 容器(Container):镜像中的应用程序运行后形成的进程就是容器,只是Docker会给容器进程做隔离,对外不可见。即将这些文件中编写的程序、函数加载到内存中运行,形成进程,只不过会对外隔离起来.
- 一个镜像可以启动多次,形成多个容器进程。
例如你下载了一个QQ,如果我们将QQ在磁盘上的运行文件及其运行的操作系统依赖打包,形成QQ镜像。然后你可以启动多次,双开、甚至三开QQ,跟多个妹子聊天。
(2) DockerHub
开源应用程序非常多,打包这些应用往往是重复的劳动。为了避免这些重复劳动,人们就会将自己打包的应用镜像,例如Redis、MySQL镜像放到网络上,共享使用,就像GitHub的代码共享一样。
- DockerHub是一个官方的Docker镜像的托管平台。这样的平台称为Docker Registry。
- 国内也有类似于DockerHub 的公开服务,比如 网易云镜像服务、阿里云镜像库等。
我们一方面可以将自己的镜像共享到DockerHub,另一方面也可以从DockerHub拉取镜像:
(3) Docker架构
Docker是一个CS架构的程序,由两部分组成:
- 服务端(server):Docker守护进程,负责处理Docker指令,管理镜像、容器等
- 客户端(client):通过命令或RestAPI向Docker服务端发送指令。可以在本地或远程向服务端发送指令。
(4) 安装Docker
企业部署一般都是采用Linux操作系统,而其中又数CentOS发行版占比最多,因此我们在CentOS下安装Docker。
1.首先需要大家虚拟机联网,安装yum工具
1 | yum install -y yum-utils \ |
2.然后更新本地镜像源:
1 | # 设置docker镜像源 |
3.然后输入命令安装docker:
1 | yum install -y docker-ce |
4.通过命令启动docker:
1 | systemctl start docker # 启动docker服务 |
5.然后输入命令,可以查看docker版本:
1 | docker -v |
6.docker官方镜像仓库网速较差,我们需要设置国内镜像服务:
参考阿里云的镜像加速文档
三.Docker基本操作
(1) 镜像操作
(1.1) 镜像名称
首先来看下镜像的名称组成:
镜像名称一般分两部分组成:[repository]:[tag]。
在没有指定tag时,默认是latest,代表最新版本的镜像
这里的mysql就是repository,5.7就是tag,合一起就是镜像名称,代表5.7版本的MySQL镜像。
(1.2) 镜像命令
常见的镜像操作命令如图:
可以利用docker xx —help命令查看指令的用法
例如,查看save命令用法,可以输入命令:
1 | docker save --help |
结果:
(1.3) 案例
需求一:从DockerHub中拉取一个nginx镜像并查看
- 首先去镜像仓库搜索nginx镜像,比如DockerHub:
- 选择一个所需要的镜像
3.点进去,拉取自己需要的镜像的版本,不指定tag默认最新版
4.通过命令:docker images 查看拉取到的镜像
如此可以看到我们已经成功拉取到了Nginx镜像
- 需求二:将nginx镜像导出磁盘,然后再通过load加载回来
1.使用docker save导出镜像到磁盘
命令格式:
1 | docker save -o [保存的目标文件名称] [镜像名称] |
运行命令:
1 | docker save -o nginx.tar nginx:latest |
结果如图:
先删除本地之前拉取的nginx镜像:
1 | docker rmi nginx:latest |
然后运行命令,加载本地文件:
1 | docker load -i nginx.tar |
通过指令查看镜像
1 | docker images |
结果:
可以看到我们已经成功加载回来了Nginx镜像
(2) 容器操作
(2.1) 容器命令
容器操作的命令如图:
容器保护三个状态:
- 运行:进程正常运行
- 暂停:进程暂停,CPU不再运行,并不释放内存
- 停止:进程终止,回收进程占用的内存、CPU等资源
(2.2) 进程隔离
默认情况下,容器是隔离环境,其内部会模拟一个独立的Linux文件系统,看起来如同一个linux服务器一样,我们直接访问宿主机的端口,无法访问到容器内部署的应用。
我们需要将容器内的应用端口与宿主机的端口进行映射,才能直接通过宿主机访问容器内应用。
例如,当我们在容器内部署Nginx时,我们将容器的80端口与宿主机的80端口关联起来,当我们访问宿主机的80端口时,就会被映射到容器的80,这样就能访问到nginx了:
(2.2) 运行容器
可以去DockerHub查看具体要运行镜像的帮助文档查看如何启动镜像
例如在Nginx镜像页面往下翻可以看到如下How to use this image?
例如:
运行nginx容器的命令:
1 | docker run --name myNginx -p 80:80 -d nginx |
命令解读:
- docker run :创建并运行一个容器
- –name : 给容器起一个名字,例如myNginx
- -p :将宿主机端口与容器端口映射,冒号左侧是宿主机端口,右侧是容器端口(80:80)
- -d:后台运行容器
- nginx:镜像名称,例如nginx
(2.3) 案例
需求:运行Nginx镜像并进入Nginx容器,修改HTML文件内容,添加“观止study欢迎您”
1.运行镜像
1 | docker run --name myNginx -p 80:80 -d nginx |
2.使用docker exec进入我们刚刚创建的nginx容器的命令为:
1 | docker exec -it myNginx bash |
命令解读:
docker exec :进入容器内部,执行一个命令
-it : 给当前进入的容器创建一个标准输入、输出终端,允许我们与容器交互
myNginx:要进入的容器的名称
bash:进入容器后执行的命令,bash是一个linux终端交互命令
容器内部会模拟一个独立的Linux文件系统,看起来如同一个linux服务器一样:
nginx的环境、配置、运行文件全部都在这个文件系统中,包括我们要修改的html文件。
查看DockerHub网站中的nginx页面,可以知道nginx的html目录位置在/usr/share/nginx/html
3.执行命令,进入该目录:
1 | cd /usr/share/nginx/html |
查看目录下文件:
4.修改index.html的内容
容器内没有vi命令,无法直接修改,我们用下面的命令来修改:
1 | sed -i -e 's#Welcome to nginx#观止study欢迎您#g' -e 's#<head>#<head><meta charset="utf-8">#g' index.html |
5.在浏览器访问自己的虚拟机ip地址,例如我的是:http://192.168.150.101:80,即可看到结果:
(2.4) 查看容器
查看容器运行日志:
- docker logs
- docker logs -f 可以持续查看日志
查看容器状态:
- docker ps
- docker ps -a 查看所有容器,包括已经停止的
(2.5) 容器转镜像
我们可以将修改后的容器转为镜像,再打包可供自己或他人继续使用
1.容器转镜像
1 | docker commit 容器id 镜像名称:版本号 |
2.打包为压缩文件
1 | docker save -o 压缩文件名称 镜像名称:版本号 |
可以通过如下命令将压缩文件重新转为镜像
1 | docker load -i 压缩文件名称 |
(3) 数据卷(容器数据管理)
(3.1) 为什么需要数据卷
在之前的nginx案例中,修改nginx的html页面时,需要进入nginx内部。并且因为没有编辑器,修改文件也很麻烦。而且删除容器后在容器内的所有修改都不会保留。
这就是因为容器与数据(容器内文件)耦合带来的后果。
而使用数据卷能将容器与数据分离,解耦合,方便操作和维护容器内数据,保证数据安全
(3.2) 什么是数据卷
数据卷(volume)是一个虚拟目录,指向宿主机文件系统中的某个目录。一个数据卷可以同时被多个容器挂载。
一旦完成数据卷挂载,对容器的一切操作都会作用在数据卷对应的宿主机目录了。
同时,我们操作宿主机的/var/lib/docker/volumes/html目录,就等于操作容器内的/usr/share/nginx/html目录了(双向绑定)
这样我们就可以不进入容器内部修改其数据文件了。
(3.3) 数据集操作命令
数据卷操作的基本语法如下:
1 | docker volume |
docker volume命令是数据卷操作,根据命令后跟随的command来确定下一步的操作:
- create 创建一个volume
- inspect 显示一个或多个volume的信息
- ls 列出所有的volume
- prune 删除未使用的volume
- rm 删除一个或多个指定的volume
① 创建数据卷
1 | docker volume create [数据卷名字] |
② 查看所有数据
1 | docker volume ls |
结果:
③ 查看数据卷详细信息卷
1 | docker volume inspect [数据卷名字] |
结果:
可以看到,我们创建的html这个数据卷关联的宿主机目录为/var/lib/docker/volumes/html/_data目录。
(3.4) 挂载数据卷
我们在创建容器时,可以通过 -v 参数来挂载一个数据卷到某个容器内目录,命令格式如下:
1 | docker run \ |
这里的-v就是挂载数据卷的命令:
- -v html:/root/htm :(数据卷:容器文件)把html数据卷挂载到容器内的/root/html这个目录中
- 一旦完成数据卷挂载,在数据卷中的一切操作都会作用于容器中对应的目录中。
(3.5) 案例-1
需求:创建一个nginx容器,修改容器内的html目录内的index.html内容
分析:上个案例中,我们已经知道nginx的html目录所在位置/usr/share/nginx/html ,我们需要把这个目录挂载到html这个数据卷上,方便操作其中的内容。
① 创建容器并挂载数据卷到容器内的HTML目录
1 | docker run --name mn -v html:/usr/share/nginx/html -p 80:80 -d nginx |
② 进入html数据卷所在位置,并修改HTML内容
1 | # 查看html数据卷的位置 |
(3.6) 挂载本地目录
容器不仅仅可以挂载数据卷,也可以直接挂载到宿主机目录上。关联关系如下:
带数据卷模式:宿主机目录 —> 数据卷 —> 容器内目录
直接挂载模式:宿主机目录 —> 容器内目录
目录挂载与数据卷挂载的语法是类似的:
-v [宿主机目录]:[容器内目录]
-v [宿主机文件]:[容器内文件]
特点:
数据卷挂载耦合度低,由docker来管理目录,但是目录较深,不好找
目录挂载耦合度高,需要我们自己管理目录,不过目录容易寻找查看\
如图:
(3.7) 案例-2
需求:创建并运行一个MySQL容器,将宿主机目录直接挂载到容器
实现思路如下:
1)在DockerHub中拉取mysql镜像
2)在本机中创建目录/tmp/mysql/data和/tmp/mysql/conf
3)可以将自己的配置文件hmy.cnf上传到/tmp/mysql/conf
4)在DockerHub查阅资料,创建并运行MySQL容器,要求:
① 挂载/tmp/mysql/data到mysql容器内数据存储目录
② 挂载/tmp/mysql/conf/hmy.cnf到mysql容器的配置文件
③ 设置MySQL密码
1 | docker run \ |
四.Dockerfile自定义镜像
常见的镜像在DockerHub就能找到,但是我们自己写的项目就必须自己构建镜像了。
而要自定义镜像,就必须先了解镜像的结构才行。
(1) 镜像结构
镜像是将应用程序及其需要的系统函数库、环境、配置、依赖打包而成。
我们以MySQL为例,来看看镜像的组成结构:
简单来说,镜像就是在系统函数库、运行环境基础上,添加应用程序文件、配置文件、依赖文件等组合,然后编写好启动脚本打包在一起形成的文件。
我们要构建镜像,其实就是实现上述打包的过程。
(2) Dockerfile语法
构建自定义的镜像时,并不需要一个个文件去拷贝,打包。
我们只需要告诉Docker,我们的镜像的组成,需要哪些BaseImage、需要拷贝什么文件、需要安装什么依赖、启动脚本是什么,将来Docker会帮助我们构建镜像。
而描述上述信息的文件就是Dockerfile文件。
Dockerfile就是一个文本文件,其中包含一个个的指令(Instruction),用指令来说明要执行什么操作来构建镜像。每一个指令都会形成一层Layer,而第一行必须是FROM,从一个基础镜像来构建。基础镜像可以是基本操作系统,如Ubuntu。也可以是其他人制作好的镜像,例如:java:8-alpine
更多详细语法说明,请参考官网文档: https://docs.docker.com/engine/reference/builder
(3) 案例
案例资料:
链接:https://pan.baidu.com/s/1TcEyFHUK645qu09EzdjGYA
提取码:rmho
(3.1) 基于Ubuntu构建Java项目
需求:基于Ubuntu镜像构建一个新镜像,运行一个java项目
步骤1:在电脑上新建一个空文件夹docker-demo
步骤2:拷贝资料中的docker-demo.jar文件到docker-demo这个目录
步骤3:拷贝资料中的jdk8.tar.gz文件到docker-demo这个目录
步骤4:拷贝资料提供的Dockerfile到docker-demo这个目录
其中的内容如下:
1 | # 指定基础镜像 |
步骤5:将上述准备好的docker-demo上传到虚拟机任意目录,然后进入docker-demo目录下
步骤6:构建镜像:
1 | docker build -t javaweb:1.0 . |
步骤7:创建容器并运行
1 | docker run --name web -p 8090:8090 -d javaweb:1.0 |
最后访问 http://192.168.150.101:8090/hello/count,即可看到效果。其中的ip需改成自己服务器ip
(3.2) 基于java8构建Java项目
虽然我们可以基于Ubuntu基础镜像,添加任意自己需要的安装包,构建镜像,但是却比较麻烦。所以大多数情况下,我们都可以在一些安装了部分软件的基础镜像上做改造。
例如,构建java项目的镜像,可以在已经准备了JDK的基础镜像基础上构建。
需求:基于java:8-alpine镜像,将一个Java项目构建为镜像
实现思路如下:
- ① 新建一个空的目录,然后在目录中新建一个文件,命名为Dockerfile
- ② 拷贝上述资料提供的docker-demo.jar到这个目录中
③ 编写Dockerfile文件:
- a )基于java:8-alpine作为基础镜像
- b )将app.jar拷贝到镜像中
- c )暴露端口
- d )编写入口ENTRYPOINT
内容如下:
1 | FROM java:8-alpine |
④ 使用docker build命令构建镜像
1 | docker build -t javaweb:1.0 . |
⑤ 使用docker run创建容器并运行
1 | docker run --name web -p 8090:8090 -d javaweb:1.0 |
最后访问 http://192.168.150.101:8090/hello/count,即可看到相同效果
五.Docker-Compose
Docker Compose可以基于Compose文件帮我们快速的部署分布式应用,而无需手动一个个创建和运行容器!
(1) 初识
Compose文件是一个文本文件,通过指令定义集群中的每个容器如何运行。可以看做是将多个docker run命令写到一个文件,只是语法稍有差异。
格式如下:
1 | version: "3.8" |
上面的Compose文件就描述一个项目,其中包含两个容器:
mysql:一个基于mysql:5.7.25镜像构建的容器,并且挂载了两个目录
web:一个基于docker build临时构建的镜像容器,映射端口时8090
DockerCompose的详细语法参考官网:https://docs.docker.com/compose/compose-file/
使用基本类似
(2) 安装
(2.1) 下载
在Linux下使用需要通过命令下载:
1 | # 安装 |
(2.2) 修改文件权限
修改文件权限,让其变成可执行文件:
1 | # 修改权限 |
(2.3) Base自动补全命令:
在使用时会有提示
1 | # 补全命令 |
(3) 部署微服务集群
需求:将资料中的cloud-demo微服务集群利用DockerCompose部署
案例资料:
链接:https://pan.baidu.com/s/1TcEyFHUK645qu09EzdjGYA
提取码:rmho
实现思路:
① 查看资料提供的cloud-demo文件夹,里面已经编写好了docker-compose文件
② 修改自己的cloud-demo项目,将数据库、nacos地址都命名为docker-compose中的服务名
③ 使用maven打包工具,将项目中的每个微服务都打包为app.jar
④ 将打包好的app.jar拷贝到cloud-demo中的每一个对应的子目录中
⑤ 将cloud-demo上传至虚拟机,利用 docker-compose up -d 来部署
(3.1) compose文件
查看资料提供的cloud-demo文件夹,里面已经编写好了docker-compose文件,而且每个微服务都准备了一个独立的目录:
内容如下:
1 | version: "3.2" |
可以看到,其中包含5个service服务:
nacos:作为注册中心和配置中心
- image: nacos/nacos-server: 基于nacos/nacos-server镜像构建
- environment:环境变量
- MODE: standalone:单点模式启动
- ports:端口映射,这里暴露了8848端口
mysql:数据库
- image: mysql:5.7.25:镜像版本是mysql:5.7.25
- environment:环境变量
- MYSQL_ROOT_PASSWORD: 123:设置数据库root账户的密码为123
- volumes:数据卷挂载,这里挂载了mysql的data、conf目录,其中有我提前准备好的数据
userservice、orderservice、gateway:都是基于Dockerfile临时构建的
查看mysql目录,可以看到其中已经准备好了cloud_order、cloud_user表:
查看微服务目录,可以看到都包含Dockerfile文件:
内容如下:
1 | FROM java:8-alpine |
(3.2) 修改微服务配置
因为微服务将来要部署为docker容器,而容器之间互联不是通过IP地址,而是通过容器名。这里我们需要将order-service、user-service、gateway服务的mysql、nacos地址都修改为基于容器名的访问。
如下所示:
1 | spring: |
(3.3) 打包
接下来需要将我们的每个微服务都打包为jar。因为之前查看到Dockerfile中的jar包名称都是app.jar,因此我们的每个微服务都需要用这个名称。
可以通过修改pom.xml中的打包名称来实现,每个微服务都需要修改:
1 | <build> |
打包后:
(3.4) 拷贝jar包到部署目录
编译打包好的app.jar文件,需要放到Dockerfile的同级目录中。注意:每个微服务的app.jar放到与服务名称对应的目录,别搞错了。
user-service:
order-service:
gateway:
(3.5) 部署
最后,我们需要将文件整个cloud-demo文件夹上传到虚拟机中,通过DockerCompose部署。
上传到任意目录:
部署:
进入cloud-demo目录,然后运行下面的命令即可:
1 | docker-compose up -d |
六.Docker镜像仓库
我们自己制作的镜像不可能全都放在DockerHub这个公开平台上去,这时就需要我们搭建属于自己的私有仓库,
(1) 搭建私有镜像仓库
搭建镜像仓库可以基于Docker官方提供的DockerRegistry来实现。
官网地址:https://hub.docker.com/_/registry
(1.1) 简化版镜像仓库
Docker官方的Docker Registry是一个基础版本的Docker镜像仓库,具备仓库管理的完整功能,但是没有图形化界面。
搭建方式比较简单,命令如下:
1 | docker run -d \ |
命令中挂载了一个数据卷registry-data到容器内的/var/lib/registry 目录,这是私有镜像库存放数据的目录。
访问http://YourIp:5000/v2/_catalog 可以查看当前私有镜像服务中包含的镜像
(1.2) 带有图形化界面版本
使用DockerCompose部署带有图象界面的DockerRegistry,命令如下:
1 | version: '3.0' |
建立一个yml文件放上述内容,上传到服务器后,在当前所在目录执行如下命令:
1 | docker-compose up -d |
在浏览器访问即可,例如:http://192.168.150.101:8080
(1.3) 配置Docker信任地址
我们的私服采用的是http协议,默认不被Docker信任,所以需要做一个配置:
1 | # 打开要修改的文件 |
(2) 推送、拉取镜像
推送镜像到私有镜像服务必须先tag,步骤如下:
① 重新tag本地镜像,名称前缀为私有仓库的地址:192.168.150.101:8080/
1 | docker tag nginx:latest 192.168.150.101:8080/nginx:1.0 |
② 推送镜像
1 | docker push 192.168.150.101:8080/nginx:1.0 |
③ 拉取镜像
1 | docker pull 192.168.150.101:8080/nginx:1.0 |