Spring Cloud Feign + Hystrix + Ribbon 服务间调用 + 降级+熔断+负载均衡

2,285 阅读7分钟

Feign 微服务间的声明式调用

Feign是一个声明式的web service客户端,它使得编写web service客户端更为容易。创建接口,为接口添加注解,即可使用Feign,简而言之,它可以帮助我们封装http请求,使得我们可以像调用本地方法一样地请求其他服务的接口。

引包: compile group: 'org.springframework.cloud', name: 'spring-cloud-starter-openfeign'

启用:@EnableFeignClients
示例:
@FeignClient(name = "${feign.name}", url = "${feign.url}")
public interface StoreClient {
    //..
}
contextId:

用于一个服务下多个接口可拆分到不同类中,将同名bean(如下为store)注册到不同的context下,否则就会报bean名称冲突

@FeignClient(name = "store", contextId="store/goods", url = "${feign.url}")
public interface StoreClient1 {
    //..
}
@FeignClient(name = store", contextId="store/cash", url = "${feign.url}")
public interface StoreClient2 {
    //..
}
服务间调用时FeignClient的两种设计思路:
  • 由服务提供方将自己要暴露给其他方的方法封装进FeignClient,打成jar包,以api的方式让服务使用方引入jar包去调用,这样的好处是:

    1. jar包内的接口做为服务提供者的一种能力暴露给外界,使用者可以明确知道自己有哪些方法可用;

    2. 只要服务提供方编写一次,多个使用者可用同一个jar包,省去了每个使用者封装feignclient的工作量

  • 服务使用者在自己的服务内封装FeginClient, 这样的好处是使用者可以通过feignclient的fallback相关属性,配置方法降级后的返回

服务降级fallback的配置

两种方式: 1.Fallback.class

@FeignClient(name = "test", url = "http://localhost:${server.port}/", fallback = Fallback.class)
protected interface TestClient {

        @RequestMapping(method = RequestMethod.GET, value = "/hello")
        Hello getHello();

        @RequestMapping(method = RequestMethod.GET, value = "/hellonotfound")
        String getException();

    }

    @Component
    static class Fallback implements TestClient {

        @Override
        public Hello getHello() {
            throw new NoFallbackAvailableException("Boom!", new RuntimeException());
        }

        @Override
        public String getException() {
            return "Fixed response";
        }

    }
}

2.FallbackFactory.class, 如果需要捕获异常的话,需要使用这种方式

@FeignClient(name = "testClientWithFactory", url = "http://localhost:${server.port}/",
            fallbackFactory = TestFallbackFactory.class)
protected interface TestClientWithFactory {

        @RequestMapping(method = RequestMethod.GET, value = "/hello")
        Hello getHello();

        @RequestMapping(method = RequestMethod.GET, value = "/hellonotfound")
        String getException();

    }

    @Component
    static class TestFallbackFactory implements FallbackFactory<FallbackWithFactory> {

        @Override
        public FallbackWithFactory create(Throwable cause) {
            return new FallbackWithFactory();
        }

    }

    static class FallbackWithFactory implements TestClientWithFactory {

        @Override
        public Hello getHello() {
            throw new NoFallbackAvailableException("Boom!", new RuntimeException());
        }

        @Override
        public String getException() {
            return "Fixed response";
        }

    }
}

Hystrix 服务的降级和熔断

引包: compile group: 'org.springframework.cloud', name: 'spring-cloud-starter-netflix-hystrix

当服务的提供方出现问题时,通过hystrix可以让服务的使用方进行容错处理,避免错误在整个服务链条中蔓延。

如果要使用Hystrix,需要在feign配置中启用它:feign.hystrix.enabled = true

降级: 调用服务失败时, 可以使用fallback方法返回一个托底数据

熔断: 当调用失败达到某种设定好的阈值时,如一分钟内有70%的请求失败,便不再请求服务提供者的接口,直接返回托底数据,处在熔断状态时,则每次请求都直接返回托底数据,不再向服务提供者发请求

半熔断: 半熔断是一种恢复机制,在熔断了一定的时间后,会再将请求发往服务提供方,如果收到成功响应,则关闭熔断

隔离策略: 当我们使用了Hystrix时,Hystrix将所有的外部调用都封装成一个HystrixCommand或者HystrixObservableCommand对象,这些外部调用将会在一个独立的线程中运行。我们可以将出现问题的服务通过熔断、降级等手段隔离开来,这样不影响整个系统的主业务。

Hystrix的处理的流程图如下:

hystrix-command-flow-chart.png

常用配置:

hystrix:
  command:
    #全局默认配置
    default:
      #线程隔离相关
      execution:
        timeout:
          #是否给方法执行设置超时时间(降级相关),默认为true
          enabled: true
        isolation:
          #配置请求隔离的方式,这里是默认的线程池方式。还有一种信号量的方式semaphore
          strategy: threadPool
          thread:
            #方式执行的超时时间,默认为1000毫秒,在实际场景中需要根据情况设置
            timeoutInMilliseconds: 10000
  circuitBreaker:   #熔断器相关配置
    enabled: true   #是否启动熔断器
    requestVolumeThreshold: 20     #该属性设置滚动窗口中熔断的最小失败请求数量,如果此属性值为20,则在窗口时间内(如10s内),如果只收到19个请求且都失败了,则断路器也不会开启,默认值:20
    sleepWindowInMilliseconds: 5000    #熔断后,在此值的时间的内,hystrix会拒绝新的请求,只有过了这个时间断路器才会重新超时发送请求
    errorThresholdPercentage: 50   #设置失败百分比的阈值。如果失败比率超过这个值,则熔断

Ribbon 客户端负载均衡

