# springcloud-demo **Repository Path**: zrclass/springcloud-demo ## Basic Information - **Project Name**: springcloud-demo - **Description**: SpringCloud 学习代码及文档总结,涵盖eureka,openfeign,ribbon,hystrix,gateway等组件的学习和用法及原理的总结 - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 1 - **Created**: 2023-07-19 - **Last Updated**: 2023-07-25 ## Categories & Tags **Categories**: Uncategorized **Tags**: Eureka, openfeign, Hystrix, Ribbon, Gateway ## README ## SpringCloud与SpringBoot版本关系 **大版本号对应关系:** | Spring Boot | Spring Cloud | 关系 | | ----------- | ----------------------- | ---------------------------------------------- | | 1.2.x | Angel版本(天使) | 兼容Spring Boot 1.2.x | | 1.3.x | Brixton版本(布里克斯顿) | 兼容Spring Boot 1.3.x,也兼容Spring Boot 1.4.x | | 1.4.x | Camden版本(卡姆登) | 兼容Spring Boot 1.4.x,也兼容Spring Boot 1.5.x | | 1.5.x | Dalston版本(多尔斯顿) | 兼容Spring Boot 1.5.x,不兼容Spring Boot 2.0.x | | 1.5.x | Edgware版本(埃奇韦尔) | 兼容Spring Boot 1.5.x,不兼容Spring Boot 2.0.x | | 2.0.x | Finchley版本(芬奇利) | 兼容Spring Boot 2.0.x,不兼容Spring Boot 1.5.x | | 2.1.x | Greenwich版本(格林威治) | 兼容Spring Boot 2.1.x | | 2.2.x,2.3.x | Hoxton版本 | 兼容SpringBoot2.2.x,2.3.x | | 2.4.x,2.5.x | 2020.x | 兼容SpringBoot2.4.x,2.5.x | | 2.5.x,2.6.x | 2021.x | 兼容SpringBoot2.5.x,2.6.x | | 3.0.x | 2022.x | 兼容SpringBoot3.0.x | ## Eureka ### **一.Eureka概述** Eureka是一个基于REST的服务,主要用于AWS(Amazon Web Services 亚马逊云计算服务)云中的定位服务,以实现中间层服务器的负载平衡和故障转移在 Spring Cloud **微服务**架构中通常用作**注册中心**, 我们称这个服务为 Eureka Server,还有一个与之交互的客户端称之为 Eureka Client . ### **二.Eureka架构图** ![img](https://gitee.com/zrclass/springcloud-demo/raw/master/doc/studydoc/Eureka.assets/20191020122251386.png) 如上图所示,其中 Eureka Server 表示服务注册中心 (Eureka服务端) Application Service表示服务提供方 (Eureka客户端,需要在Eureka服务端注册) Application Client 表示服务消费方 (Eureka客户端,需要在Eureka服务端注册) Make Remote Call 表示远程调用 服务在Eureka上注册,然后每隔30秒发送心跳来更新它们的租约。如果客户端不能多次续订租约,那么它将在大约90秒内从服务器注册表中剔除。注册信息和更新被复制到集群中的所有eureka节点。来自任何区域的客户端都可以查找注册表信息(每30秒发生一次)来定位它们的服务(可能在任何区域)并进行远程调用。**Eureka Client需要每30秒给Eureka Server发一次心跳,同时更新Server上最新的注册信息到本地,如果Server多次没有收到来自客户端的心跳,那么在90秒内会被Server上剔除.** ![img](https://gitee.com/zrclass/springcloud-demo/raw/master/doc/studydoc/Eureka.assets/20210207202315380.png) ### **三.Eureka客户端与服务端(注册中心)之间的通信** 1.**register(注册)**: Eureka客户端将关于运行实例的信息注册到Eureka服务器。注册发生在第一次心跳。 2.**renew(更新 / 续借)**:Eureka客户端需要更新最新注册信息(续借),通过每30秒发送一次心跳。更新通知是为了告诉Eureka服务器实例仍然存活。如果服务器在90秒内没有看到更新,它会将实例从注册表中删除。建议不要更改更新间隔,因为服务器使用该信息来确定客户机与服务器之间的通信是否存在广泛传播的问题。 3.**Fetch Registry(抓取注册信息)**:Eureka客户端从服务器(注册中心)获取注册表信息并在本地缓存。之后,客户端使用这些信息来查找其他服务。通过在上一个获取周期和当前获取周期之间获取增量更新,这些信息会定期更新(每30秒更新一次)。获取的时候可能返回相同的实例。Eureka客户端自动处理重复信息。 4.**Cancel(取消)**:Eureka客户端在关机时向Eureka注册中心发送一个取消请求。这将从服务器的实例注册表中删除实例,从而有效地将实例从流量中取出. 5.**Eviction (服务剔除)**:当 Eureka Client 和 Eureka Server 不再有心跳时,Eureka Server 会将该服务实例从服务注册列表中删除,即服务剔除。 ### **四.Eureka自我保护机制** 如果 Eureka 服务器检测到超过预期数量的注册客户端终止了连接,并且同时正在等待被驱逐,那么它们将进入自我保护模式。这样做是为了确保灾难性网络事件不会擦除eureka注册表数据,并将其向下传播到所有客户端。 任何客户端,如果连续3次心跳更新失败,那么它将被视为非正常终止,病句将被剔除。当超过当前注册实例15%的客户端都处于这种状态,那么自我保护将被开启。 举例:比如你有10个user-service节点注册到eureka-server中,这个时候挂了两台。那么正常比例就是:(10-2)/10 = 80% 。只有80%的节点正常,20%的节点不正常(超过15%的异常率)。 这个时候就会引发自我保护机制。 当自我保护开启以后,eureka服务器将停止剔除所有实例,直到: 1. **它看到的心跳续借的数量回到了预期的阈值之上,或者** 2. **自我保护被禁用** 默认情况下,自我保护是启用的,并且,默认的阈值是要大于当前注册数量的15% ### 五.Eureka集群原理 ![img](https://gitee.com/zrclass/springcloud-demo/raw/master/doc/studydoc/Eureka.assets/20191020122251386.png) 从图中可以看出 Eureka Server 集群相互之间通过 Replicate 来同步数据,相互之间不区分主节点和从节点,所有的节点都是平等的。在这种架构中,节点通过彼此互相注册来提高可用性,每个节点需要添加一个或多个有效的 serviceUrl 指向其他节点。 如果某台 Eureka Server 宕机,Eureka Client 的请求会自动切换到新的 Eureka Server 节点。当宕机的服务器重新恢复后,Eureka 会再次将其纳入到服务器集群管理之中。当节点开始接受客户端请求时,所有的操作都会进行节点间复制,将请求复制到其它 Eureka Server 当前所知的所有节点中。 另外 Eureka Server 的同步遵循着一个非常简单的原则:只要有一条边将节点连接,就可以进行信息传播与同步。所以,如果存在多个节点,只需要将节点之间两两连接起来形成通路,那么其它注册中心都可以共享信息。每个 Eureka Server 同时也是 Eureka Client,多个 Eureka Server 之间通过 P2P 的方式完成服务注册表的同步。 Eureka Server 集群之间的状态是采用异步方式同步的,所以不保证节点间的状态一定是一致的,不过基本能保证最终状态是一致的。 **1.Eureka 分区** Eureka 提供了 Region 和 Zone 两个概念来进行分区,这两个概念均来自于亚马逊的 AWS: - region:可以理解为地理上的不同区域,比如亚洲地区,中国区或者深圳等等。没有具体大小的限制。根据项目具体的情况,可以自行合理划分 region。 - zone:可以简单理解为 region 内的具体机房,比如说 region 划分为深圳,然后深圳有两个机房,就可以在此 region 之下划分出 zone1、zone2 两个 zone。 上图中的 us-east-1c、us-east-1d、us-east-1e 就代表了不同的 Zone。Zone 内的 Eureka Client 优先和 Zone 内的 Eureka Server 进行心跳同步,同样调用端优先在 Zone 内的 Eureka Server 获取服务列表,当 Zone 内的 Eureka Server 挂掉之后,才会从别的 Zone 中获取信息。 **2.Eurka 保证 AP** Eureka Server 各个节点都是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。而 Eureka Client 在向某个 Eureka 注册时,如果发现连接失败,则会自动切换至其它节点。只要有一台 Eureka Server 还在,就能保证注册服务可用(保证可用性),只不过查到的信息可能不是最新的(不保证强一致性)。 ### 六.Eureka配置解析 1.EurekaServer配置解析 ```yml server: port: 7001 spring: application: name: service-eureka # 引入spring security,配置权限认证的用户名和密码 security: user: #登录注册中心的用户名和密码 name: admin password: admin eureka: instance: # eureka服务端的实例名称 # 单机 hostname: localhost hostname: 127.0.0.1 instance-id: ${eureka.instance.hostname}:${server.port} prefer-ip-address: true # Eureka客户端向服务端发送心跳的时间间隔,单位为秒(默认是30秒) lease-renewal-interval-in-seconds: 30 # Eureka服务端在收到最后一次心跳后等待时间上限 ,单位为秒(默认是90秒),超时剔除服务 lease-expiration-duration-in-seconds: 90 server: # 禁用自我保护,保证不可用服务被及时删除,默认启用为true enable-self-preservation: false # Server 清理无效节点的时间间隔,默认60000毫秒,即60秒。 eviction-interval-timer-in-ms: 60000 # 自我保护续约百分比,默认是0.85 renewal-percent-threshold: 0.85 client: # false表示不向注册中心注册自己 register-with-eureka: false # false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要检索服务 fetch-registry: false service-url: # 设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址 # 单机 defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ defaultZone: http://${spring.security.user.name}:${spring.security.user.password}@${eureka.instance.hostname}:${server.port}/eureka/ # 相互注册 # defaultZone: http://eureka7002.com:7002/eureka/ ``` 2.EurekaClient配置解析 ```yml server: port: 8001 spring: application: name: service-provider eureka: client: register-with-eureka: true fetch-registry: true service-url: defaultZone: http://localhost:7001/eureka # 集群版 #defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka # 是否注册到注册中心 enabled: true instance: hostname: 127.0.0.1 port: 7001 user: admin password: admin instance-id: ${eureka.instance.hostname}:${server.port} # 访问路径可以显示ip地址 prefer-ip-address: true ``` ### 七.Eureka的工作流程 1、Eureka Server 启动成功,等待服务端注册。在启动过程中如果配置了集群,集群之间定时通过 Replicate 同步注册表,每个 Eureka Server 都存在独立完整的服务注册表信息 2、Eureka Client 启动时根据配置的 Eureka Server 地址去注册中心注册服务 3、Eureka Client 会每 30s 向 Eureka Server 发送一次心跳请求,证明客户端服务正常 4、当 Eureka Server 90s 内没有收到 Eureka Client 的心跳,注册中心则认为该节点失效,会注销该实例 5、单位时间内 Eureka Server 统计到有大量的 Eureka Client 没有上送心跳,则认为可能为网络异常,进入自我保护机制,不再剔除没有上送心跳的客户端 6、当 Eureka Client 心跳请求恢复正常之后,Eureka Server 自动退出自我保护模式 7、Eureka Client 定时全量或者增量从注册中心获取服务注册表,并且将获取到的信息缓存到本地 8、服务调用时,Eureka Client 会先从本地缓存找寻调取的服务。如果获取不到,先从注册中心刷新注册表,再同步到本地缓存 9、Eureka Client 获取到目标服务器信息,发起服务调用 10、Eureka Client 程序关闭时向 Eureka Server 发送取消请求,Eureka Server 将实例从注册表中删除 这就是Eurka基本工作流程 ### **八.同为注册中心Eureka与zookeeper比较** CAP理论指出,一个分布式系统不可能同时满足C(一致性)、A(可用性)和P(分区容错性)。由于分区容错性在是分布式系统中必须要保证的,因此我们只能在A和C之间进行权衡。在此Zookeeper保证的是CP, 而Eureka则是AP。 **Zookeeper保证CP**: 当向注册中心查询服务列表时,我们可以容忍注册中心返回的是几分钟以前的注册信息,但不能接受服务直接down掉不可用。也就是说,服务注册功能对可用性的要求要高于一致性。但是zk会出现这样一种情况,当master节点因为网络故障与其他节点失去联系时,剩余节点会重新进行leader选举。问题在于,选举leader的时间太长,30 ~ 120s, 且选举期间整个zk集群都是不可用的,这就导致在选举期间注册服务瘫痪。在云部署的环境下,因网络问题使得zk集群失去master节点是较大概率会发生的事,虽然服务能够最终恢复,但是漫长的选举时间导致的注册长期不可用是不能容忍的。 **Eureka保证AP**:Eureka看明白了这一点,因此在设计时就优先保证可用性。Eureka各个节点都是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。而Eureka的客户端在向某个Eureka注册或时如果发现连接失败,则会自动切换至其它节点,只要有一台Eureka还在,就能保证注册服务可用(保证可用性),只不过查到的信息可能不是最新的(不保证强一致性)。除此之外,Eureka还有一种自我保护机制,如果在15分钟内超过15%的节点都没有正常的心跳,那么Eureka就认为客户端与注册中心出现了网络故障,此时会出现以下几种情况: ​ 1.Eureka不再从注册列表中移除因为长时间没收到心跳而应该过期的服务 ​ 2.Eureka仍然能够接受新服务的注册和查询请求,但是不会被同步到其它节点上(即保证当前节点依然可用) ​ 3.当网络稳定时,当前实例新的注册信息会被同步到其它节点中 因此, **Eureka可以很好的应对因网络故障导致部分节点失去联系的情况,而不会像zookeeper那样使整个注册服务瘫痪**。 引用博客地址 :https://blog.csdn.net/weixin_42232931/article/details/102647679?spm=1001.2014.3001.5502 ## OpenFeign ### 前言-微服务架构设计风格 微服务设于基于RESTful架构,使用RESTful可以将愈发复杂单体应用通过HTTP请求、JSON传输数据拆分为不同的业务模块,达到服务独立部署、快速启动、模块协同开发、低耦合、代码复用、职责单一的目的,使团队间相对隔离的敏捷式开发。微服务的盛行首要解决的便是不同服务间调用的问题。 **现有解决方案** - **Ribbon+RestTemplate** - **Feign** - **OpenFeign** 1.**Ribbon+RestTemplate**是基于**HTTP**+**TCP**实现服务间通信,其原理是通过RestTemplate构造HTTP请求来完成,相当于单体应用通过HTTP调用第三方接口,是一种远程调用的实现方式。 2.**Feign**基于Ribbon+RestTemplate,其内部是通过JDK动态代理的方式,将对外调用的接口抽象成接口,使用远程API的Method方法实例,进行MethodHandler方法处理器分发,根据参数构造Request实例,一般是Controller实现自定义的接口,供外部调用。Feign内部实现了负载均衡,根据负载量、请求量分发至不同的服务,但目前Feign已不再维护。 3.**OpenFeign**是springcloud在Feign的基础上支持了SpringMVC的注解,如@RequestMapping等等。OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。 ### **一.OpenFeign与Feign的关系** `feign`不是一个中间件,`feign`是`spring cloud`组件中的一个轻量级`restful`的`http`服务客户端。其作用是简化接口的调用,将`http`调用转为`rpc`调用,让调用远程接口像同进程应用内的接口调用一样简单。 与`dubbo`的`rpc`远程调用一样,通过动态代理实现接口的调用。`feign`通过封装包装请求体、发送`http`请求、获取接口响应结果、序列化响应结果等接口调用动作来简化接口的调用。 `openfeign`是`spring cloud`在`feign`的基础上支持了`spring mvc`的注解,如`@RequesMapping`、`@GetMapping`、`@PostMapping`等。`openfeign`还实现与`Ribbon`的整合。 服务提供者只需要提供`API`接口,而不需要像`dubbo`那样需要强制使用`implements`实现接口,即使用`fegin`不要求服务提供者在`Controller`使用`implements`关键字实现接口。 ### 二.Feign底层实现原理 ​ `openfeign`通过包扫描将所有被`@FeignClient`注解注释的接口扫描出来,并为每个接口注册一个`FeignClientFactoryBean`实例。`FeignClientFactoryBean`是一个`FactoryBean`,当`Spring`调用`FeignClientFactoryBean`的`getObject`方法时,`openfeign`返回一个`Feign`生成的动态代理对象,拦截接口的方法执行。 ​ `feign`会为代理的接口的每个方法`Method`都生成一个`MethodHandler`。 ​ 当为接口上的`@FeignClient`注解的`url`属性配置服务提供者的`url`时,其实就是不与`Ribbon`整合,此时由`SynchronousMethodHandler`实现接口方法远程同步调用,使用默认的`Client`实现类`Default`实例发起`http`请求。 ​ 当接口上的`@FeignClient`注解的`url`属性不配置时,且会走负载均衡逻辑,也就是需要与`Ribbon`整合使用。这时候不再是使用默认的`Client`(`Default`)调用接口,而是使用`LoadBalancerFeignClient`调用接口,由`LoadBalancerFeignClient`实现与`Ribbon`的整合。 ### 三.Ribbon介绍及其与Feign的关系 `Ribbon`是`Netflix`发布的开源项目,提供在服务消费端实现负载均衡调用服务提供者,从注册中心读取所有可用的服务提供者,在客户端每次调用接口时采用如轮询负载均衡算法选出一个服务提供者调用,因此,`Ribbon`是一个客户端负载均衡器。 `Ribbon`提供多种负载均衡算法的实现、提供重试支持。`Feign`也提供重试支持,在`SynchronousMethodHandler`的`invoke`方法中实现,但`Feign`的重试比较简单,只是向同一个服务节点发送请求,而`Ribbon`的失败重试是支持重新选择一个服务节点调用的,在服务提供者部署多个节点的情况下,显然`Feign`的重试机制意义不大。 ### 四.Riboon原理及其与EurekaClient的关系 **Ribbon远程调用的底层原理**: `Ribbon`与`Fegin`整合的桥梁是`FeignLoadBalancer`。 - 1、`Ribbon`会注册一个`ILoadBalancer`(默认使用实现类`ZoneAwareLoadBalancer`)负载均衡器,`Feign`通过`LoadBalancerFeignClient`调用`FeignLoadBalancer`的`executeWithLoadBalancer`方法来使用`Ribbon`的`ILoadBalancer`负载均衡器选择一个提供者节点发送`http`请求,实际发送请求还是`OpenFeign`的`FeignLoadBalancer`发起的,`Ribbon`从始至终都只负责负载均衡选出一个服务节点。 - 2、`spring-cloud-netflix-ribbon`的自动配置类会注册一个`RibbonLoadBalancerClient`,此`RibbonLoadBalancerClient`正是`Ribbon`为`spring cloud`的负载均衡接口提供的实现类,用于实现`@LoadBalancer`注解语意。 `Ribbon`并非直接通过`DiscoveryClient`从注册中心获取服务的可用提供者,而是通过`ServerList`从注册中心获取服务提供者,`ServerList`与`DiscoveryClient`不一样,`ServerList`不是`Spring Cloud`定义的接口,而是`Ribbon`定义的接口。以`spring-cloud-kubernetes-ribbon`为例,`spring-cloud-kubernetes-ribbon`为`Ribbon`提供`ServerList`的实现`KubernetesServerList`。`Ribbon`负责定时调用`ServerList`的`getUpdatedListOfServers`方法更新可用服务提供者。 怎么去获取可用的服务提供者节点由你自己去实现`ServerList`接口,并将实现的`ServerList`注册到`Spring`容器。如果不提供`ServerList`,那么使用的将是`Ribbon`提供的默认实现类`ConfigurationBasedServerList`,`ConfigurationBasedServerList`并不会从注册中心读取获取服务节点,而是从配置文件中读取。 如果我们使用的注册中心是`Eureka`,当我们在项目中添加`spring-cloud-starter-netflix-eureka-client`时,其实就已经往项目中导入了一个`ribbon-eureka`的`jar`,由该`jar`包提供`Ribbon`与`Eureka`整合所需的`ServerList`:`DiscoveryEnabledNIWSServerList`。 ![img](https://gitee.com/zrclass/springcloud-demo/raw/master/doc/studydoc/OpenFeign.assets/1mtjduu3cx.png) **`Ribbon`默认提供了`ConfigurationBasedServerList`实现是`ServerList`,默认从配置文件中获取服务节点。 但是eureka中使用DiscoveryEnabledNIWSServerList实现了`ServerList`接口,通过EurekaClient来拉取目标服务信息。** ```java @Override public List getUpdatedListOfServers(){ return obtainServersViaDiscovery(); } private List obtainServersViaDiscovery() { List serverList = new ArrayList(); if (eurekaClientProvider == null || eurekaClientProvider.get() == null) { logger.warn("EurekaClient has not been initialized yet, returning an empty list"); return new ArrayList(); } EurekaClient eurekaClient = eurekaClientProvider.get(); if (vipAddresses!=null){ for (String vipAddress : vipAddresses.split(",")) { // if targetRegion is null, it will be interpreted as the same region of client List listOfInstanceInfo = eurekaClient.getInstancesByVipAddress(vipAddress, isSecure, targetRegion); // 拉取 ... } return serverList; } ``` ### 五.Ribbon的失败重试机制 `Ribbon`提供`RetryHandler`接口,并且默认使用`DefaultLoadBalancerRetryHandler`。`LoadBalancerCommand`的`submit`方法中(在`FeignLoadBalancer`的`executeWithLoadBalancer`方法中调用),如果配置重试次数大于`0`,则会调用`RxJava`的`API`支持重试。 ```java public Observable submit(final ServerOperation operation) { // ....... final int maxRetrysSame = retryHandler.getMaxRetriesOnSameServer(); final int maxRetrysNext = retryHandler.getMaxRetriesOnNextServer(); // Use the load balancer Observable o = (server == null ? selectServer() : Observable.just(server)) .concatMap(new Func1>() { @Override public Observable call(Server server) { //....... // 调用相同节点的重试次数 if (maxRetrysSame > 0) o = o.retry(retryPolicy(maxRetrysSame, true)); return o; } }); // 调用不同节点的重试次数 if (maxRetrysNext > 0 && server == null) o = o.retry(retryPolicy(maxRetrysNext, false)); return o.onErrorResumeNext(...); } ``` 默认`maxRetrysSame`(调用相同节点的重试次数)为`0`,默认`maxRetrysNext`(调用不同节点的重试次数)为`1`。`retryPolicy`方法返回的是一个判断是否重试的决策者,由该决策者决定是否需要重试(抛出的异常是否允许重试,是否达到最大重试次数)。 ```java private Func2 retryPolicy(final int maxRetrys, final boolean same) { return new Func2() { @Override public Boolean call(Integer tryCount, Throwable e) { if (e instanceof AbortExecutionException) { return false; } // 大于最大重试次数 if (tryCount > maxRetrys) { return false; } if (e.getCause() != null && e instanceof RuntimeException) { e = e.getCause(); } // 调用RetryHandler判断是否重试 return retryHandler.isRetriableException(e, same); } }; } ``` ### 六.OpenFeign架构及配置详解 OpenFeign的架构图 ![在这里插入图片描述](README.assets/87daa68013492a50391225994c621cb0.png) Feign本身提供了很多扩展点,例如: - 日志级别**logLevel** - 契约**contract** - 客户端**client** - 超时设置**options** - 编码器**encoder** - 解码器**decoder** - 拦截器**requestInterceptor** 这些扩展点,我们在使用原生Feign时,可以通过`Feign.Builder`指定,最后再通过`target`生成动态代理类,完成Bean注册 ![Feign.Builder](README.assets/5f876016dc66e1ae40b0bc474be3dda5.png) #### 1. 通过配置文件配置 **application.properties**,**格式:** ```javascript feign.client.config.{服务名}.{配置名} = {配置值} ``` 我们配置一些你可能用的上的扩展项,比如:`日志级别配置`、`契约配置`、`超时配置`、`编解码配置`、`拦截器配置`,如下: ```javascript # 日志级别配置 feign.client.config.default.loggerLevel = BASIC # 契约配置 feign.client.config.default.contract = feign.Contract.Default # 连接超时配置 feign.client.config.default.connectTimeout = 5000 # 读取超时配置 feign.client.config.default.readTimeout = 30000 # 编码器配置 feign.client.config.default.encoder = feign.jackson.JacksonEncoder # 解码器配置 feign.client.config.default.decoder = feign.jackson.JacksonDecoder # 拦截器配置, 是数组, 需要自定义RequestInterceptor feign.client.config.default.requestInterceptors[0]=com.tiangang.demo.c.interceptor.MyFeignRequestInterceptor ``` **有效范围说明** - `全局生效`:配置 **{服务名}** 为 **default** ,如上面例子中所示 - `局部生效`:配置 **{服务名}** 为 **具体服务名** 例如,下面的配置仅对调用`service-provider服务`有效。 ```javascript feign.client.config.service-provider.loggerLevel = BASIC ``` **验证是否生效** 我使用**service-consumer**发起调用,可以在启动service-consumer **启动服务** 时,构建 **动态代理前** 打断点查看**Feign.Builder**。 即在`FeignClientFactoryBean.loadBalance`方法的调`target`之前打断点: ![FeignClientFactoryBean.loadBalance打断点查看Feign.Builder](README.assets/639ffb4a118d66e7e7491bf78c95efa8.png) 配置后的`Feign.Builder`,确认已经按**application.properties**配置: ![Feign.Builder查看调试属性-已配置](README.assets/39fad49436d4b63f18b882db45a292ca.png) #### 2. 通过Java Bean配置 通过Java代码配置的话需要定义一个配置类,例如我命名为:`FeignConfig`,里面定义需要配置的`@Bean`,与上面配置文件的`配置项保持一致`!为了做区分,这里将编解码器改为Gson。 ```javascript public class FeignConfig { // 日志级别配置 @Bean public Logger.Level feignLoggerLevel() { return Logger.Level.BASIC; } // 契约配置 @Bean public Contract feignContract() { return new Contract.Default(); } // 超时配置 @Bean public Request.Options options() { return new Request.Options(5000, 30000); } // 编解码器配置Jackson /*@Bean public Encoder encoder() { return new JacksonEncoder(); } @Bean public Decoder decoder() { return new JacksonDecoder(); }*/ // 编解码器配置Gson @Bean public Encoder encoder() { return new GsonEncoder(); } @Bean public Decoder decoder() { return new GsonDecoder(); } // 拦截器配置 @Bean public MyFeignRequestInterceptor myFeignRequestInterceptor() { return new MyFeignRequestInterceptor(); } } ``` 有效范围说明 - `全局生效(扫描到的所有服务)` 两种方式: - 1.在FeignConfig上加`@Configuration`注解(需要保证能扫描到) - 2.在启动类的`@EnableFeignClients`注解中配置`defaultConfiguration` ```javascript @EnableFeignClients(defaultConfiguration = FeignConfig.class) ``` - `局部生效(指定服务)`:在接口API的`@FeignClient`注解中配置 ```javascript @FeignClient(value = "service-provider", configuration = FeignConfig.class) ``` ### 七.OpenFeign配置注意事项 #### 1.用对Http Client ##### 1.1 feign中http client 如果不做特殊配置,OpenFeign默认使用jdk自带的HttpURLConnection,我们知道HttpURLConnection没有连接池、性能和效率比较低,如果采用默认,很可能会遇到性能问题导致系统故障。 可以采用Apache HttpClient,properties文件中增加下面配置: ```javascript feign.httpclient.enabled=true ``` pom文件中增加依赖: ```javascript io.github.openfeign feign-httpclient 9.3.1 ``` 也可以采用OkHttpClient,properties文件中增加下面配置: ```javascript feign.okhttp.enabled=true ``` pom文件中增加依赖: ```javascript io.github.openfeign feign-okhttp 10.2.0 ``` ##### **1.2 ribbon中的Http Client** 通过OpenFeign作为含有注册中心的客户端时,默认使用Ribbon做负载均衡,Ribbon默认也是用jdk自带的HttpURLConnection,需要给Ribbon也设置一个Http client,比如使用okhttp,在properties文件中增加下面配置: ```javascript ribbon.okhttp.enabled=true ``` #### 2:全局超时时间 OpenFeign可以设置超时时间,简单粗暴,设置一个全局的超时时间,如下: ```javascript feign.client.config.default.connectTimeout=2000 feign.client.config.default.readTimeout=60000 ``` > 如果不配置超时时间,默认是连接超时10s,读超时60s,在源码feign.Request的内部类Options中定义。 这个接口设置了最大的readTimeout是60s,这个时间必须大于调用的所有外部接口的readTimeout,否则处理时间大于readTimeout的接口就会调用失败。 如下图,在一个系统中使用OpenFeign调用外部三个服务,每个服务提供两个接口,其中serviceC的一个接口需要60才能返回,那上面的readTimeout必须设置成60s。 ![img](https://gitee.com/zrclass/springcloud-demo/raw/master/doc/studydoc/OpenFeign.assets/353906d8911022a3fd42a7f8f1808499.png) 但是如果serviceA出故障了,表现是接口1超过60s才能返回,这样OpenFeign只能等到读超时,如果调用这个接口的并发量很高,会大量占用连接资源直到资源耗尽系统奔溃。要防止这样的故障发生,就必须保证接口1能fail-fast。最好的做法就是给serviceC单独设置超时时间。 #### 3.单服务设置超时时间 从上一节的讲解我们看到,需要对serviceC单独设置一个超时时间,代码如下: ```javascript feign.client.config.serviceC.connectTimeout=2000 feign.client.config.serviceC.readTimeout=60000 ``` 这个时间会覆盖第一节中默认的超时时间。但是问题又来了,serviceC中又掉了serviceD,因为serviceD的故障导致接口6发生了读超时的情况,为了不让系统奔溃,不得不对serviceC的接口5单独设置超时时间。如下图: ![img](https://gitee.com/zrclass/springcloud-demo/raw/master/doc/studydoc/OpenFeign.assets/33160a21d3305f6ecee55232f0d00a51.png) #### 4.熔断超时时间 怎样给单个接口设置超时时间,查看网上资料,必须开启熔断,配置如下: ```javascript feign.hystrix.enabled=true ``` 开启熔断后,就可以给单个接口配置超时了。如果调用serviceC的接口5的声明如下: ```javascript @FeignClient(value = "serviceC"configuration = FeignMultipartSupportConfig.class) public interface ServiceCClient { @GetMapping("/interface5") String interface5(String param); } ``` 根据上面interface5接口的声明,在properties文件中增加如下配置: ```javascript hystrix.command.ServiceCClient#interface5(param).execution.isolation.thread.timeoutInMilliseconds=60000 ``` 网上资料说的并不准确,这个超时时间并没有起作用。为什么不生效呢? ##### 4.1 使用feign超时 最终使用的超时时间来自于Options类。如果我们配置了feign的超时时间,会选择使用feign超时时间,下面代码在FeignClientFactoryBean类的configureUsingProperties方法: ```javascript if (config.getConnectTimeout() != null && config.getReadTimeout() != null) { builder.options(new Request.Options(config.getConnectTimeout(), config.getReadTimeout())); } ``` ##### 4.2 使用ribbon超时 如果没有配置feign,但是配置了ribbon的超时时间,会使用ribbon的超时时间。我们看下这段源代码,FeignLoadBalancer里面的execute方法, ```javascript public RibbonResponse execute(RibbonRequest request, IClientConfig configOverride) throws IOException { Request.Options options; if (configOverride != null) { RibbonProperties override = RibbonProperties.from(configOverride); options = new Request.Options( override.connectTimeout(this.connectTimeout), override.readTimeout(this.readTimeout)); } else { options = new Request.Options(this.connectTimeout, this.readTimeout); } //这个request里面的client就是OkHttpClient Response response = request.client().execute(request.toRequest(), options); return new RibbonResponse(request.getUri(), response); } ``` ##### 4.3 使用自定义Options 对于单个接口怎么配置超时时间,我这里给出一个方案,如果你有其他方案,欢迎探讨。我的方案是使用RestTemplate来调这个接口,单独配置超时时间,配置代码如下,这里使用OkHttpClient: ```javascript public class RestTemplateConfiguration { @Bean public OkHttp3ClientHttpRequestFactory okHttp3RequestFactory(){ OkHttp3ClientHttpRequestFactory requestFactory = new OkHttp3ClientHttpRequestFactory(); requestFactory.setConnectTimeout(2000); requestFactory.setReadTimeout(60000); return requestFactory; } @Bean @LoadBalanced public RestTemplate restTemplate(OkHttp3ClientHttpRequestFactory okHttp3RequestFactory){ return new RestTemplate(okHttp3RequestFactory); } } ``` > 为了使用ribbon负载均衡,上面加了@LoadBalanced 如果使用RestTemplate,就会使用OkHttp3ClientHttpRequestFactory中配置的时间。 #### 5.ribbon超时时间 作为负载均衡,ribbon超时时间也是可以配置的,可以在properties增加下面配置: ```javascript ribbon.ConnectTimeout=2000 ribbon.ReadTimeout=11000 ``` 复制 有文章讲ribbon配置的超时时间必须要满足接口响应时间,其实不然,配置feign的超时时间就足够了,因为它可以覆盖掉ribbon的超时时间。 #### 6.重试默认不开启 OpenFeign默认是不支持重试的,可以在源代码FeignClientsConfiguration中feignRetryer中看出。 ```javascript @Bean @ConditionalOnMissingBean public Retryer feignRetryer() { return Retryer.NEVER_RETRY; } ``` 复制 要开启重试,我们可以自定义Retryer,比如下面这行代码: ```javascript Retryer retryer = new Retryer.Default(100, 1000, 2); ``` 复制 表示每间隔100ms,最大间隔1000ms重试一次,最大重试次数是1,因为第三个参数包含了第一次请求。 #### 7.Ribbon重试 ##### 7.1 拉取服务列表 Ribbon默认从服务端拉取列表的时间间隔是30s,这个对优雅发布很不友好,一般我们会把这个时间改短,如下改成3s: ```javascript serviceC.ribbon.ServerListRefreshInterval=3 ``` ##### 7.2 重试 Ribbon重试有不少需要注意的地方,这里分享4个。 1.同一实例最大重试次数,不包括首次调用,配置如下: ```javascript serviceC.ribbon.MaxAutoRetries=1 ``` 复制 > 这个次数不包括首次调用,配置了1,重试策略会先尝试在失败的实例上重试一次,如果失败,请求下一个实例。 2.同一个服务其他实例的最大重试次数,这里不包括第一次调用的实例。默认值为1: ```javascript serviceC.ribbon.MaxAutoRetriesNextServer=1 ``` 复制 3.是否对所有操作都重试,如果改为true,则对所有操作请求都进行重试,包括post,建议采用默认配置false。 ```javascript serviceC.ribbon.OkToRetryOnAllOperations=false ``` 复制 4.对指定的http状态码进行重试 ```javascript serviceC.retryableStatusCodes=404,408,502,500 ``` 复制 #### 8.hystrix超时 如下图: ![img](https://gitee.com/zrclass/springcloud-demo/raw/master/doc/studydoc/OpenFeign.assets/80e73450084fc478ca6a1d6835a793de.png) hystrix默认不开启,但是如果开启了hystrix,因为hystrix是在Ribbon外面,所以超时时间需要符合下面规则:hystrix超时 >= (MaxAutoRetries + 1) * (ribbon ConnectTimeout + ribbon ReadTimeout) > 如果Ribbon不重试,MaxAutoRetries=0 根据上面公式,假如我们配置熔断超时时间如下: ```javascript hystrix.command.ServiceCClient#interface5(param).execution.isolation.thread.timeoutInMilliseconds=15000 ribbon.ReadTimeout=8000 ``` 复制 这个配置是不会重试一次的。serviceA调用serviceB时,hystrix会等待Ribbon返回的结果,如果Ribbon配置了重试,hystrix会一直等待直到超时。上面的配置,因为第一次请求已经耗去了8s,剩下时间7s不够请求一次了,所以是不会进行重试的。 参考博客:https://cloud.tencent.com/developer/article/1658789 ​ https://cloud.tencent.com/developer/article/1866274 ## GateWay ### 一. 网关的作用及背景 #### 1.API网关的作用 - 请求路由 在我们的系统中由于同一个接口新老两套系统都在使用,我们需要根据请求上下文将请求路由到对应的接口。 - 统一鉴权 对于鉴权操作不涉及到业务逻辑,那么可以在网关层进行处理,不用下层到业务逻辑。 - 统一监控 由于网关是外部服务的入口,所以我们可以在这里监控我们想要的数据,比如入参出参,链路时间。 - 流量控制,熔断降级 对于流量控制,熔断降级非业务逻辑可以统一放到网关层。 #### 2.GateWay产生的背景 Cloud全家桶中有个很重要的组件就是网关,在1.x版本中都是采用的Zuul网关; 但在2.x版本中,zuul的升级一直跳票,SpringCloud最后自己研发了一个网关替代Zuul, 那就是SpringCloud Gateway一句话:gateway是原zuul1.x版的替代 。 SpringCloud Gateway 是 Spring Cloud 的一个全新项目,基于 Spring 5.0+Spring Boot 2.0 和 Project Reactor 等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。 为了提升网关的性能,SpringCloud Gateway是基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty。 zuul是基于Servlet的实现,属于阻塞式编程。而SpringCloudGateway则是基于Spring5中提供的WebFlux,属于响应式编程的实现,具备更好的性能 ### 二.GateWay配置介绍 ![img](README.assets/97e1f6f046bdb0c12bf67e2ed8f649b2.png) #### 1.GateWay三大概念 - Route(路由):路由是构建网关的基本模块,它由 ID、目标 URI、一系列的断言和过滤器组成,如果断言为 true 则匹配该路由 - Predicate(断言):参考的是 Java8 中的 java.util.function.Predicate。开发人员可以匹配 HTTP 请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由 - Filter(过滤):指的是 Spring 框架中 GatewayFilter 的实例,使用过滤器,可以在请求被路由之前或之后对请求进行修改。 ```yaml spring: cloud: gateway: discovery: locator: enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由 routes: - id: service-provider # 路由标示,必须唯一 uri: lb://SERVICE-PROVIDER # 路由的目标地址;动态路由使用,lb://微服务名 predicates: # 路由断言,判断请求是否符合规则 - Path=/provider/** # 路径断言,判断路径是否是以/provider开头,如果是则符合 - id: service-consumer uri: lb://SERVICE-CONSUMER predicates: - Path=/consumer/** ``` ##### 1.1 Route(路由) gateway 中可以配置多个 `Route`。一个 `Route` 由路由 id,转发的 uri,多个 `Predicates` 以及多个 `Filters` 构成。**处理请求时会按优先级排序,找到第一个满足所有 `Predicates` 的 Route**。 ##### 1.2 Predicate(断言) Gateway中predicates配置除了Path断言工厂,还有十几个: | 名称 | 说明 | 示例 | | :--------- | :----------------------------- | :----------------------------------------------------------- | | 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 | 权重处理 | | ##### 1.3 Filter(过滤) gateway提供了31种不同的内置路由过滤器工厂。常用的如: | 名称 | 说明 | | :------------------- | :--------------------------- | | AddRequestHeader | 给当前请求添加一个请求头 | | RemoveRequestHeader | 移除请求中的一个请求头 | | AddResponseHeader | 给响应结果中添加一个响应头 | | RemoveResponseHeader | 从响应结果中移除有一个响应头 | | RequestRateLimiter | 限制请求的流量 | **1.2.1 添加过滤器** ```yaml spring: cloud: gateway: discovery: locator: enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由 routes: - id: service-provider # 路由标示,必须唯一 uri: lb://SERVICE-PROVIDER # 路由的目标地址;动态路由使用,lb://微服务名 predicates: # 路由断言,判断请求是否符合规则 - Path=/provider/** # 路径断言,判断路径是否是以/provider开头,如果是则符合 - id: service-consumer uri: lb://SERVICE-CONSUMER predicates: - Path=/consumer/** filters: # 针对某个服务添加过滤器 - AddRequestHeader=name,zhangsan default-filters: # 全局过滤器给请求添加请求头信息 - AddRequestHeader=name,zhangsan - AddRequestHeader=age,10 ``` **1.2.2 自定义全局过滤器(鉴权)** 通过实现GlobalFilter实现自定义全局过滤器 ```java @Component public class LoginFilter implements Ordered, GlobalFilter { private static final Logger logger = Logger.getLogger(LoginFilter.class.getName()); private static final String TOKEN = "token"; /** * 不拦截的urL集合 */ @Value("#{'${ignore.login.urls}'.split(',')}") public List ignoreUrl; @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { String requestUrl = exchange.getRequest().getPath().toString(); // 忽略url,不需要拦击的url,包括登录,验证码,license获取,版本获取 // isIgnore=true 不拦截,isIgnore=false 拦截,判断请求url是否在不需要拦截url列表,如果在,则直接放行 boolean isIgnore = CollectionUtil.contains(ignoreUrl, requestUrl); if (!isIgnore) { ServerHttpRequest request = exchange.getRequest(); ServerHttpResponse response = exchange.getResponse(); String token = request.getHeaders().getFirst(TOKEN); logger.info("token=" + token); if (StrUtil.isBlank(token)) { // 从header中获取token,判断 request中是否有token token = request.getQueryParams().getFirst(TOKEN); logger.info("token=" + token); if (StrUtil.isBlank(token)) { final DataBuffer buffer = this.getResponseMessage(response, -1, "认证失败"); return response.writeWith(Mono.just(buffer)); } } // 解析token,获取当前用户id // Long userId = RedisTokenUtil.fromTokenGetUserId(token); Long userId = 0l; if (userId == null) { logger.info("header中没有token或token错误"); final DataBuffer buffer = this.getResponseMessage(response, -1, "认证失败"); return response.writeWith(Mono.just(buffer)); } logger.info("header中有token,放行,检查redis"); // boolean hasUserId = RedisTokenUtil.checkLoginUserId(userId); boolean hasUserId = true; if (BooleanUtils.isFalse(hasUserId)) { logger.info("redis中不存在,用户登录已过期"); final DataBuffer buffer = this.getResponseMessage(response, 50000, "尚未登录或登录已过期,请重新登录"); return response.writeWith(Mono.just(buffer)); } // 判断根据userId存入的token是否和当前不一致,不一致证明用户在其他地方登录token变更 // boolean checkToken = RedisTokenUtil.checkLoginUserToken(userId, token); boolean checkToken = true; if (BooleanUtils.isFalse(checkToken)) { logger.info("redis中不存在,用户已登录"); final DataBuffer buffer = this.getResponseMessage(response, 50000, "用户在别处登录,强制退出!"); return response.writeWith(Mono.just(buffer)); } logger.info("和redis中匹配,放行,执行接口"); logger.info("更新redis中的过期时间"); } return chain.filter(exchange); } /** * 过滤器执行优先级 * * @return 数值越小,优先级越高 */ @Override public int getOrder() { return 1; } /** * 获取返回信息 * * @return DataBuffer */ private DataBuffer getResponseMessage(ServerHttpResponse response, int code, String message) { JSONObject messageObj = new JSONObject(); //状态码 messageObj.put("code", code); //状态信息 messageObj.put("message", message); //当前时间 messageObj.put("ctime", System.currentTimeMillis()); byte[] bits = messageObj.toString().getBytes(StandardCharsets.UTF_8); DataBuffer buffer = response.bufferFactory().wrap(bits); response.setStatusCode(HttpStatus.UNAUTHORIZED); response.getHeaders().add("Content-Type", "text/json;charset=UTF-8"); return buffer; } } ``` **1.2.3 gateway中过滤器的执行顺序** - 每一个过滤器都必须指定一个int类型的order值,**order值越小,优先级越高,执行顺序越靠前**。 - GlobalFilter通过实现Ordered接口,或者添加@Order注解来指定order值,由我们自己指定 - 路由过滤器和defaultFilter的order由gateway指定,默认是按照声明顺序从1递增。 - 当过滤器的order值一样时,会按照 defaultFilter > 路由过滤器 > GlobalFilter的顺序执行。 ### 三.GateWay工作原理 #### 1.GateWay架构原理 1.拉取注册中心的服务列表 2.获取请求地址,根据routes路由规则判断匹配所拉取的服务列表 3.路由规则匹配上了某个服务列表中的服务,负载均衡转发个对应的服务 ![在这里插入图片描述](README.assets/c86cfae288407a132d4bba18fe657989.png) #### 2.GateWay的内部流程 ![image-20230724125717842](README.assets/image-20230724125717842.png) 1)服务启动时: 1.服务启动时,加载配置文件中的路由配置 2.将路由配置转化为RouteDefinition (封装gateway路由属性信息的bean,即Route的bean定义) 3.RouteLocator读取RouteDefinition并将RouteDefinition 转换成Route对象( Route对象中定义并实例化了路由断言、过滤器、路由地址及路由优先级等信息 ),并提供了获取Route对象的方法 2)客户端请求到达时: 1.Netty Server监听到客户端请求,通过所有路由的路由断言,看是否匹配上路由 2.匹配上路由,封装具体路由的Handler(将路由的一些信息封装到ServerWebExchange对象中),并将对应的过滤器链加入到Handler中 3.拿到具体路由的执行Handler后,执行过滤器链,并通过路由信息转发请求到对应的服务 ``` RoutePredicateHandlerMapping的执行顺序 通过路由定位器获取全部路由(RouteLocator) 通过路由的谓语(Predicate)过滤掉不可用的路由信息 查找到路由信息后将路由信息设置当上下文环境中(GATEWAY_ROUTE_ATTR) 返回gatway自定的webhandler(FilteringWebHandler) ``` ![img](README.assets/ca4b2452d4aa4598d5c686dd7e9f2b3d-1690176810416.png)