SpringCloud(Eureka+Ribbon+Nacos+Feign+Gateway+Docker+...)
一、SpringCloud简介
1.概述
SpringCloud是目前国内使用最广泛的微服务框架。官网地址:https://spring.io/projects/spring-cloud。
SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配:
2.与SpringBoot的版本兼容关系
二、服务拆分及远程调用
1.案例介绍
- cloud-demo:父工程,管理依赖
- order-service:订单微服务,只负责订单相关业务
- user-service:用户微服务,只负责用户相关业务
- 微服务的要求:
- 订单微服务和用户微服务都必须有各自的数据库,相互独立
- 订单服务和用户服务都对外暴露Restful的接口
- 订单服务如果需要查询用户信息,只能调用用户服务的Restful接口,不能查询用户数据库
2.服务拆分注意事项
- 单一职责:根据业务模块拆分,做到单一职责,不要重复开发相同业务
- 数据独立:不同微服务都应该有自己独立的数据库,不要访问其它微服务的数据库
- 面向服务:将自己的业务暴露为接口,供其它微服务调用
3.基于RestTemplate远程调用
实现跨服务远程调用,就是通过发送http请求的方式来调用另一个微服务。
RestTemplate是由Spring框架提供的一个可用于应用中调用rest服务的类,它简化了与http服务的通信方式,统一了RESTFul的标准,封装了http连接,只需要传入url及其返回值类型即可。
(1)创建RestTemplate对象,并设为Bean
@Bean public RestTemplate restTemplate(){ return new RestTemplate(); }(2)利用RestTemplate发送http请求,获取数据
@Autowired private RestTemplate restTemplate; @GetMapping("{orderId}") public Order queryOrderByUserId(@PathVariable("orderId") Long orderId) { //1.根据id查询订单 Order order = orderService.queryOrderById(orderId); //2.查询用户 //2.1 动态封装http请求 String url = "http://localhost:8081/user/"+order.getUserId(); //2.2 发送http请求查询用户 User user = restTemplate.getForObject(url, User.class); //3.将user信息封装到order中 order.setUser(user); //4.返回order return order; }
【tips】服务提供者:被其它微服务调用的服务(提供接口给其它微服务)。
服务消费者:调用其它微服务的服务(调用其它微服务提供的接口)。
4.Eureka
(1)上述远程调用的问题
在上面代码的 url 中,调用服务的地址采用硬编码,这在后续的开发中肯定是不理想的,这就需要服务注册中心(Eureka)来解决这个事情。
(2)eureka的作用
- 消费者如何得知提供者实例地址?
- 提供者服务实例启动后,将自己的信息注册到 eureka-server(Eureka服务端),即服务注册。eureka-server 保存服务名称到服务实例地址列表的映射关系,消费者根据服务名称,拉取实例地址列表,即服务发现或服务拉取。
- 【tips】服务提供者和消费者都称为Eureka的客户端。
- 消费者如何从多个提供者实例中选择具体的实例?
- 消费者从实例列表中利用负载均衡算法选中一个实例地址,向该实例地址发起远程调用。
- 消费者如何得知某个提供者实例是否依然健康,是不是已经宕机?
- 提供者会每隔一段时间(默认30秒)向 eureka-server 发起请求,报告自己状态,称为心跳续约。当超过一定时间没有发送心跳时,eureka-server 会认为该微服务实例故障,将该实例从服务列表中剔除,这样消费者拉取服务时,就能将故障实例排除了。
(3)eureka的使用
1)搭建eureka注册中心
-
创建新模块eureka-server,导入spring-cloud-starter-netflix-eureka-server坐标
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency>
-
编写启动类,并开启注册中心自动装配注解
@SpringBootApplication @EnableEurekaServer public class EurekaApplication { public static void main(String[] args) { SpringApplication.run(EurekaApplication.class, args); } }
-
编写配置文件
server: port: 10086 # 服务端口 spring: application: name: eureka-server # eureka的服务名称 eureka: client: service-url: # eureka的地址信息 defaultZone: http://127.0.0.1:10086/eureka
【tips】配置文件中的defaultZone 是因为前面配置类开启了注册中心所需要配置的 eureka 的地址信息,而且 eureka 本身也是一个微服务,这里也要将自己注册进来,当后面 eureka 集群时,这里就可以填写多个,使用 “,” 隔开。
2)服务注册
将user-service服务注册到注册中心:
-
在user-service模块中导入spring-cloud-starter-netflix-eureka-client坐标
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
-
编写配置文件
spring: application: name: user-service # user-service服务名称 eureka: client: service-url: # eureka的地址信息 defaultZone: http://127.0.0.1:10086/eureka
- 【tips】IDEA模拟多实例部署:
3)服务拉取
在 order-service 中完成服务拉取,然后通过负载均衡挑选一个服务,实现远程调用,即让 order-service 向 eureka-server 拉取 user-service 的信息,实现服务拉取。
服务拉取是基于服务名称获取服务列表,然后再对服务列表做负载均衡,获取服务信息。
-
修改order-service中访问的url路径,用★服务名称★代替ip地址和端口
String url = "http://user-service/user/"+order.getUserId();
-
给启动类OrderApplication中的RestTemplate添加负载均衡注解
@Bean @LoadBalanced public RestTemplate restTemplate(){ return new RestTemplate(); }
5.Ribbon
(1)负载均衡的流程
SpringCloud 底层提供了一个名为 Ribbon 的组件,来实现负载均衡功能。
为什么只需要输入service 名称,不需要获取ip和端口就能访问了呢?
——SpringCloud Ribbon 底层采用了一个LoadBalancerInterceptor拦截器,拦截了 RestTemplate 发出的请求,对地址做了修改。 基本流程如下: 拦截 RestTemplate 请求 http://userservice/user/1,RibbonLoadBalancerClient 会从请求url中获取服务名称,也就是 user-service。 DynamicServerListLoadBalancer 根据 user-service 到 eureka 拉取服务列表,eureka 返回列表:localhost:8081、localhost:8082;IRule 利用内置负载均衡规则,从列表中选择一个,例如 localhost:8081;RibbonLoadBalancerClient 修改请求地址,用 localhost:8081 替代 userservice,得到 http://localhost:8081/user/1,发起真实请求。
(2)负载均衡策略
- 负载均衡的策略都定义在 IRule 接口中,而 IRule 有很多不同的实现类:
- 常见的负载均衡策略:
内置负载均衡规则类 |
描述 |
RoundRobinRule |
简单轮询服务列表来选择服务器。它是Ribbon默认的负载均衡规则。 |
AvailabilityFilteringRule |
对以下两种服务器进行忽略:
(1)在默认情况下,这台服务器如果3次连接失败,这台服务器就会被设置为“短路”状态。短路状态将持续30秒,如果再次连接失败,短路的持续时间就会几何级地增加。
(2)并发数过高的服务器。如果一个服务器的并发连接数过高,配置了AvailabilityFilteringRule规则的客户端也会将其忽略。
并发连接数的上限,可以由客户端的<clientName>.<clientConfigNameSpace>.ActiveConnectionsLimit属性进行配置。
|
WeightedResponseTimeRule |
为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择服务器,这个权重值会影响服务器的选择。 |
ZoneAvoidanceRule |
以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询。 |
BestAvailableRule |
忽略那些短路的服务器,并选择并发数较低的服务器。 |
RandomRule |
随机选择一个可用的服务器。 |
RetryRule |
重试机制的选择逻辑。 |
(3)修改负载均衡策略
默认是轮询,那么如何修改负载均衡策略呢?有两种方式:
-
代码方式:在启动类或配置类中定义一个新的IRule Bean,返回想要的负载均衡策略。
@Bean public IRule randomRule(){ //随机选择策略 return new RandomRule(); }
-
配置文件方式:在order-service的 application.yml 文件中,添加新的配置修改负载均衡策略:
user-service: # 指定使用该负载均衡策略的微服务名称,比如这里是order-service调用user-service ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则
【tips】代码方式属于全局设置,无论调用哪个微服务都按此策略;配置文件方式是想调谁就配谁,只针对该微服务有效。
(4)懒加载与饥饿加载(eager-load)
当启动 order-service,第一次访问时,请求时间会很长,这是因为 Ribbon 的懒加载机制。
Ribbon 默认采用懒加载,即第一次访问时才创建 LoadBalanceClient,拉取集群地址,所以请求时间会很长。
饥饿加载则会在项目启动时创建 LoadBalanceClient,降低第一次访问的耗时,可以通过以下配置开启饥饿加载:
ribbon: eager-load: enabled: true # 开启饥饿加载 clients: user-service # 项目启动时直接去拉取user-service的集群
三、★Nacos注册中心
1.Nacos简介
Nacos是阿里巴巴的产品,现在是SpringCloud中的一个组件。相比Eureka功能更加丰富,在国内受欢迎程度较高。
2.Nacos安装
(1)下载安装包并解压即安装完成
(2)启动
进入bin目录,执行命令:
./startup.cmd -m standalone # 单机启动
【tips】默认端口8848
3.使用Nacos进行服务注册
这里上来就直接服务注册,很多东西可能有疑惑,其实 Nacos 本身就是一个 SprintBoot 项目,这点从启动的控制台打印就可以看出来,所以就不再需要去额外搭建一个像 Eureka 的注册中心。
(1)导入坐标
1)在 cloud-demo 父工程中引入 SpringCloudAlibaba 的依赖,进行版本管理:
<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>2)如果已导入eureka依赖,需将其注释掉
3)在微服务(order-service、user-service)中导入nacos客户端依赖,用来将微服务注册到nacos注册中心,包括发现其他微服务:
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>(2)配置nacos服务地址
spring: cloud: nacos: server-addr: localhost:8848 # nacos服务地址
(3)实现服务注册与发现
在启动类上使用 @EnableDiscoveryClient 注解来开启服务注册与发现功能(把该微服务注册到nacos):
@EnableDiscoveryClient @SpringBootApplication public class GulimallCouponApplication {
4.服务分级存储模型
(1)简介
- 一级是服务,例如user-service
- 二级是集群,例如杭州或上海
- 三级是实例,例如杭州机房的某台部署了user-service的服务器
(2)如何设置实例的集群属性?
修改实例的 application.yml 文件,添加集群配置:
spring: cloud: nacos: server-addr: localhost:8848 discovery: cluster-name: HZ # 自定义集群名称,这里HZ代表杭州配置好集群后可以查看Nacos的服务列表,可以查看每个实例所属集群:
5.NacosRule负载均衡
Ribbon默认的负载均衡策略是 ZoneAvoidanceRule,并不能实现根据同集群优先来实现负载均衡,改成 NacosRule 即可。
用 order-service 调用 user-service,所以在 order-service 配置负载均衡策略。
user-service: ribbon: NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule #负载均衡规则
6.根据权重负载均衡
服务器设备性能有差异,部分实例所在机器性能较好,另一些较差,我们希望性能好的机器承担更多的用户请求。但默认情况下 NacosRule 是同集群内随机挑选,不会考虑机器的性能问题。
Nacos 提供了根据权重配置来控制访问频率,0~1 之间,权重越大则访问频率越高,权重修改为 0,则该实例永远不会被访问。
在 Nacos服务列表,找到实例列表编辑,即可修改权重。
7.环境隔离
Nacos 提供了 namespace 来实现环境隔离功能。 Nacos 中可以有多个 namespace;namespace 下可以有 group、service 等;不同 namespace 之间相互隔离,例如不同 namespace 的服务不可相互调用。
(1)创建命名空间
默认情况下,所有 service、data、group 都在同一个名为 public(保留空间)的namespace。
(2)配置命名空间
spring: cloud: nacos: server-addr: localhost:8848 discovery: cluster-name: HZ namespace: namespace_id # 命名空间ID
8.临时实例与非临时实例
Nacos 的服务实例分为两种类型:
- 临时实例(默认):如果实例宕机超过一定时间,会被注册中心从服务列表剔除。
- 非临时实例:如果实例宕机,不会从服务列表剔除,也可以叫永久实例。
spring: cloud: nacos: discovery: ephemeral: false # 设置为非临时实例,默认是临时实例(true)
9.Nacos与Eureka的比较
(1)共同点
- 都支持服务注册和服务拉取(pull)
- 都支持服务提供者的心跳检测
- Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式
- 临时实例心跳不正常会被剔除,非临时实例则不会被剔除
- Nacos支持服务列表变更的消息推送(push)模式,服务列表更新更及时;Eureka只能进行pull
- Nacos集群默认采用AP方式(可用性),当集群中存在非临时实例时,采用CP模式(一致性);Eureka采用AP方式。
10.Nacos统一配置管理——配置中心
Nacos除了可以做注册中心,同样可以做配置管理来使用。
当微服务部署的实例越来越多时,逐个修改微服务配置就显得十分不便。因此需要一种统一配置管理的方案,可以集中管理所有实例的配置。
(1)创建配置
在Nacos中添加配置信息:
【tips】项目的核心配置和需要热更新的配置才有必要放到 nacos 中管理。基本不会变更的一些配置(例如数据库连接)还是保存在微服务本地比较好。
(2)拉取配置
1)Nacos 读取配置文件的流程
在没加入 Nacos 配置之前,微服务的配置信息都配置在各自的application.yml中,此时Nacos获取配置是这样:
而加入Nacos统一配置管理后,它的读取是在配置文件之前,但此时Nacos服务端地址是配置在之前的配置文件中,现在 Nacos 无法根据地址去获取配置了。 因此,Nacos 服务端地址必须放在优先级更高的 bootstrap.yml 文件中:
2)拉取配置的步骤
-
导入nacos-config坐标
<!--nacos配置管理依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency>
-
添加 bootstrap.yml并配置nacos服务端信息,这些配置信息决定了项目启动时去nacos读取哪个文件
spring: application: name: userservice # 服务名称 profiles: active: dev #所处环境,这里是dev cloud: nacos: server-addr: localhost:8848 # Nacos地址 config: file-extension: yaml # 文件后缀名
【tips】:bootstrap.yml中配置的信息要与Data ID一致:
11.配置热更新
(1)配置热更新:修改 nacos 中的配置信息后,微服务无需重启即可让配置生效,实现配置的自动刷新。
(2)配置热更新的两种方式
- 用 @value 读取配置时,搭配 @RefreshScope
- ★直接用 @ConfigurationProperties 读取配置(推荐)
-
在 user-service 服务中,添加一个 PatternProperties 类,读取 patterrn.dateformat 属性
@Data @Component @ConfigurationProperties(prefix = "pattern") public class PatternProperties { public String dateformat; }
@Autowired private PatternProperties patternProperties; @GetMapping("now2") public String now2(){ //格式化时间 return LocalDateTime.now().format(DateTimeFormatter.ofPattern(patternProperties.dateformat)); }
12.多环境配置共享
(1)在服务启动时,nacos 会读取多个配置文件:
- [spring.application.name]-[spring.profiles.active].yaml,例如:userservice-dev.yaml
- [spring.application.name].yaml,例如:userservice.yaml
(2)配置优先级
当 nacos 和服务本地有相同属性时,优先级有高低之分:
13.Nacos集群搭建
搭建一个有三个nacos节点,一个nginx反向代理(负载均衡)的nacos集群,这里以单点数据库为例。三个nacos节点的地址为:
- nacos1:127.0.0.1:8845
- nacos2:127.0.0.1:8846
- nacos3:127.0.0.1:8847
(1)初始化数据库
Nacos 默认数据存储在内嵌数据库 Derby 中,不属于生产可用的数据库。官方推荐的最佳实践是使用带有主从的高可用数据库集群,主从模式的高可用数据库。这里我们以单点的数据库为例。
首先新建一个数据库,命名为 nacos,导入资料中的SQL语句。
(2)配置nacos
1)进入 nacos 的 conf 目录,修改配置文件 cluster.conf.example,重命名为 cluster.conf。
2)在cluster.conf中添加三个nacos节点的地址:
#it is ip #example 127.0.0.1:8845 127.0.0.1.8846 127.0.0.1.88473)修改 application.properties 文件,添加数据库配置:
spring.datasource.platform=mysql db.num=1 db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC db.user.0=root db.password.0=12344)将 nacos 文件夹复制三份,分别命名为:nacos1、nacos2、nacos3,分别在各自application.properties 文件中修改端口号为8845、8846、8847。
5)分别启动三个nacos节点:
./startup.cmd
(3)配置Nginx反向代理
1)修改 nginx 文件夹下的 conf/nginx.conf 文件,配置如下:
upstream 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; } }2)启动nginx,在浏览器访问:http://localhost/nacos,访问成功表示nacos集群搭建成功
四、★基于Feign远程调用
1.基于RESTTemplate远程调用的问题
以前利用 RestTemplate 发起远程调用的代码:
- 代码可读性差,编程体验不统一
- 参数复杂URL难以维护
2.Feign简介
Feign 是一个声明式的 http 客户端,官方地址:https://github.com/OpenFeign/feign。
作用就是解决上面提到的问题,实现 http 请求的发送。
3.使用Feign实现远程调用
这里是order-service远程调用user-service,因此要在order-service中配置Feign客户端。
(1)导入坐标
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>(2)添加注释
在 order-service 启动类添加@EnableFeignClients注解开启 Feign
(3)编写FeignClient接口
- @FeignClient("user-service"):参数填写的是微服务名
- @GetMapping("/user/{id}"):参数填写的是请求路径
- 这个客户端主要是基于 SpringMVC 的注解 @GetMapping 来声明远程调用的信息
- Feign可以帮助我们发送 http 请求,因此无需自己使用 RestTemplate 来发送了
@FeignClient("user-service") public interface UserClient { @GetMapping("/user/{id}") User getById(@PathVariable("id") Long id); }(4)使用FeignClient中定义的方法代替RestTemplate
@Autowired private UserClient userClient; //基于Feign的远程调用 @GetMapping("{orderId}") public Order queryOrderByUserId(@PathVariable("orderId") Long orderId) { //1.根据id查询订单 Order order = orderService.queryOrderById(orderId); //2.查询用户 User user = userClient.getById(orderId); //3.将user信息封装到order中 order.setUser(user); //4.返回order return order; }
4.自定义Feign的配置
Feign 可以支持很多的自定义配置,如表所示:
【tips】日志级别分为四种:
- NONE:不记录任何日志信息(默认值)
- BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
- HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
- FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据
一般情况下,默认值就能满足使用,如果要自定义配置,有两种方式。以修改日志级别为例来演示使用和自定义配置:
(1)法一:基于配置文件自定义配置
(1)法一:基于配置文件自定义配置
-
全局生效,针对所有服务
feign: client: config: default: # 这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置 loggerLevel: FULL # 日志级别
-
局部生效,针对某个微服务
feign: client: config: user-service: # 针对某个微服务的配置 loggerLevel: FULL # 日志级别
(2)法二:基于Java代码自定义配置
声明一个配置类,在配置类中声明一个 Logger.Level 的对象
public class DefaultFeignConfiguration { @Bean public Logger.Level feignLogLevel(){ return Logger.Level.BASIC; // 日志级别为BASIC } }
-
全局生效:将上面的配置类放到启动类的 @EnableFeignClients 注解中
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class)
-
局部生效:将上面的配置类放到对应的 @FeignClient 注解中
@FeignClient(value = "user-service", configuration = DefaultFeignConfiguration.class)
5.性能优化
Feign 底层发起 http 请求,依赖于其它框架。其底层客户端实现有:
- URLConnection:默认实现,不支持连接池
- Apache HttpClient :支持连接池
- OKHttp:支持连接池
- 使用连接池代替默认的 URLConnection
- 日志级别应该尽量用 basic/none,可以有效提高性能。 这里用 Apache 的HttpClient来演示连接池。
(1)导入坐标
<!--httpClient的依赖 --> <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-httpclient</artifactId> </dependency>
(2)配置信息
feign: client: config: default: # default全局的配置 loggerLevel: BASIC # 日志级别,BASIC就是基本的请求和响应信息 httpclient: enabled: true # 开启feign对HttpClient的支持 max-connections: 200 # 最大的连接数 max-connections-per-route: 50 # 每个路径的最大连接数
6.Feign的最佳实践分析
基于Feign实现远程调用时,在消费者order-service和提供者user-service中存在相同的代码,对于这些相同代码,我们有两种处理方式:
(1)继承接口
- 定义一个 API 接口,利用定义方法,并基于 SpringMVC 注解做声明
- 让order-service中的 Feign 客户端和user-service中的Controller 都继承该接口
缺点:
- 服务提供方和服务消费方存在紧耦合
- 参数列表中的注解映射并不会继承,因此 Controller 中必须再次声明方法、参数列表、注解
(2)抽取模块
将 FeignClient 抽取为一个独立模块,并把接口有关的 pojo、默认的 Feign配置都放到这个模块中,提供给所有消费者使用。
例如:将 UserClient、User、Feign 的默认配置都抽取到一个 feign-api 包中,让所有微服务都依赖这个模块,即可直接使用。
1)创建新模块
2)导入坐标
<!--Feign客户端依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>3)将 order-service中 的 UserClient 和 pojo User 都复制到 feign-api 项目中
4)扫描FeignClient
当定义的 FeignClient 不在 SpringBootApplication 的扫描包范围下时,这些 FeignClient 就不能使用。
当定义的 FeignClient 不在 SpringBootApplication 的扫描包范围下时,这些 FeignClient 就不能使用。
解决方案:修改 order-service 启动类上的 @EnableFeignClients 注解,指定FeignClient所在的包或字节码文件。
@EnableFeignClients(basePackages = "com.itcast.feign.clients")
五、★统一网关Gateway
1.Gateway简介
Spring Cloud Gateway 是 Spring Cloud 的一个全新项目,该项目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等响应式编程和事件流技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。
Gateway 网关是服务的守门神,是所有微服务的统一入口。
在 SpringCloud 中网关的实现包括两种:
- gateway
- zuul
2.网关的作用
(1)权限控制
网关作为微服务入口,需要校验用户是否有请求资格,如果没有则进行拦截。
(2)路由和负载均衡
一切请求都必须先经过 gateway,但网关不处理业务,而是根据某种规则,把请求转发到某个微服务,这个过程叫做路由。当然路由的目标服务有多个时,还需要做负载均衡。
(3)限流
当请求流量过高时,在网关中按照下流的微服务能够接受的速度来放行请求,避免服务压力过大。
3.网关的入门使用
(1)新建模块 gateway,引入网关和服务发现依赖坐标
<!--网关--> <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>
(2)编写启动类
@SpringBootApplication public class GatewayApplication { public static void main(String[] args) { SpringApplication.run(GatewayApplication.class, args); } }
(3)编写路由配置、nacos地址和路由规则
server: port: 10010 # 网关端口 spring: application: name: gateway # 服务名称 cloud: nacos: server-addr: localhost:8848 # nacos地址 gateway: routes: # 网关路由配置 - id: user-service # 路由id,自定义,只要唯一即可 # uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址 uri: lb://userservice # 路由的目标地址 lb就是负载均衡,后面跟服务名称 predicates: # 路由断言,也就是判断请求是否符合路由规则的条件 - Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求
【tips】将符合 -Path 规则的一切请求,都代理到 uri参数指定的地址。上面的例子中,将 /user/** 开头的请求,代理到 lb://userservice,其中 lb 是负载均衡(LoadBalance),根据服务名拉取服务列表,实现负载均衡。
(4)启动网关服务进行测试
4.路由断言工厂(Route Predicate Factory)
上面在配置文件中写的断言规则只是字符串,这些字符串会被 Predicate Factory 读取并处理,转变为路由判断的条件。
例如 Path=/user/** 是按照路径匹配,这个规则是由 org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory 类来处理的。
Spring提供的11中基本的断言工厂:
5.路由过滤器GateFilter
(1)概述
GatewayFilter 是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理。
Spring提供了31种不同的路由过滤器工厂。官方文档:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gatewayfilter-factories
(2)局部过滤器
以 AddRequestHeader 为例,给所有进入 user-service 的请求添加一个请求头:Truth,China No.1!
1)在gateway的配置文件中配置过滤器
gateway: routes: # 网关路由配置 - id: user-service # 路由id,自定义,只要唯一即可 # uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址 uri: lb://user-service # 路由的目标地址 lb就是负载均衡,后面跟服务名称 predicates: # 路由断言,也就是判断请求是否符合路由规则的条件 - Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求 filters: - AddRequestHeader=Truth,China No.1! # 添加请求头2)获取请求头信息
在user-service中controller的任意方法添加一个参数:
@GetMapping("/{id}") public User queryById(@PathVariable("id") Long id, @RequestHeader String truth) { System.out.println("truth" + truth); return userService.queryById(id); }
(3)默认过滤器(default-filters)
默认过滤器对所有的路由都生效,则可以将过滤器工厂写到 default-filters 下:spring: cloud: gateway: default-filters: - AddRequestHeader=Key,value # 添加请求头
(4)全局过滤器(GlobalFilter)
上面介绍的网关提供的 31 种过滤器工厂,每一种过滤器的作用都是固定的。如果我们希望拦截请求,做自己的业务逻辑则没办法实现。
全局过滤器的作用也是处理一切进入网关的请求和微服务响应,这与 GatewayFilter 的作用一样。但区别在于 GlobalFilter 的逻辑可以写代码来自定义规则;而 GatewayFilter 通过配置定义,处理逻辑是固定的。
因此,GlobalFilter也可以理解为全局自定义过滤器。
下面以判断登录用户权限为例,演示GlobalFilter:
1)需求
定义全局过滤器,拦截请求,判断请求的参数是否满足下面条件,如果同时满足则放行,否则拦截:
- 参数中是否有 authorization authorization
- 参数值是否为 admin
- 创建自定义接口,实现GlobalFilter接口
-
重写filter方法
//@Order(-1) //设置过滤器优先级 @Component public class AuthorizeFilter implements GlobalFilter, Ordered { @Override 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.不满足要求,拦截 //5.1 设置拦截状态码 exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); //5.2 设置拦截 return exchange.getResponse().setComplete(); } /** * 设置过滤器优先级,数越小优先级越高 * @return */ @Override public int getOrder() { return -1; } }
(5)过滤器顺序
请求进入网关会碰到三类过滤器:DefaultFilter、当前路由过滤器、GlobalFilter。
请求路由后,网关会将三者合并到一个过滤器链(集合)中,排序后依次执行每个过滤器。
排序规则:
- 每一个过滤器都必须指定一个 int 类型的 order 值,order 值越小,执行顺序越靠前
- GlobalFilter 通过实现 Ordered 接口,或者使用 @Order 注解来指定 order 值,由我们自己指定
- 路由过滤器和 defaultFilter 的 order 由 Spring 指定,默认是按照声明顺序从1递增
- 当过滤器的 order 值一样时,会按照 default-filters > 路由过滤器 > GlobalFilter 的顺序执行。
(6)跨域问题处理
1)什么是跨域?
跨域:域名不一致就是跨域,主要包括:
- 域名不同: www.taobao.com 和 www.taobao.org 和 www.jd.com 和 miaosha.jd.com
- 域名相同,端口不同:localhost:8080和localhost8081
2)解决方案——CORS
网关处理跨域问题采用的是CORS方案,只需要简单配置即可实现:
spring: cloud: gateway: globalcors: # 全局的跨域处理 add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题 corsConfigurations: '[/**]': allowedOrigins: # 允许哪些网站的跨域请求 allowedOrigins: “*” 允许所有网站 - "http://localhost:8090" allowedMethods: # 允许跨域ajax的请求方式 - "GET" - "POST" - "DELETE" - "PUT" - "OPTIONS" allowedHeaders: "*" # 允许在请求中携带的头信息 allowCredentials: true # 是否允许携带cookie maxAge: 360000 # 这次跨域检测的有效期
六、Docker
1.简介
Docker是一个快速交付应用、运行应用的技术。目前大型项目组件较多,运行环境复杂,部署时会碰到一些问题:
- 依赖关系复杂,容易出现兼容性问题
- 开发、测试、生产环境有差异
(1)Docker解决依赖兼容问题
- Docker允许开发中将应用、依赖、函数库、配置一起打包,形成可移植镜像
- Docker应用运行在容器中,使用沙箱机制,相互隔离
(2)Docker解决不同生产环境的系统差异问题
- Docker将用户程序与所需要调用的系统(比如Ubuntu)函数库一起打包
- Docker运行到不同操作系统时,直接基于打包的库函数,借助于操作系统的Linux内核来运行
2.Docker与虚拟机的比较
- docker是一个系统进程;虚拟机是在操作系统中的操作系统
- docker体积小、启动速度快、性能好;虚拟机体积大、启动速度慢、性能一般
3.镜像和容器
(1)概念
- 镜像(Image):Docker将应用程序及其所需的依赖、函数库、环境、配置等文件打包在一起,称为镜像。
- 容器(Container):镜像中的应用程序运行后形成的进程就是容器,只是Docker会给容器做隔离,对外不可见。
(2)Docker和DockerHub
DockerHub是一个Docker镜像的托管平台,这样的平台称为Docker Registry。 国内也有类似于DockerHub 的公开服务,比如 网易云镜像服务、阿里云镜像库等。
(3)Docker架构
Docker是一个C-S架构的程序,由两部分组成:
- 服务端(server):Docker守护进程,负责处理Docker指令,管理镜像、容器等
- 客户端(client):通过命令或RestAPI向Docker服务端发送指令。可以在本地或远程向服务端发送指令。
4.安装Docker
将Docker安装到CentOS下。
(1)更新本地镜像源
yum-config-manager \ --add-repo \ https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo sed -i 's/download.docker.com/mirrors.aliyun.com\/docker-ce/g' /etc/yum.repos.d/docker-ce.repo yum makecache fast
(2)输入安装命令
yum install -y docker-ce(3)开启指定端口或直接关闭***
(4)启动、停止、重启docker、查看状态、版本命令
systemctl start docker # 启动docker服务 systemctl stop docker # 停止docker服务 systemctl restart docker # 重启docker服务 systemctl status docker #查看docker状态 docker -v # 查看docker版本(5)配置镜像加速
docker官方镜像仓库网速较差,需要设置国内镜像服务: 参考阿里云的镜像加速文档:https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors。
(6)开启开机自动启动docker
systemctl enable docker
5.Docker基本操作命令
(1)镜像操作命令
1)案例1:从DockerHub中拉取一个nginx镜像并查看
- 去镜像仓库(如DockerHub)搜索nginx镜像
- 通过命令:docker pull nginx 拉取需要的镜像
【tips】镜像名称一般分两部分组成:[repository]:[tag],tag指的是版本,不写默认最新版latest。
- 通过命令:docker images 查看拉取到的镜像
2)案例2:将nginx镜像导出磁盘,将案例1拉取的镜像删除,然后再加载回来(练习帮助文档--help的使用)
- 利用docker save --help命令查看打包命令docker save的用法
- 使用打包命令docker save -o nginx.tar nginx:latest将镜像打包
- 利用docker load --help命令查看docker load的用法
- 通过命令 docker rmi 将案例1拉取的镜像删除
- 通过命令 docker load -i 包名.tar 加载镜像nginx.tar
(2)容器操作命令
1)案例1:创建并运行一个Nginx容器
-
去docker hub查看Nginx的容器运行命令
docker run --name myNginx -p 80:80 -d nginx
-p 80:80:将宿主机端口与容器端口映射,左边是宿主机端口,右边是容器端口
-d:后台运行容器
nginx:基于nginx(:latest)镜像创建该容器
- 通过命令 docker ps 查看所有正在运行的容器和状态
【tips】-a:通过docker ps -a 可以查看所有容器和状态,包括运行和没运行的。
- 访问nginx,并通过 docker logs myNginx 查看myNginx容器日志
【tips】通过 docker logs -f myNginx 可以持续查看日志
2)进入myNginx容器,修改HTML文件内容,添加“hello Docker”,退出容器,停止容器并删除该容器
- 通过 docker exec -it myNginx bash 进入myNginx容器
-it:给当前进入的容器创建一个标准输入、输出终端,允许我们与容器交互
bash:进入容器后执行的命令,bash是一个linux终端交互命令。加上bash,进入容器后就可以用Linux的一些命令了。
-
进入nginx的HTML所在目录 /usr/share/nginx/html
cd /usr/share/nginx/html
-
修改index.html的内容
sed -i 's#Welcome to nginx#hello Docker#g' index.html
【tips】exec命令可以进入容器修改文件,但是一般不推荐在容器内修改文件。
- 使用exit命令退出容器
- 使用 docker stop myNginx 命令停止容器
- 使用 docker rm myNginx 命令删除容器
(3)数据卷(volume)操作命令
数据卷(volume):是一个虚拟目录,指向宿主机文件系统中的某个目录,二者相互关联,在数据卷中进行的修改会保存到宿主机对应目录中,同样地在宿主机修改也会影响数据卷中的内容。
1)容器与数据的耦合问题
- 不便于修改:当要修改容器的html内容时,需要进入容器内部修改,很不方便
- 数据不可复用:在容器内的修改对外是不可见的。所有修改对新创建的容器是不可复用的
- 升级维护困难:数据在容器内,如果要升级容器必然删除旧容器,所有数据都跟着删除了
将容器与数据分离,解耦合,方便操作容器内数据,保证数据安全。
3)操作命令
docker volume create Name | 创建一个名为Name的volume |
docker volume inspect Name |
显示指定Name或多个volume的信息 |
docker volume ls | 列出所有volume |
docker volume prune | 删除所有未使用的volume |
docker volume rm Name | 删除指定Name或多个volume |
(4)挂载数据卷
1)案例1:创建并运行mn容器,使用数据卷操作修改HTML文件内容,添加“hello Docker”
-
创建并运行mn容器,把html数据卷(位于宿主机)挂载到容器内的/usr/share/nginx/html这个目录
docker run --name mn -p 80:80 -v html:/usr/share/nginx/html -d nginx
-v html:/usr/share/nginx/html 将html这个数据卷挂载到容器内的/usr/share/nginx/html这个目录中
②目录挂载与数据卷挂载的语法是类似的:
-v [宿主机文件]:[容器内文件]
-v [宿主机目录]:[容器内目录]
-
查看html数据卷在宿主机中的位置
docker volume inspect html
-
进入挂载点目录并修改文件
#进入该目录 cd /var/lib/docker/volumes/html/_data # 修改文件 vim index.html
2)案例2:创建并运行一个MySQL容器,将宿主机目录直接挂载到容器目录
- 将资料中的mysql.tar文件上传到虚拟机,通过load命令加载为镜像
- 创建目录 /tmp/mysql/data 和目录 /tmp/mysql/conf,将资料提供的hmy.cnf文件上传到/tmp/mysql/conf
-
创建并运行MySQL容器,起名为mysql
docker run \ --name mysql \ -e MYSQL_ROOT_PASSWORD=1234 \ #设置mysql密码 -p 3306:3306 \ #将宿主机端口与容器端口映射 -v /usr/local/mysqlDocker/data:/var/lib/mysql \ #目录挂载:目录挂目录 -v /usr/local/mysqlDocker/conf/hmy.cnf:/etc/mysql/conf.d/hmy.cnf \ #目录挂载:文件挂文件 -d \ mysql:5.7.25
- 打开Navicat测试数据库连接
6.基于Dockerfile自定义镜像
(1)镜像结构
- 基础镜像层(BaseImage):包含基本的系统函数库、环境变量、文件系统
- 入口(Entrypoint):是镜像中应用程序启动的命令
- 其它层:在BaseImage基础上添加依赖、安装程序、完成整个应用的安装和配置
(2)Dockerfile
Dockerfile就是一个文本文件,其中包含若干指令(Instruction),用指令来说明要执行什么操作来构建镜像。每一个指令都会形成一层Layer。
常用指令:
更新详细语法说明,参考官网文档: https://docs.docker.com/engine/reference/builder。
# 指定基础镜像 FROM ubuntu:16.04 # 配置环境变量,JDK的安装目录 ENV JAVA_DIR=/usr/local # 拷贝jdk和java项目的包 COPY ./jdk8.tar.gz $JAVA_DIR/ COPY ./docker-demo.jar /tmp/app.jar # 安装JDK RUN cd $JAVA_DIR \ && tar -xf ./jdk8.tar.gz \ && mv ./jdk1.8.0_144 ./java8 # 配置环境变量 ENV JAVA_HOME=$JAVA_DIR/java8 ENV PATH=$PATH:$JAVA_HOME/bin # 暴露端口 EXPOSE 8090 # 入口,java项目的启动命令 ENTRYPOINT java -jar /tmp/app.jar
(3)自定义镜像
1)案例1:基于Ubuntu镜像构建一个新镜像,运行一个java项目
- 新建一个空文件夹docker-demo
- 拷贝资料中的docker-demo.jar文件到docker-demo目录
- 拷贝资料中的jdk8.tar.gz文件到docker-demo目录
- 拷贝资料提供的Dockerfile到docker-demo目录
-
在docker-demo目录下执行命令
docker build -t javaweb:1.0 .
-t:后面指定新镜像名和版本
.:这个"."表示Dockerfile所在目录
-
创建并运行web容器,并进行访问测试
docker run --name web -p 8090:8090 -d javaweb:1.0
2)案例2:基于java:8-alpine镜像构建一个新镜像,运行一个java项目
如果再要构建一个新的Java镜像,那么案例1中Dockerfile文件安装jdk、配置环境变量等指令需要重复去写,java:8-alpine镜像将这些指令构建的层封装成一个镜像,以后构建java项目只需要基于java:8-alpine为基础镜像构建即可。
- 新建一个空文件夹docker-demo2
- 拷贝资料中的docker-demo.jar文件到docker-demo2目录
-
重新编写Dockerfile文件:
# 指定基础镜像 FROM java:8-alpine #将app拷贝到镜像中 COPY ./docker-demo.jar /tmp/app.jar # 暴露端口 EXPOSE 8090 # 入口,java项目的启动命令 ENTRYPOINT java -jar /tmp/app.jar
-
在docker-demo目录下执行命令
docker build -t javaweb:2.0 .
-
创建并运行web容器,并进行访问测试
docker run --name web -p 8090:8090 -d javaweb:2.0
7.搭建私有镜像仓库
镜像仓库( Docker Registry )有公共的和私有的两种形式:
- 公共仓库:例如Docker官方的 Docker Hub,国内也有一些云服务商提供类似于 Docker Hub 的公开服务,比如 网易云镜像服务、DaoCloud 镜像服务、阿里云镜像服务等。
- 私有仓库:除了使用公开仓库外,用户还可以在本地搭建私有 Docker Registry。企业自己的镜像最好是采用私有Docker Registry来实现。
-
***采用的是http协议,默认不被Docker信任,所以搭建私有仓库前需要做一个配置:
# 打开要修改的文件 vi /etc/docker/daemon.json # 添加内容: "insecure-registries":["http://192.168.152.100:8080"] # 重加载 systemctl daemon-reload # 重启docker systemctl restart docker
-
使用DockerCompose部署带有图象界面的DockerRegistry,命令如下:
version: '3.0' services: registry: image: registry volumes: - ./registry-data:/var/lib/registry ui: image: joxit/docker-registry-ui:static ports: - 8080:80 environment: - REGISTRY_TITLE=Docker_resp - REGISTRY_URL=http://registry:5000 depends_on: - registry
- 访问测试,出现以下页面说明搭建成功:
(2)操作私有仓库
将nginx:latest推送到私有仓库:
-
推送本地镜像到仓库前都必须重命名(docker tag)镜像,并以镜像仓库地址为前缀
docker tag nginx:latest 192.168.152.100:8080/nginx:1.0
-
推送重命名后的镜像
docker push 192.168.152.100:8080/nginx:1.0
-
拉取镜像:复制代码并拉取
docker pull 192.168.152.100:8080/nginx:1.0
七、Docker Compose
1.简介
Docker Compose可以基于Compose文件进行快速部署分布式应用。Compose文件是一个文本文件,通过指令定义集群中的每个容器如何运行。
version: "3.8" services: mysql: image: mysql:5.7.25 environment: MYSQL_ROOT_PASSWORD: 123 volumes: - "/tmp/mysql/data:/var/lib/mysql" - "/tmp/mysql/conf/hmy.cnf:/etc/mysql/conf.d/hmy.cnf" web: build: . ports: - "8090:8090"【tips】可以看出Compose文件中的指令与Dockerfile文件中的指令是对应的。DockerCompose的详细语法参考官网:https://docs.docker.com/compose/compose-file/。
2.安装DockerCompose
(1)下载DockerCompose或上传资料中的安装包
(2)修改文件权限为可执行(+x)
chmod +x /usr/local/bin/docker-compose(3)配置Base自动补全命令
curl -L https://raw.githubusercontent.com/docker/compose/1.29.1/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose【tips】若报错,修改hosts文件:
echo "199.232.68.133 raw.githubusercontent.com" >> /etc/hosts
3.基于DockerCompose部署微服务
将cloud-demo微服务集群利用DockerCompose部署到Linux虚拟机。
(1)实现思路
- 查看提供的cloud-demo文件夹,里面已经编写好了docker-compose文件
- 修改cloud-demo项目,将数据库、nacos地址都命名为docker-compose中的服务名
- 使用maven打包工具,将项目中的每个微服务都打包为app.jar
- 将打包好的app.jar拷贝到cloud-demo中的每一个对应的子目录中
- 将cloud-demo上传至虚拟机,利用 docker-compose up -d 来部署
- 资料提供的cloud-demo文件夹,里面已经编写好了docker-compose文件,而且每个微服务都准备了一个独立的目录:
-
docker-compose文件内容如下:
version: "3.2" # 5个微服务 services: # nacos:作为注册中心和配置中心 nacos: # 自定义容器名,下同 image: nacos/nacos-server # 基于nacos/nacos-server镜像构建 environment: MODE: standalone # 单点模式启动 ports: - "8848:8848" mysql: image: mysql:5.7.25 # 基于mysql 5.7.25版本构建镜像 environment: MYSQL_ROOT_PASSWORD: 123 # 数据卷挂载,这里挂载了mysql的data、conf目录 volumes: - "$PWD/mysql/data:/var/lib/mysql" - "$PWD/mysql/conf:/etc/mysql/conf.d/" # userservice、orderservice、gateway:都是基于各自文件夹下的Dockerfile文件临时构建的镜像 userservice: build: ./user-service orderservice: build: ./order-service gateway: build: ./gateway ports: - "10010:10010"
- 查看微服务目录,可以看到每一个微服务都包含Dockerfile文件:
-
Dockerfile文件内容如下:
FROM java:8-alpine # 基于java:8-alpine构建镜像 COPY ./app.jar /tmp/app.jar # 将当前目录下的app.jar包拷贝到/tmp目录下 ENTRYPOINT java -jar /tmp/app.jar # 执行启动java项目的命令
-
修改微服务配置:因为微服务要部署为docker容器,而容器之间互联不是通过IP地址,而是通过容器名。因此需要将order-service、user-service、gateway模块的mysql、nacos地址配置都修改为基于容器名的访问:
spring: datasource: url: jdbc:mysql://mysql:3306/cloud_order?useSSL=false username: root password: 123 driver-class-name: com.mysql.jdbc.Driver application: name: orderservice cloud: nacos: server-addr: nacos:8848 # nacos服务地址
-
打包:将每个微服务都打包。因为之前查看到Dockerfile中的jar包名称都是app.jar,因此每个微服务都需要用这个名称。 可以通过修改pom.xml中的打包名称来实现,每个微服务都需要修改:
<build> <!-- 服务打包的最终名称 --> <finalName>app</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
- 编译打包好的app.jar文件,需要放到Dockerfile的同级目录中
-
将cloud-demo文件夹上传到虚拟机中,由DockerCompose部署:
docker-compose up -d