引包: compile group: 'org.springframework.cloud', name: 'spring-cloud-starter-netflix-ribbon'

负载均衡可分为客户端负载均衡和服务端负载均衡。

如nginx就是服务端负载均衡的代表,nginx收到某个请求时,根据配置的负载均衡策略,从该请求域名对应的机器中选择一个,然后把请求发往这个机器。

而客户端负载均衡,则是各个微服务都注册到一个注册中心,如consul,而服务A本地维护着注册中心各个服务对应各个机器的列表,当服务A要请求服务B的服务时,它要根据负载均衡的策略,从服务B对应的机器中选择一个,发起请求。Ribbon要解决的问题就是客户端的负载均衡。

常用配置:

ribbon:
 # 当前实例上重试次数,不包含第一次请求,默认0
 MaxAutoRetries: 5
 # 切换实例的个数,不包含第一次请求的实例,如果服务注册列表小于配置值,那么会循环请求  A > B > A,默认1
 MaxAutoRetriesNextServer: 3
 #是否所有操作都进行重试
 OkToRetryOnAllOperations: false
 #连接超时时间,单位为毫秒
 ConnectTimeout: 3000
 #读取的超时时间,单位为毫秒
 ReadTimeout: 3000
# 实例配置
clientName:
  ribbon:
   MaxAutoRetries: 5
   MaxAutoRetriesNextServer: 3
   OkToRetryOnAllOperations: false
   ConnectTimeout: 3000
   ReadTimeout: 3000

OkToRetryOnAllOperations: 当该值为false时,只有Get请求会重试,当该值为true时,所有的请求都会重试,包括post,put等。 所以该值的默认值设定是false,就是为了防止post,put方法重试时,多次修改数据,而接口没有实现幂等性,导致数据出错。

我们在写接口时也要注意,不要在get请求的逻辑里去修改数据,否则重试机制可能导致接口多次调用,数据多次被修改造成不一致。

这些组件的超时时间之间有什么关系,该如何设置?

单次http请求超时时间: Ribbon 和 Feign的超时时间(ConnectTimeout,ReadTimeout)都是控制单次Http请求超时的,如果同时配置了Feign和Ribbon的超时时间,Feign的超时配置会覆盖Ribbon的配置。

实际测试中feign和配置如下图:

feignRibbon配置.png

在RetryableFeignLoadBalancer中会进行配置的覆盖,如果有feign的配置,就覆盖ribbon的,如下:

validTimeoutCode.png

一次请求的总耗时限制为:

ReqTime=(ConnectTimeout+ReadTimeout)ReqTime=(ConnectTimeout + ReadTimeout )

http请求总次数: 请求的总次数是根据ribbon的MaxAutoRetries(当前实例上重试次数,重试次数是不算第一次的)和MaxAutoRetriesNextServer(切换实例的个数,当然也是不算第一次请求的实例)来计算的,如果给单独的feignClient配置了,单独配置的优先级是高于全局的配置的。请求总次数为:

TotalNum=(MaxAutoRetries+1)(MaxAutoRetriesNextServer+1)TotalNum = (MaxAutoRetries+1)*(和MaxAutoRetriesNextServer+1)

Ribbon重试总时间限制: 但根据Ribbon的配置,每个请求耗时ReqTime,共请求TotalNum次,那么请求的总时间限制应该为:

TotalRibbonLimitTime=ReqTimeTotalNumTotalRibbonLimitTime = ReqTime * TotalNum

totalTimeOut.png

实际重试总耗时: 正常的请求ActualConnectTime和ActualReadTime应该是小于我们配置的超时时间的(如果不是,那配置就有问题,需要调整),可以重试的次数仍需要遵循ribbon配置的限制,所以实际的耗时应该为:

ActualToTalTime=ActualReqTimeTotalNum=ActualConnectTime+ActualReadTimeTotalNumActualToTalTime = ActualReqTime * TotalNum = (ActualConnectTime + ActualReadTime)* TotalNum

Hystrix超时时间: Hystrix的超时配置execution.isolation.thread.timeoutInMilliseconds(以下简称HystrixTimeout),是用来控制何时降级的,超过这个时间就返回fallback数据了。

那么到底什么时候会降级呢?

在请求超时的条件下,每一次请求都会把Ribbon的超时时间配置花光,所以降级时间为:

FallbackTime=Min(TotalRibbonLimitTime,HystrixTimeout)FallbackTime = Min(TotalRibbonLimitTime, HystrixTimeout)

在请求没超时,但是请求返回码非200的情况下,降级时间为:

FallbackTime=Min(ActualToTalTime,HystrixTimeout)FallbackTime = Min(ActualToTalTime, HystrixTimeout)

综合来看,降级时间应为:

FallbackTime=Min(ActualToTalTime,TotalRibbonLimitTime,HystrixTimeout)FallbackTime = Min(ActualToTalTime,TotalRibbonLimitTime, HystrixTimeout)

所以在实际的项目配置中,HystrixTimeout 应该稍微大于 TotalRibbonLimitTime, 如果HystrixTimeout < TotalRibbonLimitTime, 那么重试还没结束就降级返回托底数据了,但重试依然会进行,后面的重试即使成功也没有用了,但HystrixTimeout设置的远大于TotalRibbonLimitTime也没有用,因为实际的fallback时间为三个时间重较小的那一个,Min(ActualToTalTime,TotalRibbonLimitTime, HystrixTimeout)。

另外,如果服务方在feign超时时间限制内返回了结果,但返回状态码非200(400,500等),也不会进行重试,因为Ribbon的重试机制会判断非200返回的状态码是否在可重试状态码列表中,这个列表通过retryableStatusCodes来配置,retryableStatusCodes默认值是空。