Spring Cloud
- 什么是微服务
- Spring Cloud
- 提供者与消费者
- 环境搭建
- 服务注册中心
- Hystrix组件使用
- Gateway组件使用
什么是微服务
- 官方定义:微服务就是由一系列围绕自己业务开发的微小服务构成,他们独立部署运行在自己的进程里,基于分布式的管理
- 通俗定义:微服务是一种架构,这种架构是将单个的整体应用程序分割成更小的项目关联的独立的服务。一个服务通常实现一组独立的特性或功能,包含自己的业务逻辑和适配器。各个微服务之间的关联通过暴露api来实现。这些独立的微服务不需要部署在同一个虚拟机,同一个系统和同一个应用服务器中。
单体应用
优点
- 单一架构模式在项目初期很小的时候开发方便,测试方便,部署方便,运行良好。
缺点
- 应用随着时间的推进,加入的功能越来越多,最终会变得巨大,一个项目中很有可能数百万行的代码,互相之间繁琐的jar包。
- 久而久之,开发效率低,代码维护困难
- 还有一个如果想整体应用采用新的技术,新的框架或者语言,那是不可能的。
- 任意模块的漏洞或者错误都会影响这个应用,降低系统的可靠性
微服务架构
优点
- 将服务拆分成多个单一职责的小的服务,进行单独部署,服务之间通过网络进行通信
- 每个服务应该有自己单独的管理团队,高度自治
- 服务各自有自己单独的职责,服务之间松耦合,避免因一个模块的问题导致服务崩溃
缺点
- 开发人员要处理分布式系统的复杂性
- 多服务运维难度,随着服务的增加,运维的压力也在增大
- 服务治理 和 服务监控 关键
微服务是一种经过良好架构设计的分布式架构方案,
微服务架构特征:
- 单一职责:微服务拆分粒度更小,每一个服务都对应唯一的业务能力,做到单一职责,避免重复业务开发
- 面向服务:微服务对外暴露业务接口
- 自治:团队独立、技术独立、数据独立、部署独立
- 隔离性强:服务调用做好隔离、容错、降级,避免出现级联问题
Spring Cloud
springcloud是一个含概多个子项目的开发工具集,集合了众多的开源框架,他利用了Spring Boot开发的便利性实现了很多功能,如服务注册,服务注册发现,负载均衡等.SpringCloud在整合过程中主要是针对Netflix(耐非)开源组件的封装.SpringCloud的出现真正的简化了分布式架构的开发
- Spring Cloud NetFlix
基于美国Netflix公司开源的组件进行封装,提供了微服务一栈式的解决方案。
- Spring Cloud alibaba
在Spring cloud netflix基础上封装了阿里巴巴的微服务解决方案。
- Spring Cloud Spring
目前spring官方趋势正在逐渐吸收Netflix组件的精华,并在此基础进行二次封装优化,打造spring专有的解决方案
核心架构及其组件
# 1.核心组件说明
- eurekaserver、consul、nacos 服务注册中心组件
- rabbion & openfeign 服务负载均衡 和 服务调用组件
- hystrix & hystrix dashboard 服务断路器 和 服务监控组件
- zuul、gateway 服务网关组件
- config 统一配置中心组件
- bus 消息总线组件
版本
# 1.版本选择官方建议 https://spring.io/projects/spring-cloud
- Angel 版本基于springboot1.2.x版本构建与1.3版本不兼容
- Brixton 版本基于springboot1.3.x版本构建与1.2版本不兼容
`2017年Brixton and Angel release官方宣布报废
- Camden 版本基于springboot1.4.x版本构建并在1.5版本通过测试
`2018年Camden release官方宣布报废
- Dalston、Edgware 版本基于springboot1.5.x版本构建目前不能再springboot2.0.x版本中使用
`Dalston(达尔斯顿)将于2018年12月官方宣布报废。Edgware将遵循Spring Boot 1.5.x的生命周期结束。
- Finchley 版本基于springboot2.0.x版本进行构建,不能兼容1.x版本
- Greenwich 版本基于springboot2.1.x版本进行构建,不能兼容1.x版本
- Hoxton 版本基于springboot2.2.x版本进行构建
提供者与消费者
服务提供者:一次业务中,被其它微服务调用的服务。(提供接口给其它微服务)
服务消费者:一次业务中,调用其它微服务的服务。(调用其它微服务提供的接口)
服务调用关系
服务提供者:暴露接口给其它微服务调用
服务消费者:调用其它微服务提供的接口
提供者与消费者角色其实是相对的
一个服务可以同时是服务提供者和服务消费者
环境搭建
创建一个空项目
创建一个普通的maven项目,作为父模块删除src
在maven父项目依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.blb</groupId>
<artifactId>springcloud_parent</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- 继承springboot的父项目-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
</parent>
<!--定义springcloud使用版本号-->
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR6</spring-cloud.version>
</properties>
<!--全局管理springcloud版本,并不会引入具体依赖-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
服务注册中心
所谓服务注册中心就是在整个的微服务架构中单独提出一个服务,这个服务不完成系统的任何的业务功能,仅仅用来完成对整个微服务系统的服务注册和服务发现,以及对服务健康状态的监控和管理功能
# 1.服务注册中心
- 可以对所有的微服务的信息进行存储,如微服务的名称、IP、端口等
- 可以在进行服务调用时通过服务发现查询可用的微服务列表及网络地址进行服务调用
- 可以对所有的微服务进行心跳检测,如发现某实例长时间无法访问,就会从服务注册表移除该实例。
常用的注册中心
springcloud支持的多种注册中心Eureka、Consul、Zookeeper、以及阿里巴巴推出Nacos。这些注册中心在本质上都是用来管理服务的注册和发现以及服务状态的检查的。
Eureka
Eureka是Netflix开发的服务发现框架,本身是一个基于REST的服务。SpringCloud将它集成在其子项目spring-cloud-netflix中, 以实现SpringCloud的服务注册和发现功能。
Eureka包含两个组件:Eureka Server和Eureka Client。
eureka的作用
服务提供者启动时向eureka注册自己的信息
eureka保存这些信息
消费者根据服务名称向eureka拉取提供者信息
服务消费者利用负载均衡算法,从服务列表中挑选一个
服务提供者会每隔30秒向EurekaServer发送心跳请求,报告健康状态
eureka会更新记录服务列表信息,心跳不正常会被剔除
消费者就可以拉取到最新的信息
在父项目下创建eureka模块
引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--引入 eureka server-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
配置文件
#eureka server端口号 默认是8761
server:
port: 8761
#指定服务名称,注意服务名不能出现下划线 默认服务名不区分大小写
spring:
application:
name: EUREKASERVER
#eureka server服务注册中心地址 暴露服务地址
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
#关闭 eureka client的立即注册
register-with-eureka: false
#让当前应用仅仅作为服务注册中心
fetch-registry: false
server.port=8761
spring.application.name=eurekaserver
eureka.client.service-url.defaultZone=http://localhost:8761/eureka
eureka.client.register-with-eureka=false #不再将自己同时作为客户端进行注册
eureka.client.fetch-registry=false #关闭作为客户端时从eureka server获取服务信息
开启Eureka Server,入口类加入注解
package com.blb;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer//开启当前应用是一个服务注册中心
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class,args);
}
}
访问
http://localhost:8761/
Eureka Client
新建一个maven项目
依赖
加入依赖
<!--引入eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
配置文件
server.port=8888 #服务端口号
spring.application.name=eurekaclient8888 #服务名称唯一标识
eureka.client.service-url.defaultZone=http://localhost:8761/eureka #eureka注册中心地址
server:
port: 8080
spring:
application:
name: eurekaclient
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
开启eureka客户端加入注解
package com.blb;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient //让当前微服务作为一个eureka serve 客户端 进行微服务注册
public class EurekaClientApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaClientApplication.class,args);
}
}
启动之前的8761的服务注册中心,在启动eureka客户端服务
查看eureka server的服务注册情况
eureka自我保护机制
默认情况下,如果Eureka Server在一定时间内(默认90秒)没有接收到某个微服务实例的心跳,Eureka Server将会移除该实例。但是当网络分区故障发生时,微服务与Eureka Server之间无法正常通信,而微服务本身是正常运行的,此时不应该移除这个微服务,所以引入了自我保护机制。Eureka Server在运行期间会去统计心跳失败比例在 15 分钟之内是否低于 85%,如果低于 85%,Eureka Server 会将这些实例保护起来,让这些实例不会过期。这种设计的哲学原理就是"宁可信其有不可信其无!"。自我保护模式正是一种针对网络异常波动的安全保护措施,使用自我保护模式能使Eureka集群更加的健壮、稳定的运行
在eureka server端关闭自我保护机制
eureka.server.enable-self-preservation=false #关闭自我保护
eureka.server.eviction-interval-timer-in-ms=3000 #超时3s自动清除
官方不建议关闭
微服务修改减短服务心跳的时间
eureka.instance.lease-expiration-duration-in-seconds=10 #用来修改eureka server默认接受心跳的最大时间 默认是90s
eureka.instance.lease-renewal-interval-in-seconds=5 #指定客户端多久向eureka server发送一次心跳 默认是30s
eureka集群
建立三个项目
配置文件分别为
服务端集群
8761配置
server.port=8761
spring.application.name=eurekaserver
eureka.client.service-url.defaultZone=http://localhost:8762/eureka,http://localhost:8763/eureka
eureka.client.register-with-eureka=false #不再将自己同时作为客户端进行注册
eureka.client.fetch-registry=false #关闭作为客户端时从eureka server获取服务信息
8762配置
server.port=8762
spring.application.name=eurekaserver
eureka.client.service-url.defaultZone=http://localhost:8761/eureka,http://localhost:8763/eureka
eureka.client.register-with-eureka=false #不再将自己同时作为客户端进行注册
eureka.client.fetch-registry=false #关闭作为客户端时从eureka server获取服务信息
8763配置
server.port=8763
spring.application.name=eurekaserver
eureka.client.service-url.defaultZone=http://localhost:8761/eureka,http://localhost:8762/eureka
eureka.client.register-with-eureka=false #不再将自己同时作为客户端进行注册
eureka.client.fetch-registry=false #关闭作为客户端时从eureka server获取服务信息
客户端配置
server.port=8888 #服务端口号
spring.application.name=eurekaclient8888 #服务名称唯一标识
eureka.client.service-url.defaultZone=http://localhost:8761/eureka,http://localhost:8762/eureka,http://localhost:8763/eureka #eureka注册中心地址多个
客户端集群
配置
只需服务名一致,端口不一样即可
server.port=8888 #服务端口号
spring.application.name=eurekaclient8888 #服务名称唯一标识
eureka.client.service-url.defaultZone=http://localhost:8761/eureka,http://localhost:8762/eureka,http://localhost:8763/eureka #eureka注册中心地址
server.port=8889 #服务端口号
spring.application.name=eurekaclient8888 #服务名称唯一标识
eureka.client.service-url.defaultZone=http://localhost:8761/eureka,http://localhost:8762/eureka,http://localhost:8763/eureka #eureka注册中心地址
为了方便idea也提供了简单的方式
Consul
consul是一个可以提供服务发现,健康检查,多数据中心,Key/Value存储等功能的分布式服务框架,用于实现分布式系统的服务发现与配置。与其他分布式服务注册与发现的方案,使用起来也较为简单。Consul用Golang实现,因此具有天然可移植性(支持Linux、Windows和Mac OS X);安装包仅包含一个可执行文件,方便部署。
Consul安装
在指定目录解压即可,注意目录不要包含中文
启动 Consul
启动服务注册中心,在consul安装目录下打开cmd
consul agent -dev
访问 Consul
http:端口 默认 8500
http://localhost:8500
开发consul 客户端
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--引入consul依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
编写配置文件
server:
port: 8083
spring:
application:
name: consulclient
cloud:
consul:
host: localhost
port: 8500
server.port=8003
spring.application.name=consulclient8889
spring.cloud.consul.host=localhost #注册consul服务的主机
spring.cloud.consul.port=8500 #注册consul服务的端口号
spring.cloud.consul.discovery.register-health-check=false #关闭consu了服务的健康检查[不推荐]
spring.cloud.consul.discovery.service-name=${
spring.application.name} #指定注册的服务名称 默认就是应用名
启动服务查看consul界面服务信息
发现服务不能使用,原因如下
consul server 检测所有客户端心跳,但是发送心跳时必须给与响应该服务才能正常使用,现在所有客户端中并没有引入健康检查依赖,所以导致健康检查始终不通过,导致服务不能使用
consul 开启健康监控检查
默认情况consul监控健康是开启的,但是必须依赖健康监控依赖才能正确监控健康状态所以直接启动会显示错误,引入健康监控依赖之后服务正常
<!-- 这个包是用做健康度监控的-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
不同注册中心区别
# 1.CAP定理
- CAP定理:CAP定理又称CAP原则,指的是在一个分布式系统中,一致性(Consistency)、可用性
- (Availability)、分区容错性(Partition tolerance)。CAP 原则指的是,这三个要素最多只能同时实现两
- 点,不可能三者兼顾。
`一致性(C):在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的
数据副本)
`可用性(A):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用
性)
`分区容忍性(P),就是高可用性,一个节点崩了,并不影响其它的节点(100个节点,挂了几个,不影响服务,
越多机器越好)
# 2.Eureka特点
- Eureka中没有使用任何的数据强一致性算法保证不同集群间的Server的数据一致,仅通过数据拷贝的方式争取注册
- 中心数据的最终一致性,虽然放弃数据强一致性但是换来了Server的可用性,降低了注册的代价,提高了集群运行
- 的健壮性。
# 3.Consul特点
- 基于Raft算法,Consul提供强一致性的注册中心服务,但是由于Leader节点承担了所有的处理工作,势必加大了注
- 册和发现的代价,降低了服务的可用性。通过Gossip协议,Consul可以很好地监控Consul集群的运行,同时可以方
- 便通知各类事件,如Leader选择发生、Server地址变更等。
-
# 4.zookeeper特点
- 基于Zab协议,Zookeeper可以用于构建具备数据强一致性的服务注册与发现中心,而与此相对地牺牲了服务的可用
- 性和提高了注册需要的时间。
服务间通信方式
在springcloud中服务间调用方式主要是使用 http restful方式进行服务间调用
基于RestTemplate的服务调用
spring框架提供的RestTemplate类可用于在应用中调用rest服务,它简化了与http服务的通信方式,统一了RESTful的标准,封装了http链接, 我们只需要传入url及返回值类型即可。相较于之前常用的HttpClient,RestTemplate是一种更优雅的调用RESTful服务的方式。
创建两个服务并注册到consul注册中心中
- users 代表用户服务 端口为 9999
- orders 代表商品服务 端口为 8888
`注意:这里服务仅仅用来测试,没有实际业务意义
两个服务的配置文件
server:
port: 9999
spring:
application:
name: users
cloud:
consul:
host: localhost
port: 8500
server:
port: 8888
spring:
application:
name: orders
cloud:
consul:
host: localhost
port: 8500
@RestController
public class OrderController {
@Value("${server.port}")
private int port;
@RequestMapping("/order")
public String order(){
return "order服务被调用 当前提供服务的端口"+port;
}
}
@RestController
public class UserController {
@RequestMapping("/user")
public String user(){
RestTemplate restTemplate=new RestTemplate();
String order = restTemplate.getForObject("http://localhost:8888/order", String.class);
return "user ok 结果为:" +order;
}
}
rest Template是直接基于服务地址调用没有在服务注册中心获取服务,也没有办法完成服务的负载均衡如果需要实现服务的负载均衡需要自己书写服务负载均衡策略
ForObject
返回的是响应结果
get请求
Map goods = restTemplate.getForObject(BaseURL+"findGoodsById?goodsId=12", Map.class);
System.out.println(goods.get("goodsName"));
post请求(发送的是json串)
返回的是响应体
Map goods = restTemplate.postForObject(BaseURL + "/save", newGoods("huawei", 99.99), Map.class);
System.out.println(goods.get("code"));
ForEntity
get请求
ResponseEntity<Goods> forEntity = restTemplate.getForEntity(BaseURL +"findGoodsById?goodsId=12", Goods.class);
System.out.println("http status:"+forEntity.getStatusCode());
System.out.println("http response body:"+forEntity.getBody());
post请求
ResponseEntity<Map> responseEntity = restTemplate.postForEntity(BaseURL+ "/save", new Goods("huawei", 99.99), Map.class);
System.out.println("http status:"+responseEntity.getStatusCode());
System.out.println("http response body:"+responseEntity.getBody());
Ribbon负载均衡
Spring Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,它基于Netflix Ribbon实现。通过Spring Cloud的封装,可以让我们轻松地将面向服务的REST模版请求自动转换成客户端负载均衡的服务调用
依赖
1.如果使用的是eureka client 和 consul client,无须引入依赖,因为在eureka,consul中默认集成了ribbon组件
2.如果使用的client中没有ribbon依赖需要显式引入如下依赖
<!--引入ribbon依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
使用restTemplate + ribbon进行服务调用
- 使用discoveryclient 进行客户端调用
- 使用loadBalanceClient 进行客户端调用
- 使用@loadBalanced 进行客户端调用
- discoveryclient 服务发现对象 根据服务idu服务注册中心获取对应的服务列表到本地中
缺点:没有负载均衡,需要自己实现负载均衡
- LoadBalancerClient 负载均衡对象客户端,根据服务id去服务注册中心获取服务列表,根据默认负载均衡策略,选择列表中一台机器进行返回
缺点:使用时需要每次先根据服务id获取一个负载均衡机器之后再通过restTemplate调用服务
- @LoadBalance 负载均衡客户端注解
修饰范围:作用在方法上
作用: 让当前方法 当前对象具有负载均衡特性
使用discovery Client形式调用
@Autowired
private DiscoveryClient discoveryClient;
//获取服务列表
List<ServiceInstance> products = discoveryClient.getInstances("服务ID");
for (ServiceInstance product : products) {
log.info("服务主机:[{}]",product.getHost());
log.info("服务端口:[{}]",product.getPort());
log.info("服务地址:[{}]",product.getUri());
log.info("====================================");
}
@RestController
public class UserController {
private static final Logger log = LoggerFactory.getLogger(UserController.class);
@Autowired
private DiscoveryClient discoveryClient; //服务注册发现客户端对象
@RequestMapping("/user")
public String user(){
RestTemplate restTemplate=new RestTemplate();
List<ServiceInstance> ordersInstances = discoveryClient.getInstances("orders");
ordersInstances.forEach(ordersInstance ->{
log.debug("服务主机: {} 服务端口:{} 服务地址:{}",ordersInstance.getHost(),ordersInstance.getPort(),ordersInstance.getUri());
});
String order = restTemplate.getForObject(ordersInstances.get(0).getUri()+"/order", String.class);
return "user ok 结果为:" +order;
}
}
使用loadBalance Client形式调用
@Autowired
private LoadBalancerClient loadBalancerClient;
//根据负载均衡策略选取某一个服务调用
ServiceInstance product = loadBalancerClient.choose("服务ID");
log.info("服务主机:[{}]",product.getHost());
log.info("服务端口:[{}]",product.getPort());
log.info("服务地址:[{}]",product.getUri());
@RestController
public class UserController {
private static final Logger log = LoggerFactory.getLogger(UserController.class);
@Autowired
private LoadBalancerClient loadBalancerClient;
@RequestMapping("/user")
public String user(){
RestTemplate restTemplate=new RestTemplate();
ServiceInstance serviceInstance = loadBalancerClient.choose("orders");
log.debug("服务主机: {} 服务端口:{} 服务地址:{}",serviceInstance.getHost(),serviceInstance.getPort(),serviceInstance.getUri());
String order = restTemplate.getForObject(serviceInstance.getUri()+"/order", String.class);
return "user ok 结果为:" +order;
}
}
可以看出就是轮询的方式跳转
使用@loadBalanced
//1.整合restTemplate + ribbon
@Bean
@LoadBalanced
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
//2.调用服务位置注入RestTemplate
@Autowired
private RestTemplate restTemplate;
//3.调用
String forObject = restTemplate.getForObject("http://服务名/路径",String.class);
@Configuration
public class beanConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
Ribbon负载均衡策略
负载均衡的规则都定义在IRule接口中,而IRule有很多不同的实现类:
内置负载均衡规则类 | 规则描述 |
---|---|
RoundRobinRule | 简单轮询服务列表来选择服务器。它是Ribbon默认的负载均衡规则。 |
AvailabilityFilteringRule | 对以下两种服务器进行忽略: (1)在默认情况下,这台服务器如果3次连接失败,这台服务器就会被设置为“短路”状态。短路状态将持续30秒,如果再次连接失败,短路的持续时间就会几何级地增加。 (2)并发数过高的服务器。如果一个服务器的并发连接数过高,配置了AvailabilityFilteringRule规则的客户端也会将其忽略。并发连接数的上限,可以由客户端的..ActiveConnectionsLimit属性进行配置。 |
WeightedResponseTimeRule | 为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择服务器,这个权重值会影响服务器的选择。 |
ZoneAvoidanceRule | 以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询。 |
BestAvailableRule | 忽略那些短路的服务器,并选择并发数较低的服务器。 |
RandomRule | 随机选择一个可用的服务器。 |
RetryRule | 重试机制的选择逻辑 |
默认的实现就是ZoneAvoidanceRule,是一种轮询方案
负载均衡原理
基本流程如下:
- 拦截我们的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,发起真实请求
修改服务的默认负载均衡策略
方式一
修改配置类,或在启动类定义一个新的IRule:
@Bean
public IRule randomRule(){
return new RandomRule();
}
方式二
配置文件方式:在order-service的application.yml文件中,添加新的配置也可以修改规则:
服务名.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule
products.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule
userservice: # 给某个微服务配置负载均衡规则,这里是userservice服务
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则
两者区别,方式一是访问所有服务都是随机,方式而是访问userservice这一种服务是随机
饥饿加载
Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长。
而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,通过下面配置开启饥饿加载:
ribbon:
eager-load:
enabled: true
clients: userservice
OpenFeign 组件
Feign是一个声明式的伪Http客户端,它使得写Http客户端变得更简单。使用Feign,只需要创建一个接口并注解。它具有可插拔的注解特性(可以使用springmvc的注解),可使用Feign 注解和JAX-RS注解。Feign支持可插拔的编码器和解码器。Feign默认集成了Ribbon,默认实现了负载均衡的效果并且springcloud为feign添加了springmvc注解的支持。
RestTemplate+ribbon的问题
- 1.每次调用服务都需要写这些代码,存在大量的代码冗余
- 2.服务地址如果修改,维护成本增高
- 3.使用时不够灵活
引入依赖OpenFeign依赖
<!--Open Feign依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
入口类加入注解开启OpenFeign支持
@EnableFeignClients //开启openfein客户端调用
@SpringBootApplication
@EnableDiscoveryClient //开启服务注册
@EnableFeignClients //开启openfein客户端调用
public class CategoryApplication {
public static void main(String[] args) {
SpringApplication.run(CategoryApplication.class,args);
}
}
创建一个客户端调用接口
package com.blb.feignclient;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
//调用商品服务的接口
@FeignClient(value = "PRODUCT") //value :用来书写调用服务的服务id
public interface ProductClient {
@GetMapping("/product") //服务调用路径
public String product(); //返回类型必须一致,方法名可以不一致
}
这个客户端主要是基于SpringMVC的注解来声明远程调用的信息,比如:
- 服务名称:PRODUCT
- 请求方式:GET
- 请求路径:/product
- 请求参数:xxx
- 返回值类型:String
这样,Feign就可以帮助我们发送http请求,无需自己使用RestTemplate来发送了。
使用feignClient客户端对象调用服务
product服务
@RestController
public class ProductController {
private static final Logger log = LoggerFactory.getLogger(ProductController.class);
@Value("${server.port}")
private int port;
@GetMapping("/product")
public String product(){
log.debug("商品服务");
return "product ok,当前提供服务端口: "+port;
}
}
Category服务
@RestController
public class CategoryController {
private static final Logger log = LoggerFactory.getLogger(CategoryController.class);
//注入客户端对象
@Autowired
private ProductClient productClient;
@GetMapping("/category")
public String category(){
log.debug("category service....");
//1. RestTemplate 2. RestTemplate+Ribbon 3.Openfeign
String product = productClient.product();
return "category ok"+product;
}
}
使用步骤
使用Feign的步骤:
① 引入依赖
② 添加@EnableFeignClients注解
③ 编写FeignClient接口
④ 使用FeignClient中定义的方法代替RestTemplate
可以看到比原来RestTemplate的调用方便了许多,
负载均衡
复制了一个product服务,端口号为7002
多次刷新访问
可以看出默认的负载均衡策略也是 轮询的方式,因为底层也是使用的ribbon
调用服务并传参
服务和服务之间通信,不仅仅是调用,往往在调用过程中还伴随着参数传递,接下来重点来看看OpenFeign在调用服务时如何传递参数
参数传递类型
- 传递零散类型参数
- 传递对象类型
- 数组或集合类型
零散类型传递
querystring方式传递参数
注意:在openfeign接口中声明必须要给参数加入注解 @requestParam
public class ProductController {
private static final Logger log = LoggerFactory.getLogger(ProductController.class);
@Value("${server.port}")
private int port;
@GetMapping("/product")
public String product(String name,Integer age){
log.debug("商品服务: name:{},age :{}",name,age);
return "product ok,当前提供服务端口: "+port+"name: "+name+"age: "+age;
}
}
//调用商品服务的接口
@FeignClient(value = "PRODUCT") //value :用来书写调用服务的服务id
public interface ProductClient {
@GetMapping("/product") //服务调用路径
//@RequestParam("xxx")一定要与被调用服务的参数一样否则springmvc识别不了
public String product(@RequestParam("name") String name,@RequestParam("age") int age); //返回类型必须一致,方法名可以不一致
}
@RestController
public class CategoryController {
private static final Logger log = LoggerFactory.getLogger(CategoryController.class);
@Autowired
private ProductClient productClient;
@GetMapping("/category")
public String category(){
log.debug("category service....");
//1. RestTemplate 2. RestTemplate+Ribbon 3.Openfeign
String product = productClient.product("dyk",18);
return "category ok"+product;
}
}
路径传递参数
注意:在openfeign接口声明中必须给参数加入注解 @PathVariable
//调用商品服务的接口
@FeignClient(value = "PRODUCT") //value :用来书写调用服务的服务id
public interface ProductClient {
//路径传参
@GetMapping("/test/{id}/{name}")
public String test(@PathVariable("id") Integer id, @PathVariable("name") String name);
}
@RestController
public class ProductController {
@GetMapping("/test/{id}/{name}")
public String test(@PathVariable("id") Integer id,@PathVariable("name") String name){
return "id: "+id+" name: "+name;
}
}
对象类型参数传递
实体类
public class Product {
private Integer id;
private String name;
private double price;
private Date bir;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
public Date getBir() {
return bir;
}
public void setBir(Date bir) {
this.bir = bir;
}
@Override
public String toString() {
return "Product{" +
"id=" + id +
", name='" + name + '\'' +
", price=" + price +
", bir=" + bir +
'}';
}
}
form表单方式
application/json方式
两边都必须加上@RequestBody注解
@RestController
public class ProductController {
@PostMapping("/test1")
public Product test1(@RequestBody Product product){
return product;
}
}
//调用商品服务的接口
@FeignClient(value = "PRODUCT") //value :用来书写调用服务的服务id
public interface ProductClient {
@PostMapping("/test1")
public Product test1(@RequestBody Product product);
}
数组和集合
数组
注意:在openfeign传递数组的接口声明中必须给参数加入注解 @RequsetParam
@RestController
public class ProductController {
@GetMapping("/test2")
public String test2(String [] ids){
return ids.toString();
}
}
//调用商品服务的接口
@FeignClient(value = "PRODUCT") //value :用来书写调用服务的服务id
public interface ProductClient {
@GetMapping("/test2")
public String test2(@RequestParam("ids") String [] ids);
}
集合
springmvc不能直接接收集合类型参数,如果想要接收集合参数必须将集合放入对象中,使用对象的方式接收
public class CollectionVo {
private List<String> ids;
public List<String> getIds() {
return ids;
}
public void setIds(List<String> ids) {
this.ids = ids;
}
@Override
public String toString() {
return "CollectionVo{" +
"ids=" + ids +
'}';
}
}
@GetMapping("/test3")
public String test3(CollectionVo collectionVo){
return collectionVo.toString();
}
@GetMapping("/test3")
public String test3(@RequestParam("ids") String [] ids);
自定义配置
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即可。
OpenFeign超时设置
默认情况下,openFiegn在进行服务调用时,要求服务提供方处理业务逻辑时间必须在1S内返回,如果超过1S没有返回则OpenFeign会直接报错,不会等待服务执行,但是往往在处理复杂业务逻辑是可能会超过1S,因此需要修改OpenFeign的默认服务调用超时时间
模拟超时
- 服务提供方加入线程等待阻塞
@GetMapping("/test/{id}/{name}")
public String test(@PathVariable("id") Integer id,@PathVariable("name") String name) throws InterruptedException {
Thread.sleep(1000);
return "id: "+id+" name: "+name;
}
结果
修改OpenFeign默认超时时间
注意应该在调用者的配置文件修改,填的是服务名id
feign.client.config.PRODUCT.connectTimeout=5000 #配置指定服务连接超时
feign.client.config.PRODUCT.readTimeout=5000 #配置指定服务等待超时
feign:
client:
config:
PRODUCT:
connectTimeout: 5000
readTimeout: 5000
修改当前服务调用所有服务时间
feign.client.config.default.connectTimeout=5000 #配置所有服务连接超时
feign.client.config.default.readTimeout=5000 #配置所有服务等待超时
OpenFeign调用详细日志展示
往往在服务调用时我们需要详细展示feign的日志,默认feign在调用是并不是最详细日志输出,因此在调试程序时应该开启feign的详细日志展示。feign对日志的处理非常灵活可为每个feign客户端指定日志记录策略,每个客户端都会创建一个logger默认情况下logger的名称是feign的全限定名需要注意的是,feign日志的打印只会DEBUG级别做出响应。
而日志的级别分为四种:
- NONE:不记录任何日志信息,这是默认值。
- BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
- HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
- FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。
基于配置文件修改feign的日志级别可以针对单个服务
server:
port: 7001
spring:
application:
name: category
cloud:
consul:
port: 8500
host: localhost
feign:
client:
config:
PRODUCT: # 针对某个微服务的配置
connectTimeout: 5000
readTimeout: 5000
loggerLevel: full # 日志级别
logging:
level:
root: info
com.blb.feignclient: debug
也可以针对所有服务
feign:
client:
config:
default: # 这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
loggerLevel: FULL # 日志级别
feign.client.config.PRODUCT.loggerLevel=full #开启指定服务日志展示
#feign.client.config.default.loggerLevel=full #全局开启服务日志展示
logging.level.com.baizhi.feignclients=debug #指定feign调用客户端对象所在包,必须是debug级别
Java代码方式修改日志
也可以基于Java代码来修改日志级别,先声明一个类,然后声明一个Logger.Level的对象
public class DefaultFeignConfiguration {
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.BASIC; // 日志级别为BASIC
}
}
如果要全局生效,将其放到启动类的@EnableFeignClients这个注解中:
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration .class)
如果是局部生效,则把它放到对应的@FeignClient这个注解中:
@FeignClient(value = "userservice", configuration = DefaultFeignConfiguration .class)
Feign使用优化
Feign底层发起http请求,依赖于其它的框架。其底层客户端实现包括:
•URLConnection:默认实现,不支持连接池
•Apache HttpClient :支持连接池
•OKHttp:支持连接池
因此提高Feign的性能主要手段就是使用连接池代替默认的URLConnection。
依赖
<!--httpClient的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
配置
feign:
client:
config:
default: # default全局的配置
loggerLevel: BASIC # 日志级别,BASIC就是基本的请求和响应信息
httpclient:
enabled: true # 开启feign对HttpClient的支持
max-connections: 200 # 最大的连接数
max-connections-per-route: 50 # 每个路径的最大连接数
总结,Feign的优化:
1.日志级别尽量用basic
2.使用HttpClient或OKHttp代替URLConnection
① 引入feign-httpClient依赖
② 配置文件开启httpClient功能,设置连接池参数
最佳实践
所谓最近实践,就是使用过程中总结的经验,最好的一种使用方式。
自习观察可以发现,Feign的客户端与服务提供者的controller代码非常相,甚至可以直接CV
继承方式
一样的代码可以通过继承来共享:
1)定义一个API接口,利用定义方法,并基于SpringMVC注解做声明。
2)Feign客户端和Controller都集成改接口
优点:
- 简单
- 实现了代码共享
缺点:
-
服务提供方、服务消费方紧耦合
-
参数列表中的注解映射并不会继承,因此Controller中必须再次声明方法、参数列表、注解
抽取方式
将Feign的Client抽取为独立模块,并且把接口有关的POJO、默认的Feign配置都放到这个模块中,提供给所有消费者使用。
例如,将UserClient、User、Feign的默认配置都抽取到一个feign-api包中,所有微服务引用该依赖包,即可直接使用。
但是有可能有包扫描的问题
方式一:
指定Feign应该扫描的包:
@EnableFeignClients(basePackages = "com.blb.feign.clients")
方式二:
指定需要加载的Client接口:
@EnableFeignClients(clients = {
UserClient.class})
Hystrix组件使用
通俗定义: Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统中,许多依赖不可避免的会调用失败,超时、异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障(服务雪崩现象),提高分布式系统的弹性。
作用
-
hystrix 用来保护微服务系统 实现 服务降级 服务熔断
-
服务雪崩
-
服务降级
-
服务熔断
服务雪崩
在微服务之间进行服务调用是由于某一个服务故障,导致级联服务故障的现象,称为雪崩效应。雪崩效应描述的是提供方不可用,导致消费方不可用并将不可用逐渐放大的过程
现象:在一个时刻微服务系统中所有微服务均不可用的这种现象,称之为服务雪崩现象
引发:在服务之间进行服务的调用是由于某一个服务故障,导致级联服务故障的现象,称为雪崩效应,雪崩效应的提供方不可用,导致消费方不可用,逐渐放大的过程
根本原因:调用链路中链路某一服务因为执行业务时间过程,或者是大规模出现异常导致自身服务不可用,并把这种不可用放大
如下调用链路
Service A的流量波动很大,流量经常会突然性增加!那么在这种情况下,就算Service A能扛得住请求,Service B和Service C未必能扛得住这突发的请求。此时,如果Service C因为抗不住请求,变得不可用。那么Service B的请求也会阻塞,慢慢耗尽Service B的线程资源,Service B就会变得不可用。紧接着,Service A也会不可用
服务熔断
,有点类似保险丝,熔断器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器(hystrix)的故障监控,某个异常条件被触发,直接熔断整个服务。向调用方法返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方法无法处理的异常,就保证了服务调用方的线程不会被长时间占用,避免故障在分布式系统中蔓延,乃至雪崩。如果目标服务情况好转则恢复调用。服务熔断是解决服务雪崩的重要手段
服务降级
服务降级: 关闭微服务系统中某些边缘服务 保证系统核心服务正常运行
服务压力剧增的时候根据当前的业务情况及流量对一些服务和页面有策略的降级,以此缓解服务器的压力,以保证核心任务的进行。同时保证部分甚至大部分任务客户能得到正确的响应。也就是当前的请求处理不了了或者出错了,给一个默认的返回
降级和熔断总结
共同点
- 目的很一致,都是从可用性可靠性着想,为防止系统的整体缓慢甚至崩溃,采用的技术手段;
- 最终表现类似,对于两者来说,最终让用户体验到的是某些功能暂时不可达或不可用;
- 粒度一般都是服务级别,当然,业界也有不少更细粒度的做法,比如做到数据持久层(允许查询,不允许增删改);
- 自治性要求很高,熔断模式一般都是服务基于策略的自动触发,降级虽说可人工干预,但在微服务架构下,完全靠人显然不可能,开关预置、配置中心都是必要手段;sentinel
异同点
- 触发原因不太一样,服务熔断一般是某个服务(下游服务)故障引起,而服务降级一般是从整体负荷考虑;
- 管理目标的层次不太一样,熔断其实是一个框架级的处理,每个微服务都需要(无层级之分),而降级一般需要对业务有层级之分(比如降级一般是从最外围服务边缘服务开始)
总结
- 熔断必会触发降级,所以熔断也是降级一种,区别在于熔断是对调用链路的保护,而降级是对系统过载的一种保护处理
服务熔断的实现
项目中引入hystrix依赖
<!--引入hystrix-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
开启断路器
@SpringBootApplication //代表springboot应用
@EnableDiscoveryClient //代表服务注册中心客户端 consul client
@EnableCircuitBreaker //开启hystrix服务熔断
public class HystrixApplication {
public static void main(String[] args) {
SpringApplication.run(HystrixApplication.class,args);
}
}
使用HystrixCommand注解实现断路
为每一个调用接口提供自定义备选处理
注意:fallbackMethod方法返回类型和参数列表必须要和熔断的方法一样
@HystrixCommand(fallbackMethod = "testfallback")//熔断之后处理 fallbackMethod 书写快速失败的方法名
@RestController
public class TestController {
@GetMapping("/test")
@HystrixCommand(fallbackMethod = "testfallback")//熔断之后处理 fallbackMethod 书写快速失败的方法名
public String test(Integer id){
if(id<=0){
throw new RuntimeException("id小于0");
}
return "test";
}
//返回类型和参数列表必须要和熔断的方法一样
public String testfallback(Integer id){
return "服务已经熔断";
}
}
当id传值-1并且多次刷新的时候
在传正确的值1
使用Hystrix提供默认备选处理
注意:默认的处理方法,返回类型必须是String类型,不能有参数列表
@RestController
public class TestController {
@GetMapping("/test")
@HystrixCommand(defaultFallback = "defaultfallback")//熔断之后处理
public String test(Integer id){
if(id<=0){
throw new RuntimeException("id小于0");
}
return "test";
}
//默认的处理方法,返回类型必须是String类型,不能有参数列表
public String defaultfallback(){
return "网络连接失败,请重试";
}
}
总结
如果触发一定条件断路器会自动打开,过了一点时间正常之后又会关闭
总结打开关闭的条件:
- 1、 当满足一定的阀值的时候(默认10秒内超过20个请求次数)
- 2、 当失败率达到一定的时候(默认10秒内超过50%的请求失败)
- 3、 到达以上阀值,断路器将会开启
- 4、 当开启的时候,所有请求都不会进行转发
- 5、 一段时间之后(默认是5秒),这个时候断路器是半开状态,会让其中一个请求进行转发。如果成功,断路器会关闭,若失败,继续开启。重复4和5
一旦断路器开启之后,所有到这个服务请求均不可用,只有在断路关闭之后才可用
真个流程:当hystrix监控到对该服务接口调用触发的两种阈值时,会在系统中自动触发熔断器,在熔断器打开期间内,任何到该接口请求均不可用,同时在断路器打开5秒后断路器会处于半开状态,此时断路器允许放行一个请求到该服务接口,如果该请求执行成功,断路器彻底关闭,如果该请求执行失败,断路器重新打开
服务降级的实现
服务降级: 站在系统整体负荷角度 实现: 关闭系统中某些边缘服务 保证系统核心服务运行
Emps 核心服务 Depts 边缘服务
客户端openfeign + hystrix实现服务降级实现
- 引入hystrix依赖
- 配置文件开启feign支持hystrix
- 在feign客户端调用加入fallback指定降级处理
- 开发降级处理方法
注意openfeign底层已经依赖了hystrix,所有引入了openfeign的依赖就不用在引入hystrix的依赖了
开启openfeign支持服务降级
feign.hystrix.enabled=true #开启openfeign支持降级
feign:
hystrix:
enabled: true
在openfeign客户端中加如Hystrix
@FeignClient(value = "HYSTRIX",fallback = HystrixClientFallBack.class) //fallback 这个属性用来指定当前服务不可用时,默认的备选处理,
public interface HystrixClient {
@GetMapping("/test")
public String test(Integer id);
}
开发fallback处理类
注意要注入容器中
@Configuration
//自定义 默认备选处理方案
public class HystrixClientFallBack implements HystrixClient{
@Override
public String test(Integer id) {
return "当前服务不可用";
}
}
Hystrix Dashboard(仪表盘)
Hystrix Dashboard的一个主要优点是它收集了关于每个HystrixCommand的一组度量。Hystrix仪表板以高效的方式显示每个断路器的运行状况。
项目中引入依赖
<!--引入hystrix dashboard 依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
入口类中开启hystrix dashboard
@SpringBootApplication
@EnableDiscoveryClient
@EnableHystrixDashboard //开启当前应用为仪表盘应用
public class HystrixDashBoardApplication {
public static void main(String[] args) {
SpringApplication.run(HystrixDashBoardApplication.class,args);
}
}
启动hystrix dashboard应用
http://localhost:7006(dashboard端口)/hystrix
端口号为你创建的springboot应用的端口号
监控的项目中入口类中加入监控路径配置[新版本坑],并启动监控项目
@Bean
public ServletRegistrationBean getServlet() {
HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
registrationBean.setLoadOnStartup(1);
registrationBean.addUrlMappings("/hystrix.stream");
registrationBean.setName("HystrixMetricsStreamServlet");
return registrationBean;
}
解决方案
- 新版本中springcloud将jquery版本升级为3.4.1,定位到monitor.ftlh文件中,js的写法如下:
$(window).load(function()
- jquery 3.4.1已经废弃上面写法
- 修改方案 修改monitor.ftlh为如下调用方式:
$(window).on("load",function()
- 编译jar源文件,重新打包引入后,界面正常响应。
在maven仓库中找到这个jar包的位置
D:\Maven\apache-maven-3.8.1\maven-repo\org\springframework\cloud\spring-cloud-netflix-hystrix-dashboard\2.2.3.RELEASE
可能你的就是maven仓库的位置前面不一样,按照自己的路径一直往下找
在templates目录下的monitor.ftlh
修改111行和149行的代码
注意前面那段配置应该放在要监视的微服务项目里面,可以创建一个配置文件,我这里就放在启动类里了
这次就没有报错
Gateway组件使用
网关统一服务入口,可方便实现对平台众多服务接口进行管控,对访问服务的身份认证、防报文重放与防数据篡改、功能调用的业务鉴权、响应数据的脱敏、流量与并发控制,甚至基于API调用的计量或者计费等等。
- 网关 = 路由转发 + 过滤器
路由转发:接收一切外界请求,转发到后端的微服务上去;
在服务网关中可以完成一系列的横切功能,例如权限校验、限流以及监控等,这些都可以通过过滤器完成 - 1.网关可以实现服务的统一管理
- 2.网关可以解决微服务中通用代码的冗余问题(如权限控制,流量监控,限流等)
zuul
zul是从设备和网站到Netflix流媒体应用程序后端的所有请求的前门。作为一个边缘服务应用程序,zul被构建为支持动态路由、监视、弹性和安全性
目前zuul组件已经从1.0更新到2.0,但是作为springcloud官方不再推荐使用zuul2.0,但是依然支持zuul2
gateway
这个项目提供了一个在springmvc之上构建API网关的库。springcloudgateway旨在提供一种简单而有效的方法来路由到api,并为api提供横切关注点,比如:安全性、监控/度量和弹性
- 基于springboot2.x 和 spring webFlux 和 Reactor 构建 响应式异步非阻塞IO模型
权限控制:网关作为微服务入口,需要校验用户是是否有请求资格,如果没有则进行拦截。
路由和负载均衡:一切请求都必须先经过gateway,但网关不处理业务,而是根据某种规则,把请求转发到某个微服务,这个过程叫做路由。当然路由的目标服务有多个时,还需要做负载均衡。
限流:当请求流量过高时,在网关中按照下流的微服务能够接受的速度来放行请求,避免服务压力过大
Zuul是基于Servlet的实现,属于阻塞式编程。而SpringCloudGateway则是基于Spring5中提供的WebFlux,属于响应式编程的实现,具备更好的性能
gateway核⼼概念
Route(路由)
路由是构建⽹关的基本模块,它由ID,⽬标URI,⼀系列的断⾔和过滤器组成,如果断⾔为true则匹配该路由
Predicate(断⾔、谓词)
开发⼈员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断⾔相匹配则进⾏路由
Filter(过滤)
指的是Spring框架中GatewayFilter的实例,使⽤过滤器,可以在请求被路由前或者之后对请求进⾏修改
依赖
<!--引入gateway网关依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
配置
server:
port: 8888
spring:
application:
name: GATEWAY
cloud:
consul:
host: localhost
port: 8500
gateway:
routes:
- id: categody_router #路由对象唯一标识,路由id,自定义,只要唯一即可
uri: http://localhost:7001/ #用来配置服务地址 http就是固定地址
predicates: #断言 用来配置路径规则,也就是判断请求是否符合路由规则的条件
- Path=/category/** # 这个是按照路径匹配,只要以/category/开头就符合要求
- id: product_router #路由对象唯一标识
uri: http://localhost:7000/ #用来配置服务地址
predicates: #断言 用来配置路径规则
- Path=/product/**
启动gateway网关项目
注意:不要依赖spring-boot-starter-web
- 直接启动报错:
- 在启动日志中发现,gateway为了效率使用webflux进行异步非阻塞模型的实现,因此和原来的web包冲突,去掉原来的web即可
java方式配置路由
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("categody_router", r -> r.path("/category/**")
.uri("http://localhost:7001"))
.route("product_router", r -> r.path("/product/**")
.uri("http://localhost:7000"))
.build();
}
}
查看网关路由规则列表
- gateway提供路由访问规则列表的web界面,但是默认是关闭的,如果想要查看服务路由规则可以在配置文件中开启
management:
endpoints:
web:
exposure:
include: "*" #开启所有web端点暴露
访问
http://localhost:8888/actuator/gateway/routes
配置路由服务负载均衡
现有路由配置方式,都是基于服务地址写死的路由转发,根据服务名称进行路由转发同时实现负载均衡
server:
port: 8888
spring:
application:
name: GATEWAY
cloud:
consul:
host: localhost
port: 8500
gateway:
routes:
- id: categody_router #路由对象唯一标识
uri: lb://category # lb代表转发后台服务使用负载均衡,category代表服务注册中心上的服务名
predicates: #断言 用来配置路径规则
- Path=/category/**
- id: product_router #路由对象唯一标识
uri: lb://PRODUCT # lb(loadbalance)代表负载均衡转发路由
predicates: #断言 用来配置路径规则
- Path=/product/**
discovery:
locator:
enabled: true #开启根据服务名动态获取路由
lb是负载均衡,根据服务名拉取服务列表,实现负载均衡
路由配置包括:
-
路由id:路由的唯一标示
-
路由目标(uri):路由的目标地址,http代表固定地址,lb代表根据服务名负载均衡
-
路由断言(predicates):判断路由的规则,
-
路由过滤器(filters):对请求或响应做处理
常用路由predicate(断言,验证)
名称 | 说明 | 示例 |
---|---|---|
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 | 权重处理 |
server:
port: 8888
spring:
application:
name: GATEWAY
cloud:
consul:
host: localhost
port: 8500
gateway:
routes:
- id: categody_router #路由对象唯一标识
uri: lb://category # lb代表转发后台服务使用负载均衡,category代表服务注册中心上的服务名
predicates: #断言 用来配置路径规则
- Path=/category/**
- After=2021-08-18T13:30:33.993+08:00[Asia/Shanghai] #指定日期之后的请求进行路由
- id: product_router #路由对象唯一标识
uri: lb://PRODUCT # lb(loadbalance)代表负载均衡转发路由
predicates: #断言 用来配置路径规则
- Path=/product/**
- After=2021-08-18T13:30:33.993+08:00[Asia/Shanghai] #指定日期之后的请求进行路由
- Cookie=username,[A-Za-z0-9]+
- Header=X-Request-Id, \d+
discovery:
locator:
enabled: true
management:
endpoints:
web:
exposure:
include: "*" #开启所有web端点暴露
指定日期之后的请求进行路由
After=2020-07-21T11:33:33.993+08:00[Asia/Shanghai]
指定日期之前的请求进行路由,过了时间失效
Before=2020-07-21T11:33:33.993+08:00[Asia/Shanghai]
指定时间段内有效
Between=2020-07-21T11:33:33.993+08:00[Asia/Shanghai],2021-07-21T11:33:33.993+08:00[Asia/Shanghai]
指定cookie的请求进行路由
curl http://localhost:8888/product --cookie "name=dyk"
- Cookie=name,[A-Za-z0-9]+
[A-Za-z0-9]+ 这是正则表达式
基于请求头中的指定属性的正则匹配路由(这里全是整数)
- Header=X-Request-Id, \d+
基于指定的请求方式请求进行路由
- Method=GET,POST
常用的Filter以及自定义filter
GatewayFilter是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理
名称 | 说明 |
---|---|
AddRequestHeader | 给当前请求添加一个请求头 |
RemoveRequestHeader | 移除请求中的一个请求头 |
AddResponseHeader | 给响应结果中添加一个响应头 |
RemoveResponseHeader | 从响应结果中移除有一个响应头 |
RequestRateLimiter | 限制请求的流量 |
用来给路由对象所有转发的请求加入指定请求头信息
- AddRequestHeader=X-Request-red, blue
用来给路由对象所有转发的请求加入指定请求参数
- AddRequestParameter=red, blue
用来给路由对象所有转发的请求加入指定响应头信息
- AddResponseHeader=X-Response-Red, AAA
-
用来给路由对象所有转发的请求加入指定请求的url加上指定前缀的信息
- PrefixPath=/mypath
如浏览器访问网关地址 /list 前缀地址/mypath 转发到服务器地址为: uri+前缀地址+地址栏路径 /mypath/list
用来给对象路由的转发请求的url去掉指定n个前缀
- StripPrefix=2
如浏览器访问网关地址 /product/list StripPrefix=1 ===>/list
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://userservice
predicates:
- Path=/user/**
filters: # 过滤器
- AddRequestHeader=name,dyk # 添加请求头
默认过滤器
如果要对所有的路由都生效,则可以将过滤器工厂写到default下。格式如下
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://userservice
predicates:
- Path=/user/**
default-filters: # 默认过滤项
- AddRequestHeader=name,dyk
自定义全局网关
//自定义网关全局filter
@Configuration
public class CustomGlobalFilter implements GlobalFilter, Ordered {
private static final Logger log = LoggerFactory.getLogger(CustomGlobalFilter.class);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("进入自定义的filter");
if(exchange.getRequest().getQueryParams().get("username")!=null){
log.info("用户身份信息合法,放行请求继续执行!!!");
return chain.filter(exchange);
}
log.info("非法用户,拒绝访问!!!");
return exchange.getResponse().setComplete();
}
@Override
// 用来指定filter执行顺序 默认顺序按照自然排序进行 -1在所有filter执行之前执行
public int getOrder() {
return 0;
}
}
需求:定义全局过滤器,拦截请求,判断请求的参数是否满足下面条件:
-
参数中是否有authorization,
-
authorization参数值是否为admin
如果同时满足则放行,否则拦截
package cn.itcast.gateway.filters;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Order(-1)
@Component
public class AuthorizeFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1.获取请求参数
MultiValueMap<String, String> params = exchange.getRequest().getQueryParams();
// 2.获取authorization参数
String auth = params.getFirst("authorization");
// 3.校验
if ("admin".equals(auth)) {
// 放行
return chain.filter(exchange);
}
// 4.拦截
// 4.1.禁止访问,设置状态码
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
// 4.2.结束处理
return exchange.getResponse().setComplete();
}
}
过滤器执行顺序
请求进入网关会碰到三类过滤器:当前路由的过滤器、DefaultFilter、GlobalFilter
请求路由后,会将当前路由过滤器和DefaultFilter、GlobalFilter,合并到一个过滤器链(集合)中,排序后依次执行每个过滤器:
排序的规则是什么呢?
- 每一个过滤器都必须指定一个int类型的order值,order值越小,优先级越高,执行顺序越靠前。
- GlobalFilter通过实现Ordered接口,或者添加@Order注解来指定order值,由我们自己指定
- 路由过滤器和defaultFilter的order由Spring指定,默认是按照声明顺序从1递增。
- 当过滤器的order值一样时,会按照 defaultFilter > 路由过滤器 > GlobalFilter的顺序执行。
网关跨域问题
spring:
cloud:
gateway:
# 。。。
globalcors: # 全局的跨域处理
add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
corsConfigurations:
'[/**]':
allowedOrigins: # 允许哪些网站的跨域请求
- "http://localhost:8090"
allowedMethods: # 允许的跨域ajax的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许在请求中携带的头信息
allowCredentials: true # 是否允许携带cookie
maxAge: 360000 # 这次跨域检测的有效期