大版本号对应关系:
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是一个基于REST的服务,主要用于AWS(Amazon Web Services 亚马逊云计算服务)云中的定位服务,以实现中间层服务器的负载平衡和故障转移在 Spring Cloud 微服务架构中通常用作注册中心, 我们称这个服务为 Eureka Server,还有一个与之交互的客户端称之为 Eureka Client .
如上图所示,其中
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上剔除.
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注册表数据,并将其向下传播到所有客户端。
任何客户端,如果连续3次心跳更新失败,那么它将被视为非正常终止,病句将被剔除。当超过当前注册实例15%的客户端都处于这种状态,那么自我保护将被开启。
举例:比如你有10个user-service节点注册到eureka-server中,这个时候挂了两台。那么正常比例就是:(10-2)/10 = 80% 。只有80%的节点正常,20%的节点不正常(超过15%的异常率)。 这个时候就会引发自我保护机制。
当自我保护开启以后,eureka服务器将停止剔除所有实例,直到:
默认情况下,自我保护是启用的,并且,默认的阈值是要大于当前注册数量的15%
从图中可以看出 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:
上图中的 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 还在,就能保证注册服务可用(保证可用性),只不过查到的信息可能不是最新的(不保证强一致性)。
1.EurekaServer配置解析
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配置解析
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
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基本工作流程
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
微服务设于基于RESTful架构,使用RESTful可以将愈发复杂单体应用通过HTTP请求、JSON传输数据拆分为不同的业务模块,达到服务独立部署、快速启动、模块协同开发、低耦合、代码复用、职责单一的目的,使团队间相对隔离的敏捷式开发。微服务的盛行首要解决的便是不同服务间调用的问题。
现有解决方案
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注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。
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
关键字实现接口。
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
是Netflix
发布的开源项目,提供在服务消费端实现负载均衡调用服务提供者,从注册中心读取所有可用的服务提供者,在客户端每次调用接口时采用如轮询负载均衡算法选出一个服务提供者调用,因此,Ribbon
是一个客户端负载均衡器。
Ribbon
提供多种负载均衡算法的实现、提供重试支持。Feign
也提供重试支持,在SynchronousMethodHandler
的invoke
方法中实现,但Feign
的重试比较简单,只是向同一个服务节点发送请求,而Ribbon
的失败重试是支持重新选择一个服务节点调用的,在服务提供者部署多个节点的情况下,显然Feign
的重试机制意义不大。
Ribbon远程调用的底层原理:
Ribbon
与Fegin
整合的桥梁是FeignLoadBalancer
。
Ribbon
会注册一个ILoadBalancer
(默认使用实现类ZoneAwareLoadBalancer
)负载均衡器,Feign
通过LoadBalancerFeignClient
调用FeignLoadBalancer
的executeWithLoadBalancer
方法来使用Ribbon
的ILoadBalancer
负载均衡器选择一个提供者节点发送http
请求,实际发送请求还是OpenFeign
的FeignLoadBalancer
发起的,Ribbon
从始至终都只负责负载均衡选出一个服务节点。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
。
Ribbon
默认提供了ConfigurationBasedServerList
实现是ServerList
,默认从配置文件中获取服务节点。 但是eureka中使用DiscoveryEnabledNIWSServerList实现了ServerList
接口,通过EurekaClient来拉取目标服务信息。
@Override
public List<DiscoveryEnabledServer> getUpdatedListOfServers(){
return obtainServersViaDiscovery();
}
private List<DiscoveryEnabledServer> obtainServersViaDiscovery() {
List<DiscoveryEnabledServer> serverList = new ArrayList<DiscoveryEnabledServer>();
if (eurekaClientProvider == null || eurekaClientProvider.get() == null) {
logger.warn("EurekaClient has not been initialized yet, returning an empty list");
return new ArrayList<DiscoveryEnabledServer>();
}
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<InstanceInfo> listOfInstanceInfo = eurekaClient.getInstancesByVipAddress(vipAddress, isSecure, targetRegion); // 拉取
...
}
return serverList;
}
Ribbon
提供RetryHandler
接口,并且默认使用DefaultLoadBalancerRetryHandler
。LoadBalancerCommand
的submit
方法中(在FeignLoadBalancer
的executeWithLoadBalancer
方法中调用),如果配置重试次数大于0
,则会调用RxJava
的API
支持重试。
public Observable<T> submit(final ServerOperation<T> operation) {
// .......
final int maxRetrysSame = retryHandler.getMaxRetriesOnSameServer();
final int maxRetrysNext = retryHandler.getMaxRetriesOnNextServer();
// Use the load balancer
Observable<T> o = (server == null ? selectServer() : Observable.just(server))
.concatMap(new Func1<Server, Observable<T>>() {
@Override
public Observable<T> 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
方法返回的是一个判断是否重试的决策者,由该决策者决定是否需要重试(抛出的异常是否允许重试,是否达到最大重试次数)。
private Func2<Integer, Throwable, Boolean> retryPolicy(final int maxRetrys, final boolean same) {
return new Func2<Integer, Throwable, Boolean>() {
@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的架构图
Feign本身提供了很多扩展点,例如:
这些扩展点,我们在使用原生Feign时,可以通过Feign.Builder
指定,最后再通过target
生成动态代理类,完成Bean注册
application.properties,格式:
feign.client.config.{服务名}.{配置名} = {配置值}
我们配置一些你可能用的上的扩展项,比如:日志级别配置
、契约配置
、超时配置
、编解码配置
、拦截器配置
,如下:
# 日志级别配置
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服务
有效。 feign.client.config.service-provider.loggerLevel = BASIC
验证是否生效
我使用service-consumer发起调用,可以在启动service-consumer 启动服务 时,构建 动态代理前 打断点查看Feign.Builder。
即在FeignClientFactoryBean.loadBalance
方法的调target
之前打断点:
配置后的Feign.Builder
,确认已经按application.properties配置:
通过Java代码配置的话需要定义一个配置类,例如我命名为:FeignConfig
,里面定义需要配置的@Bean
,与上面配置文件的配置项保持一致
!为了做区分,这里将编解码器改为Gson。
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();
}
}
有效范围说明
全局生效(扫描到的所有服务)
两种方式:
@Configuration
注解(需要保证能扫描到)@EnableFeignClients
注解中配置defaultConfiguration
@EnableFeignClients(defaultConfiguration = FeignConfig.class)
局部生效(指定服务)
:在接口API的@FeignClient
注解中配置@FeignClient(value = "service-provider", configuration = FeignConfig.class)
如果不做特殊配置,OpenFeign默认使用jdk自带的HttpURLConnection,我们知道HttpURLConnection没有连接池、性能和效率比较低,如果采用默认,很可能会遇到性能问题导致系统故障。
可以采用Apache HttpClient,properties文件中增加下面配置:
feign.httpclient.enabled=true
pom文件中增加依赖:
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
<version>9.3.1</version>
</dependency>
也可以采用OkHttpClient,properties文件中增加下面配置:
feign.okhttp.enabled=true
pom文件中增加依赖:
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
<version>10.2.0</version>
</dependency>
通过OpenFeign作为含有注册中心的客户端时,默认使用Ribbon做负载均衡,Ribbon默认也是用jdk自带的HttpURLConnection,需要给Ribbon也设置一个Http client,比如使用okhttp,在properties文件中增加下面配置:
ribbon.okhttp.enabled=true
OpenFeign可以设置超时时间,简单粗暴,设置一个全局的超时时间,如下:
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。
但是如果serviceA出故障了,表现是接口1超过60s才能返回,这样OpenFeign只能等到读超时,如果调用这个接口的并发量很高,会大量占用连接资源直到资源耗尽系统奔溃。要防止这样的故障发生,就必须保证接口1能fail-fast。最好的做法就是给serviceC单独设置超时时间。
从上一节的讲解我们看到,需要对serviceC单独设置一个超时时间,代码如下:
feign.client.config.serviceC.connectTimeout=2000
feign.client.config.serviceC.readTimeout=60000
这个时间会覆盖第一节中默认的超时时间。但是问题又来了,serviceC中又掉了serviceD,因为serviceD的故障导致接口6发生了读超时的情况,为了不让系统奔溃,不得不对serviceC的接口5单独设置超时时间。如下图:
怎样给单个接口设置超时时间,查看网上资料,必须开启熔断,配置如下:
feign.hystrix.enabled=true
开启熔断后,就可以给单个接口配置超时了。如果调用serviceC的接口5的声明如下:
@FeignClient(value = "serviceC"configuration = FeignMultipartSupportConfig.class)
public interface ServiceCClient {
@GetMapping("/interface5")
String interface5(String param);
}
根据上面interface5接口的声明,在properties文件中增加如下配置:
hystrix.command.ServiceCClient#interface5(param).execution.isolation.thread.timeoutInMilliseconds=60000
网上资料说的并不准确,这个超时时间并没有起作用。为什么不生效呢?
最终使用的超时时间来自于Options类。如果我们配置了feign的超时时间,会选择使用feign超时时间,下面代码在FeignClientFactoryBean类的configureUsingProperties方法:
if (config.getConnectTimeout() != null && config.getReadTimeout() != null) {
builder.options(new Request.Options(config.getConnectTimeout(), config.getReadTimeout()));
}
如果没有配置feign,但是配置了ribbon的超时时间,会使用ribbon的超时时间。我们看下这段源代码,FeignLoadBalancer里面的execute方法,
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);
}
对于单个接口怎么配置超时时间,我这里给出一个方案,如果你有其他方案,欢迎探讨。我的方案是使用RestTemplate来调这个接口,单独配置超时时间,配置代码如下,这里使用OkHttpClient:
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中配置的时间。
作为负载均衡,ribbon超时时间也是可以配置的,可以在properties增加下面配置:
ribbon.ConnectTimeout=2000
ribbon.ReadTimeout=11000
复制
有文章讲ribbon配置的超时时间必须要满足接口响应时间,其实不然,配置feign的超时时间就足够了,因为它可以覆盖掉ribbon的超时时间。
OpenFeign默认是不支持重试的,可以在源代码FeignClientsConfiguration中feignRetryer中看出。
@Bean
@ConditionalOnMissingBean
public Retryer feignRetryer() {
return Retryer.NEVER_RETRY;
}
复制
要开启重试,我们可以自定义Retryer,比如下面这行代码:
Retryer retryer = new Retryer.Default(100, 1000, 2);
复制
表示每间隔100ms,最大间隔1000ms重试一次,最大重试次数是1,因为第三个参数包含了第一次请求。
Ribbon默认从服务端拉取列表的时间间隔是30s,这个对优雅发布很不友好,一般我们会把这个时间改短,如下改成3s:
serviceC.ribbon.ServerListRefreshInterval=3
Ribbon重试有不少需要注意的地方,这里分享4个。
1.同一实例最大重试次数,不包括首次调用,配置如下:
serviceC.ribbon.MaxAutoRetries=1
复制
这个次数不包括首次调用,配置了1,重试策略会先尝试在失败的实例上重试一次,如果失败,请求下一个实例。
2.同一个服务其他实例的最大重试次数,这里不包括第一次调用的实例。默认值为1:
serviceC.ribbon.MaxAutoRetriesNextServer=1
复制
3.是否对所有操作都重试,如果改为true,则对所有操作请求都进行重试,包括post,建议采用默认配置false。
serviceC.ribbon.OkToRetryOnAllOperations=false
复制
4.对指定的http状态码进行重试
serviceC.retryableStatusCodes=404,408,502,500
复制
如下图:
hystrix默认不开启,但是如果开启了hystrix,因为hystrix是在Ribbon外面,所以超时时间需要符合下面规则:hystrix超时 >= (MaxAutoRetries + 1) * (ribbon ConnectTimeout + ribbon ReadTimeout)
如果Ribbon不重试,MaxAutoRetries=0
根据上面公式,假如我们配置熔断超时时间如下:
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
在我们的系统中由于同一个接口新老两套系统都在使用,我们需要根据请求上下文将请求路由到对应的接口。
对于鉴权操作不涉及到业务逻辑,那么可以在网关层进行处理,不用下层到业务逻辑。
由于网关是外部服务的入口,所以我们可以在这里监控我们想要的数据,比如入参出参,链路时间。
对于流量控制,熔断降级非业务逻辑可以统一放到网关层。
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,属于响应式编程的实现,具备更好的性能
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/**
gateway 中可以配置多个 Route
。一个 Route
由路由 id,转发的 uri,多个 Predicates
以及多个 Filters
构成。处理请求时会按优先级排序,找到第一个满足所有 Predicates
的 Route。
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 | 权重处理 |
gateway提供了31种不同的内置路由过滤器工厂。常用的如:
名称 | 说明 |
---|---|
AddRequestHeader | 给当前请求添加一个请求头 |
RemoveRequestHeader | 移除请求中的一个请求头 |
AddResponseHeader | 给响应结果中添加一个响应头 |
RemoveResponseHeader | 从响应结果中移除有一个响应头 |
RequestRateLimiter | 限制请求的流量 |
1.2.1 添加过滤器
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实现自定义全局过滤器
@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<String> ignoreUrl;
@Override
public Mono<Void> 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中过滤器的执行顺序
1.拉取注册中心的服务列表
2.获取请求地址,根据routes路由规则判断匹配所拉取的服务列表
3.路由规则匹配上了某个服务列表中的服务,负载均衡转发个对应的服务
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)
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。