当前位置 : 首页 » 文章分类 :  开发  »  Spring-Cloud-OpenFeign

Spring-Cloud-OpenFeign

Spring Cloud Feign 笔记

Feign 是一个声明式的 REST 客户端,它的目的就是让 REST 调用更加简单。
Feign 提供了 HTTP 请求的模板,通过编写简单的接口和插入注解,就可以定义好 HTTP 请求的参数、格式、地址等信息。
而 Feign 则会完全代理 HTTP 请求,我们只需要像调用方法一样调用它就可以完成服务请求及相关处理。
SpringCloud 对 Feign 进行了封装,使其支持 SpringMVC 标准注解和 HttpMessageConverters
Feign 可以与 Eureka 和 Ribbon 组合使用以支持服务注册、服务发现、负载均衡。
Feign 可以与 Hystrix 配合使用实现熔断降级。

Spring Cloud OpenFeign
https://cloud.spring.io/spring-cloud-static/spring-cloud-openfeign/2.2.2.RELEASE/reference/html/


Spring Boot 2.x中使用feign

从 Spring Boot 2.x 开始, 或者说 Spring Cloud Finchley 开始
feign starter 改名了
spring-cloud-starter-feign -> spring-cloud-starter-openfeign

同时改名的还有以下 starter
spring-cloud-starter-eureka-server —> spring-cloud-starter-netflix-eureka-server
spring-cloud-starter-eureka —> spring-cloud-starter-netflix-eureka-client
spring-cloud-starter-ribbon —> spring-cloud-starter-netflix-ribbon
spring-cloud-starter-hystrix —>spring-cloud-starter-netflix-hystrix
spring-cloud-starter-hystrix-dashboard —> spring-cloud-starter-netflix-hystrix-dashboard
spring-cloud-starter-turbine —> spring-cloud-starter-netflix-turbine
spring-cloud-starter-turbine-stream –> spring-cloud-starter-netflix-turbine-stream
spring-cloud-starter-feign —> spring-cloud-starter-openfeign
spring-cloud-starter-zuul —> spring-cloud-starter-netflix-zuul


@EnableFeignClients

@EnableFeignClients 的作用是扫描所有注解 @FeignClient 定义的feign客户端,生成其实现类。

value/basePackage

value和basePackage具有相同的功能,其中value是basePackage的别名。分别如下:
basePackage:设置自动扫描带有@FeignClient注解的基础包路径。例如:com.huangx;
value:为basePackages属性的别名,允许使用更简洁的书写方式。例如:@EnableFeignClients({“com.huangx”})
value和basePackage只能同时使用一个。

实例:下面将自动扫描 com.forezp.service 和 com.forezp.service2 包下面所有被 @FeignClient 注解修饰的类。代码如下:

@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients({"com.forezp.service", "com.forezp.service2"})
public class ServiceFeignApplication {
    public static void main(String[] args) {
        SpringApplication.run(ServiceFeignApplication.class, args);
    }
}

我们也可以使用basePackage进行设置,代码如下:

@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients(basePackages = {"com.forezp.service", "com.forezp.service2"})
public class ServiceFeignApplication {
    public static void main(String[] args) {
        SpringApplication.run(ServiceFeignApplication.class, args);
    }
}

defaultConfiguration

该属性用来自定义所有Feign客户端的配置,使用 @Configuration 进行配置。
当然也可以为某一个Feign客户端进行配置。具体配置方法见 @FeignClient的configuration 属性。

clients

设置由@FeignClient注解修饰的类列表。如果clients不是空数组,则不通过类路径自动扫描功能来加载FeignClient。实例:

@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients(clients = {SchedualServiceHi.class, SchedualServiceHi2.class})
public class ServiceFeignApplication {
    public static void main(String[] args) {
        SpringApplication.run(ServiceFeignApplication.class, args);
    }
}

上面代码中引入FeignClient客户端SchedualServiceHi,且也只引入该FeignClient客户端。


@FeignClient注解

@FeignClient 注解被 @Target(ElementType.TYPE) 修饰,表示 FeignClient 注解的作用目标在接口上
例如

@FeignClient(name = "github-client",
        url = "https://api.github.com",
        configuration = GitHubExampleConfig.class,
        fallback = GitHubClient.DefaultFallback.class)
public interface GitHubClient {
    @RequestMapping(value = "/search/repositories", method = RequestMethod.GET)
    String searchRepo(@RequestParam("q") String queryStr);

    /**
     * 容错处理类,当调用失败时,简单返回空字符串
     */
    @Component
    public class DefaultFallback implements GitHubClient {
        @Override
        public String searchRepo(@RequestParam("q") String queryStr) {
            return "";
        }
    }
}

声明接口之后,在代码中通过 @Autowired 注入 GitHubClient 接口的实例即可使用。

@FeignClient 注解的常用属性如下:
path: 定义当前FeignClient的统一前缀
decode404: 当发生http 404错误时,如果该字段位true,会调用decoder进行解码,否则抛出FeignException
fallback: 定义容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑,fallback指定的类必须实现@FeignClient标记的接口
fallbackFactory: 工厂类,用于生成fallback类示例,通过这个属性我们可以实现每个接口通用的容错逻辑,减少重复的代码

name 指定客户端名字

name: 指定 FeignClient 的名称,如果项目使用了 Eureka 服务注册 ,name 是在注册中心注册的服务的名词,大小写无关。如果没有使用服务注册,则name随意。

url 手动指定服务地址

url: url用于手动指定 @FeignClient 调用的地址,常用于调用非服务注册时的接口调用,比如调用已有的第三方http接口,使用服务注册和自动发现时不需要url。可使用配置,例如 url = "${serviceA.url}"

configuration feign配置类

configuration: Feign配置类,可以自定义Feign的Encoder、Decoder、LogLevel、Contract, 默认为 org.springframework.cloud.netflix.feign.FeignClientsConfiguration


设置header

1、 通过直接在请求上,或者在类上添加Headers的注解

@Headers({"Content-Type: application/json","Accept: application/json",Accept {contentType}})
@PostMapping(value = "/card-blank/batch-create")
Response batchCreateCard(@RequestBody CreateCardBlankDTO condition,@Param("contentType") String type);

使用 {contentType} 可以传递动态header属性

2、 通过实现RequestInterceptor接口,完成对所有的Feign请求,设置Header

import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.stereotype.Component;

@Component
public class FeignInterceptor implements RequestInterceptor{
    public void apply(RequestTemplate requestTemplate){
        requestTemplate.header("hotelId", "111111");
    }
}

Hystrix断路器(fallback)

1.4. Feign Hystrix Support
https://cloud.spring.io/spring-cloud-static/spring-cloud-openfeign/2.2.2.RELEASE/reference/html/#spring-cloud-feign-hystrix

Configuration - Hystrix Wiki
https://github.com/Netflix/Hystrix/wiki/Configuration


微服务之熔断降级

熔断
1、可熔断服务定义:服务列表、优先级
2、熔断触发:主动、被动
3、恢复时机

降级
1、可降级的服务列表
2、降级方法:分阶段降级、直接降级
3、降级触发:发生熔断、调用超时/异常
4、恢复时机

断路器
状态
Close:断路器处于关闭状态,不阻断用户请求
Open:断路器打开,阻断用户请求
closed <-> open <-> half open
窗口期到达后,进入Half open 状态,发送试探请求,测试目标服务是否可用了,如果可用,变为 closed 状态,否则转为 open 状态

统计数据
成功数、失败数、超时数、请求个数、时间

Hystrix
1、线程池隔离
调用方线程池隔离
服务方线程池隔离
2、信号量隔离
限制调用并发数

Sentinel 阿里流量防护组件


feign 中开启 hystrix断路器

在 feign 中开启 hystrix 功能,默认情况下 feign 不开启 hystrix 功能
application.yml 中开启 hystrix 即可,由于 feign 中已经包含了 hystrix ,不需要额外单独再引入 hystrix 依赖包。

# 开启Hystrix
feign.hystrix.enabled: true

feign 中给 client 接口增加 fallback 有两种方式,都是 @FeignClient 注解的参数:
1、 通过 fallback 参数指定具体的 fallback 类,这种方式无法在 fallback 中获取引起 fallback 的异常信息。
2、 通过 fallbackFactory 参数指定 fallback 工厂,这种方式可获取引起 fallback 的异常信息,推荐。

指定fallback类

指定 fallback 类的方式示例如下:

@FeignClient(name = "hello", fallback = HystrixClientFallback.class)
protected interface HystrixClient {
    @RequestMapping(method = RequestMethod.GET, value = "/hello")
    Hello iFailSometimes();
}

static class HystrixClientFallback implements HystrixClient {
    @Override
    public Hello iFailSometimes() {
        return new Hello("fallback");
    }
}

指定fallbackFactory工厂

指定 fallbackFactory 工厂的方式如下:

@FeignClient(name = "hello", fallbackFactory = HystrixClientFallbackFactory.class)
protected interface HystrixClient {
    @RequestMapping(method = RequestMethod.GET, value = "/hello")
    Hello iFailSometimes();
}

@Component
static class HystrixClientFallbackFactory implements FallbackFactory<HystrixClient> {
    @Override
    public HystrixClient create(Throwable cause) {
        return new HystrixClient() {
            @Override
            public Hello iFailSometimes() {
                return new Hello("fallback; reason was: " + cause.getMessage());
            }
        };
    }
}

1.5. Feign Hystrix Fallbacks
https://cloud.spring.io/spring-cloud-static/spring-cloud-openfeign/2.2.2.RELEASE/reference/html/#spring-cloud-feign-hystrix


Feign中使用Hystrix实现熔断降级实例

首先要开启 feign 的 hystrix 支持,application.yml 中

# 在feign中开启hystrix功能,默认情况下feign不开启hystrix功能
feign.hystrix.enabled: true

降级为0值

批量查询用户的 value,出问题降级为 0 值

@FeignClient(name = "ranks", url = "api.masikkk.com", configuration = ValuesFeignConfig.class, fallbackFactory = ValueClientFallbackFactory.class)
public interface ValueClient {
    @GetMapping(value = "/user/values")
    Response<List<ValueBean>> batchQueryValues(@RequestParam("account_ids") String accountIds);

    @Slf4j
    @Component
    class ValueClientFallbackFactory implements FallbackFactory<ValueClient> {
        @Override
        public ValueClient create(Throwable cause) {
            return accountIds -> {
                if (StringUtils.isBlank(accountIds)) {
                    return null;
                }
                List<Long> accountIdList = Splitter.on(",").omitEmptyStrings().trimResults().splitToList(accountIds).stream().map(Long::parseLong)
                        .collect(Collectors.toList());
                List<ValueBean> valueBeanList = accountIdList.stream().map(aid -> ValueBean.builder().accountId(aid).value(0L).build())
                        .collect(Collectors.toList());
                Response<List<ValueBean>> response = new Response<>();
                response.setData(valueBeanList);
                log.warn("[ValueClient#batchQueryValues] fallback, cause: {}, accountIds: {}, response: {}", cause.getMessage(), accountIdList, JSONUtils.writeValue(response));
                return response;
            };
        }
    }
}

降级为静态固定值

GisClient#getCityList 用于调接口获取城市列表,返回gis服务的直辖县列表,假如超过配置的 100+100 毫秒超时时间或 gis 服务不可用,降级为写死的固定值。
为了简单,直接把 Configuration 和 FallbackFactory 写成 client 接口的内部类了。

@FeignClient(name = "gis", url = "http://api.masikkk.com", configuration = GisFeignConfig.class, fallbackFactory = GisClientFallbackFactory.class)
public interface GisClient {
    // 获取城市列表
    @GetMapping(value = "/gis/v1/cities")
    GetCityListResponse getCityList();

    @Slf4j
    @Component
    class GisClientFallbackFactory implements FallbackFactory<GisClient> {
        @Override
        public GisClient create(Throwable cause) {
            return new GisClient() {
                @Override
                public GetCityListResponse getCityList() {
                    // 返回写死的默认list
                    log.warn("Fallback to call GIS getCityList, cause: {}", cause.getMessage());
                    GetCityListResponse response = new GetCityListResponse();
                    List<City> cityList = Lists.newArrayList();
                    cityList.add(City.builder().areaCode("429000").city("湖北省直辖县").province("湖北省").build());
                    cityList.add(City.builder().areaCode("469000").city("海南省直辖县").province("海南省").build());
                    cityList.add(City.builder().areaCode("500200").city("重庆市直辖县").province("重庆市").build());
                    cityList.add(City.builder().areaCode("659000").city("新疆维吾尔族自治区直辖县").province("新疆维吾尔自治区").build());
                    cityList.add(City.builder().areaCode("419000").city("河南省直辖县").province("河南省").build());
                    response.setData(cityList);
                    log.info("[GISClient#getCityList] fallback response: {}", JSONUtils.writeValue(response));
                    return response;
                }
            };
        }
    }

    class GisFeignConfig extends BaseFeignConfig {
        // 每个请求的超时时间,connect最多100毫秒,read最多100毫秒
        @Bean
        public Request.Options options() {
            return new Request.Options(100, 100);
        }
    }
}

降级为从缓存和数据库查询

UserSourceClient#getUserSource 用于根据 user_ids 查询用户的来源,假如调接口失败(超过配置的100+100毫秒超时时间,或对方服务不可用),降级为用 UserSourceDao 从数据库中查询配置好的默认用户来源。

@FeignClient(name = "userSourceClient", url = "api.masikkk.com", configuration = UserSourceClientConfig.class, fallbackFactory = UserSourceClientFallbackFactory.class)
public interface UserSourceClient {
    @RequestMapping(method = RequestMethod.GET, value = "uds/in/fellow/v4/sources/leads")
    QueryUserSourceResponse getUserSource(@RequestParam("user_ids") String userIds);

    @Slf4j
    @Component
    class UserSourceClientFallbackFactory implements FallbackFactory<UserSourceClient> {
        @Autowired
        private UserSourceClientFallback userSourceClientFallback;

        @Override
        public UserSourceClient create(Throwable cause) {
            log.warn("[UserSourceClient#getUserSource] fallback, cause: {}", cause.getMessage());
            return UserSourceClientFallback;
        }
    }

    @Slf4j
    @Component
    class UserSourceClientFallback implements UserSourceClient {
        @Autowired
        private UserSourceDao userSourceDao;

        @Override
        public QueryUserSourceResponse getUserSource(String userIds) {
            if (StringUtils.isBlank(userIds)) {
                return null;
            }
            SourceBean defaultSourceBean = userSourceDao.getDefaultSourceBean();
            List<Long> userIdList = Splitter.on(",").omitEmptyStrings().trimResults().splitToList(userIds).stream().map(Long::parseLong)
                    .collect(Collectors.toList());
            List<UserSourceBean> userSourceBeanList = userIdList.stream().map(uid -> {
                UserSourceBean bean = Toolkit.transformObject(defaultSourceBean, UserSourceBean.class);
                bean.setUserId(uid);
                return bean;
            }).collect(Collectors.toList());
            QueryUserSourceResponse response = new QueryUserSourceResponse();
            response.setData(userSourceBeanList);
            log.warn("[UserSourceClient#getUserSource] fallback, userIds: {}, response: {}", userIds, JSONUtils.writeValue(response));
            return response;
        }
    }

    class UserSourceClientConfig extends BaseFeignConfig {
        // 每个请求的超时时间
        @Bean
        public Request.Options options() {
            return new Request.Options(100, 100);
        }
    }
}

Hystrix配置示例

# hystrix 配置
hystrix:
  command:
    default:
      # hystrix 全局超时时间
      execution.isolation.thread.timeoutInMilliseconds: 4000
      # fallback 方法最大并发数
      fallback.isolation.semaphore.maxConcurrentRequests: 100
      # 触发熔断的10s内错误请求阈值
      circuitBreaker.requestVolumeThreshold: 200
    # 关闭 peopleclient 的超时熔断
    PeopleClient#queryEmployees(Map).execution.timeout.enabled: false
    PeopleClient#queryEmployees(String,String).execution.timeout.enabled: false
    # 懂车帝 client 超时时间设为6s
    DongCheDiClient#queryLeadsFromDongCheDi(String,Integer,Integer,String,Integer,Integer).execution.isolation.thread.timeoutInMilliseconds: 6000
    # 头条 client 超时时间设为10s
    ToutiaoClient#pullClues(String,String,Integer,Integer).execution.isolation.thread.timeoutInMilliseconds: 10000

解决HystrixRuntimeException XxClient#xxMethod() failed and no fallback available

场景:
Feign 中开启 Hystrix 后,好多通过 feign 进行的接口调用报如下错误,大致意思是接口超时

HystrixRuntimeException: PeopleClient#queryEmployees(String,String) failed and no fallback available.
HystrixRuntimeException: PeopleClient#queryEmployees(Map) timed-out and no fallback available.

比较费解,因为对于一些可能耗时比较大的接口调用,我已经在 FeignConfig 配置文件中设置了一个比较大的 Request.Options 单个请求的超时时间,不知道为什么开启 hystrix 后好多接口都 timeout 了。

原因:
查了下,原因是超过了 hystrix 的全局默认超时时间,也就是配置项 hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds 的值,这个值默认是 1s。

注意:
在 feign 中开启 hystrix 后,假如 FeignConfig 中配置的 Client 超时时间 或者 Request.Options 单个请求的超时时间比 hystrix 超时时间大,同样会报这个错,因为所有请求都已被包装为 hystrix 命令,也就是被 hystrix 代理了,超过 hystrix 超时时间都会熔断

第一次使用 hystrix 时不知道有这个问题,服务中好多接口调用偶尔会超过 1s,之前是没问题的,但开启 hystrix 后都被熔断了,但又没有配置 fallback 方法,所以出现了好多这个错误

解决方法有以下几种

增加Hystrix全局超时时间

Hystrix 默认超时时间是 1000ms,也就是 1s,为避免出上述错误可以在 application.yml 进行如下配置,改为 15 s

hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 15000

全局关闭Hystrix超时熔断

hystrix.command.default.execution.timeout.enabled: false

禁用 feign 的 hystrix

feign.hystrix.enabled: false

针对某个接口的某个方法关闭超时降级

# 关闭peopleclient的超时熔断
hystrix:
  command:
    PeopleClient#queryEmployees(Map).execution.timeout.enabled: false
    PeopleClient#queryEmployees(String,String).execution.timeout.enabled: false

针对某个接口的某个方法设置超时时间

# 开启Hystrix
feign.hystrix.enabled: true

# hystrix 配置
hystrix:
  command:
    # hystrix 全局超时时间
    default.execution.isolation.thread.timeoutInMilliseconds: 4000
    # 关闭 peopleclient 的超时熔断
    PeopleClient#queryEmployees(Map).execution.timeout.enabled: false
    PeopleClient#queryEmployees(String,String).execution.timeout.enabled: false
    # 头条、懂车帝 client 超时时间设为6s
    DongCheDiClient#queryLeadsFromDongCheDi(String,Integer,Integer,String,Integer,Integer).execution.isolation.thread.timeoutInMilliseconds: 6000
    ToutiaoClient#pullClues(String,String,Integer,Integer).execution.isolation.thread.timeoutInMilliseconds: 6000

feign中hystrix命令的写法

hystrix 命令就是配置项 hystrix.command.xxx.execution 中的 xxx, 也就是指定哪个接口方法
一般来说是 Client接口名#方法名(形参列表...)
例1

@FeignClient(name = "people", url = "api.masikkk.com", configuration = PeopleFeignConfig.class)
@RequestMapping(value = "/people/v1")
public interface PeopleClient {
     @RequestMapping(consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE, method = RequestMethod.POST)
     QueryEmployeesV2Response queryEmployees(@RequestBody Map<String, ?> request);

     default QueryEmployeesV2Response queryEmployees(String application, String secret) {
     }
}

这个 client 中的两个方法,对应的 hystrix 命令分别为

PeopleClient#queryEmployees(Map)
PeopleClient#queryEmployees(String,String)

例2

@FeignClient(name = "dongCheDi", url = "api.masikkk.com", configuration = FeignConfig.class)
public interface DongCheDiClient {

    @RequestMapping(value = "", method= RequestMethod.GET)
    DongCheDiResponse queryLeadsFromDongCheDi(@RequestParam("app_name")String appName,
                                              @RequestParam("timestamp")Integer timestamp,
                                              @RequestParam("start_time")Integer startTime,
                                              @RequestParam("token")String token,
                                              @RequestParam("limit")Integer limit,
                                              @RequestParam("last_clue_id")Integer lastClueId);
}

上面 client 中的方法,对应 hystrix 命令为
DongCheDiClient#queryLeadsFromDongCheDi(String,Integer,Integer,String,Integer,Integer)


hystrix同步主线程的MDC

HystrixConcurrencyStrategy 提供了一套默认的并发策略实现。我们可以根据我们自己不同需求通过装饰去扩展它。如每次执行 HystrixCommand 的时候都会去调用 wrapCallable(Callable) 方法,这里我们就可以通过装饰 Callable 使它提供一些额外的功能(如 ThreadLocal 上下文传递)

Hystrix实现主线程和子线程的ThreadLocal上下文传递
https://www.jianshu.com/p/2b070568ff89


Hystrix隔离策略

Hystrix 的资源隔离策略有两种:

  • 线程池隔离 THREAD (默认值),从 hystrix 线程池中找出一个线程执行,并发请求受线程池中的线程数限制
  • 信号量隔离 SEMAPHORE ,在调用线程上执行,并发请求量受信号量计数限制

配置项:

# 设置所有实例的默认值
hystrix.command.default.execution.isolation.strategy: THREAD
# 设置实例HystrixCommandKey的此属性值
hystrix.command.HystrixCommandKey.execution.isolation.strategy: SEMAPHORE

在默认情况下,推荐 HystrixCommands 使用 thread 隔离策略,HystrixObservableCommand 使用 semaphore 隔离策略
只有在高并发(单个实例每秒达到几百个调用)的调用时,才需要修改 HystrixCommands 的隔离策略为 semaphore
semaphore 隔离策略通常只用于非网络调用


THREAD 线程池隔离

线程池隔离策略: 当 n 个请求线程并发对某个接口请求调用时,会先从 hystrix 管理的线程池里面获得一个线程,然后将参数传递给这个线程去执行真正调用。线程池的大小有限,默认是 10 个线程,可以使用 maxConcurrentRequests 参数配置,如果并发请求数多于线程池线程个数,就有线程需要进入队列排队,但排队队列也有上限,默认是 5,如果排队队列也满,则必定有请求线程会走 fallback 流程。


SEMAPHORE 信号量隔离

信号量隔离策略: 当 n 个并发请求去调用一个目标服务接口时,都要获取一个信号量才能真正去调用目标服务接口,但信号量有限,默认是 10 个,可以使用 maxConcurrentRequests 参数配置,如果并发请求数多于信号量个数,就有线程需要进入队列排队,但排队队列也有上限,默认是 5,如果排队队列也满,则必定有请求线程会走 fallback 流程,从而达到限流和防止雪崩的目的。

# 设置所有实例的默认值
hystrix.command.default.execution.isolation.semaphore.maxConcurrentRequests: 10

# 设置实例 HystrixCommandKey 的此属性值
hystrix.command.HystrixCommandKey.execution.isolation.semaphore.maxConcurrentRequests: 10

线程池隔离和信号量隔离优缺点对比

1、信号量模式从始至终都只有请求线程自身,是同步调用模式,不支持超时调用,不支持直接熔断,由于没有线程的切换,开销非常小。
2、线程池模式可以支持异步调用,支持超时调用,支持直接熔断,存在线程切换,开销大。
3、使用线程池模式时,默认情况下无法将 ThreadLocal 遍历传递到执行线程中,对于需要使用 ThreadLocal 遍历进行请求跟踪的会比较麻烦。

调用量较小时,

Hystrix两种隔离模式分析
https://www.jianshu.com/p/e1a4d3bdf7c4


Hystrix断路器打开条件

正常情况下,断路器处于关闭状态(Closed),
如果调用持续出错或者超时,电路被打开进入熔断状态(Open),后续一段时间内的所有调用都会被拒绝(Fail Fast),
一段时间以后,保护器会尝试进入半熔断状态(Half-Open),允许少量请求进来尝试,
如果调用仍然失败,则回到熔断状态
如果调用成功,则回到电路闭合状态;

Hystrix在执行命令前需要检查断路器是否为打开状态:
如果断路器是打开的,那么Hystrix不会执行命令,而是转接到fallback处理逻辑
如果断路器是关闭的,那么Hystrix调到第5步(线程池/请求队列/信号量是否占满),检查是否有可用资源来执行命令

如何判断断路器的打开/关闭状态?
如果断路器打开标识为 true,直接返回true,表示断路器处于打开状态。
否则,从度量指标 metrics 中获取 HealthCounts 统计对象进一步判断(该对象记录了一个滚动时间窗内的请求信息快照,默认时间窗为10秒)

  • 如果它的请求总数 QPS 在预设的阈值范围内就返回false,表示断路器处于未打开状态,该阈值的配置参数为 circuitBreakerRequestVolumeThreshold,默认值 20
  • 如果错误百分比在阈值范围内就返回false,表示断路器处于未打开状态。该阈值的配置参数为 circuitBreakerErrorThresholdPercentage,默认值为50
  • 如果上述两个条件都不满足,就将断路器设置为打开状态(熔断/短路)。同时,如果从关闭状态切换到打开状态的话,就将时间记录到 circuitOpenedOrLastTestedTime 中。

从零学SpringCloud系列(四):服务容错保护Hystrix
https://blog.csdn.net/hao134838/article/details/105582301


Hystrix失败回退触发逻辑

四种情况会触发失败回退逻辑( fallback )。
第一种 :short-circuit ,处理链路处于熔断的回退逻辑
第二种 :semaphore-rejection ,处理信号量获得失败的回退逻辑
第三种 :thread-pool-rejection ,处理线程池提交任务拒绝的回退逻辑
第四种 :execution-timeout ,处理命令执行超时的回退逻辑
第五种 :execution-failure ,处理命令执行异常的回退逻辑
第六种 :bad-request

Hystrix(六)——失败回退逻辑
https://blog.wangqi.love/articles/Spring-Cloud/Hystrix(%E5%85%AD)%E2%80%94%E2%80%94%E5%A4%B1%E8%B4%A5%E5%9B%9E%E9%80%80%E9%80%BB%E8%BE%91.html


配置Hystrix线程池大小

ThreadPool Properties - Hystrix Wiki
https://github.com/Netflix/Hystrix/wiki/Configuration#ThreadPool


HystrixRuntimeException: XxClient#xxMethod short-circuited and no fallback available

服务中有几个外部接口调用加了 hystrix 超时降级方法,但每次服务重启或者新版本上线时,都会报大量的下面这种错误,HystrixRuntimeException: XxClient#xxMethod short-circuited and no fallback available 或者 HystrixRuntimeException: XxClient#xxMethod(String) could not be queued for execution and no fallback available ,等服务启动完成后又好了,很奇怪。
这两个异常堆栈中都能找到 java.util.concurrent.RejectedExecutionException ,也就是线程池满了,所以根本原因是 hystrix 线程池大小不够,一下子涌进来过多请求导致的。

com.netflix.hystrix.exception.HystrixRuntimeException: XxClient#xxMethod(String) short-circuited and no fallback available
...
java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@40f08d5b rejected from java.util.concurrent.ThreadPoolExecutor@5458a05c[Running, pool size = 10, active threads = 10, queued tasks = 0, completed tasks = 12]

所以,我上面描述的场景下,可能是服务刚启动时积压了一些并发请求,同时进入 XxClient#xxMethod 方法

HystrixRuntimeException: XxClient#xxMethod could not be queued for execution and no fallback available

com.netflix.hystrix.exception.HystrixRuntimeException: XxClient#xxMethod(String) could not be queued for execution and no fallback available
java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@40f08d5b rejected from java.util.concurrent.ThreadPoolExecutor@5458a05c[Running, pool size = 10, active threads = 10, queued tasks = 0, completed tasks = 12]

fallback.isolation.semaphore.maxConcurrentRequests

该属性用来设置从调用线程中允许 HystrixCommand.getFallback() 方法执行的最大并发请求数。当达到最大并发请求时,后续的请求将会被拒绝并抛出异常。
此属性对隔离策略 THREAD 和 SEMAPHORE 都起作用

# 设置所有实例的默认值
hystrix.command.default.fallback.isolation.semaphore.maxConcurrentRequests: 10
# 设置实例HystrixCommandKey的此属性值
hystrix.command.HystrixCommandKey.fallback.isolation.semaphore.maxConcurrentRequests: 10

Spring cloud系列九 Hystrix的配置属性优先级和详解
https://blog.csdn.net/hry2015/article/details/78554846


HystrixRuntimeException: XxClient#xxMethod fallback execution rejected

错误信息如下 HystrixRuntimeException: XxClient#xxMethod fallback execution rejected

Failed to call XxClient#xxMethod
com.netflix.hystrix.exception.HystrixRuntimeException: XxxClient#xxMethod(String) fallback execution rejected. com.netflix.hystrix.AbstractCommand.handleFallbackRejectionByEmittingError(AbstractCommand.java:1043) ~[hystrix-core-1.5.12.jar!/:1.5.12] com.netflix.hystrix.AbstractCommand.getFallbackOrThrowException(AbstractCommand.java:875) ~[hystrix-core-1.5.12.jar!/:1.5.12] com.netflix.hystrix.AbstractCommand.handleThreadPoolRejectionViaFallback(AbstractCommand.java:993) ~[hystrix-core-1.5.12.jar!/:1.5.12] com.netflix.hystrix.AbstractCommand.access$400(AbstractCommand.java:60) ~[hystrix-core-1.5.12.jar!/:1.5.12]
com.netflix.hystrix.AbstractCommand$12.call(AbstractCommand.java:608) ~[hystrix-core-1.5.12.jar!/:1.5.12]

原因:
大量请求同时熔断,都进入了 fallback 方法,fallback 方法默认是有个并发最大请求量限制的 fallback.isolation.semaphore.maxConcurrentRequests 默认是 10,超过这个阈值后,后续请求就会被 rejected
Hystrix 的每个 fallback 方法都有个最大并发量限制, fallback.isolation.semaphore.maxConcurrentRequests 默认值是 10,如果同时进入此 fallback 方法的请求个数超过了这个默认值,后续进入的请求就会被rejected

解决方法:
1、fallback 尽可能的简单,不要有耗时操作,如果用一个 http 接口来作为另一个 http 接口的降级处理,那你必须考虑这个 http 是不是也会失败;
2、可以适当增大 fallback.isolation.semaphore.maxConcurrentRequests


Hystrix circuit short-circuited and is OPEN

引起这个问题的原因是在一个滚动窗口内,失败次数超过触发熔断的次数,hystrix.command.default.circuitBreaker.requestVolumeThreshold 配置项,默认值是 20,就会发生短路,短路时间默认为5秒,5秒之内拒绝所有的请求,之后恢复。

# 全局配置
hystrix.command.default.circuitBreaker.requestVolumeThreshold: 20
# 指定命令配置
hystrix.command.HystrixCommandKey.circuitBreaker.requestVolumeThreshol: 20

Hystrix(六)——失败回退逻辑
https://blog.wangqi.love/articles/Spring-Cloud/Hystrix(%E5%85%AD)%E2%80%94%E2%80%94%E5%A4%B1%E8%B4%A5%E5%9B%9E%E9%80%80%E9%80%BB%E8%BE%91.html


FeignClient 配置

org.springframework.cloud.netflix.feign.FeignClientsConfiguration 中是默认配置
bean 上有 @ConditionalOnMissingBean 注解的,表示仅当上下文中不存在该实例时才实例化,如果定义了自己的该类实例会覆盖默认值。

Encoder object转http_request

Encoder
编码器,将一个对象转换成http请求体中, Spring Cloud Feign 使用 SpringEncoder

Map参数

如何使用Feign构造多参数的请求
http://www.itmuch.com/spring-cloud-sum/feign-multiple-params/

feign支持form-url-encoded

@FeignClient(name = 'client', url = 'localhost:9080', path ='/rest', configuration = CoreFeignConfiguration)
interface CoreClient {

    @RequestMapping(value = '/business', method = POST, consumes = MediaType.APPLICATION_FORM_URLENCODED)
    @Headers('Content-Type: application/x-www-form-urlencoded')
    void activate(Map<String, ?> formParams)

    public static class CoreFeignConfiguration {

      @Autowired
      private ObjectFactory<HttpMessageConverters> messageConverters

      @Bean
      public Encoder encoder() {
          return new FormEncoder();
      }
    }
}

spring boot 2.0以上,FormEncoder 在spring-cloud-starter-openfeign中
spring boot 2.0以下,需单独引入 feign-form-spring

<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

<dependency>
  <groupId>io.github.openfeign.form</groupId>
  <artifactId>feign-form-spring</artifactId>
  <version>3.4.1</version>
</dependency>

How to POST form-url-encoded data with Spring Cloud Feign
https://stackoverflow.com/questions/35803093/how-to-post-form-url-encoded-data-with-spring-cloud-feign


Decoder http_response转object

Decoder
解码器, 将一个http响应转换成一个对象,Spring Cloud Feign 使用 ResponseEntityDecoder

Retryer 重试策略

Retryer
重试策略 Retryer.NEVER_RETRY

Feign.Builder
Feign接口构建类,覆盖默认Feign.Builder,比如:HystrixFeign.Builder

FeignLoggerFactory 日志工厂

FeignLoggerFactory 日志工厂
默认为 DefaultFeignLoggerFactory

Logger.Level 日志级别

Logger.Level 日志级别
NONE, No logging (DEFAULT).
BASIC, Log only the request method and URL and the response status code and execution time.
HEADERS, Log the basic information along with request and response headers.
FULL, Log the headers, body, and metadata for both requests and responses.


Client http客户端

Feign 在默认情况下使用的是 JDK 原生的 HttpURLConnection 发送HTTP请求,没有连接池,但是对每个地址会保持一个长连接,即利用 HTTP 的 persistence connection。
我们可以用 Apache 的 HttpClient 替换 Feign 原始的 HTTP Client,通过设置连接池、超时时间等对服务之间的调用调优。
Spring Cloud 从 Brixton.SR5 版本开始支持这种替换。

HttpURLConnection

默认 http 客户端, 在 feign.Client 接口中有个 Default 实现类,使用 java 原生的 HttpURLConnection

feign.Client 接口的实现类
Http客户端接口,默认是 Client.Default ,但是我们是不使用它的默认实现,Spring Cloud Feign为我们提供了 okhttp3 和 ApacheHttpClient 两种实现方式,只需使用maven引入以下两个中的一个依赖即可,版本自由选择。

HttpClient

OKHttp

OKHttp 是现在比较常用的一个 HTTP 客户端访问工具,具有以下特点:
支持 SPDY,可以合并多个到同一个主机的请求。
使用连接池技术减少请求的延迟(如果SPDY是可用的话)。
使用 GZIP 压缩减少传输的数据量。
缓存响应避免重复的网络请求。


IOException:unexpected end of stream on Connection

feign 接口调用报如下错误,这是 Pinpoint 调用链监控发现的,服务中并没有直接报错

unexpected end of stream on Connection{api1.masikkk.com:443, proxy=DIRECT hostAddress=api1.masikkk.com/10.128.199.175:443 cipherSuite=TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 protocol=http/1.1}
unexpected end of stream on Connection{api2.masikkk.com:80, proxy=DIRECT hostAddress=api2.masikkk.com/192.168.128.98:80 cipherSuite=none protocol=http/1.1}

原因:
The error occurs when OkHttp try to reuse a connection that is in FIN_WAIT2 state in server, because the server keep_alive timeout is lesser than the client timeout
即 server 端的 tcp 连接超时时间比客户端短,server 端已经将一个连接标为超时且断开了,但 OKHttp client 还尝试用之前的连接去访问,就会报这个错。

在 http1.1 中 request 和 reponse header 中都有可能出现一个 Connection header,此 header 的含义是当 client 和 server 通信时对于长链接如何进行处理。
在 http1.1 中,client 和 server 都是默认对方支持长链接的, 如果 client 使用 http1.1 协议,但又不希望使用长链接,则需要在 header 中指明 Connection 的值为 close ;如果 server 方也不想支持长链接,则在 response 中也需要明确说明 Connection 的值为 close

解决:
给 OkHttp client 增加 Connection:close header,通过构造 OkHttp 时增加拦截器实现,如下:

OkHttpClient client = OkHttpClientFactory.getInstance().newBuilder()
        .retryOnConnectionFailure(true)
        .addNetworkInterceptor(chain -> {
            okhttp3.Request request = chain.request().newBuilder().addHeader("Connection", "close").build();
            return chain.proceed(request);
        })
        .build();

IOException: unexpected end of stream on Connection的解决方法
https://www.jianshu.com/p/8753188b315c

java.io.IOException: unexpected end of stream on Connection? [duplicate]
https://stackoverflow.com/questions/52726909/java-io-ioexception-unexpected-end-of-stream-on-connection

OkHttp3 - IOException: unexpected end of stream on okhttp3.Address@9d7c59b5
https://github.com/square/okhttp/issues/2738


Request.Options 每个请求的超时时间

// 每个请求的超时时间
@Bean
public Request.Options options() {
    return new Request.Options(5 * 1000, 6 * 1000);
}

ErrorDecoder

当调用服务时,如果服务返回的状态码不是200,就会进入到 Feign 的 ErrorDecoder 中,因此如果我们要解析异常信息,就要重写 ErrorDecoder

RequestInterceptor 请求拦截器

feign.RequestInterceptor 接口的实现类

拦截器无法定义顺序

No guarantees are give with regards to the order that interceptors are applied.

Contract

Contract
处理Feign接口注解, 默认为 SpringMvcContract ,处理Spring mvc 注解,也就是我们为什么可以用 Spring mvc 注解的原因。


FeignClientsConfiguration 源码

org.springframework.cloud.netflix.feign.FeignClientsConfiguration 源码:

package org.springframework.cloud.netflix.feign;

import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
import org.springframework.cloud.netflix.feign.support.ResponseEntityDecoder;
import org.springframework.cloud.netflix.feign.support.SpringDecoder;
import org.springframework.cloud.netflix.feign.support.SpringEncoder;
import org.springframework.cloud.netflix.feign.support.SpringMvcContract;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.core.convert.ConversionService;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.format.support.FormattingConversionService;

import com.netflix.hystrix.HystrixCommand;

import feign.Contract;
import feign.Feign;
import feign.Logger;
import feign.Retryer;
import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.hystrix.HystrixFeign;

@Configuration
public class FeignClientsConfiguration {

    @Autowired
    private ObjectFactory<HttpMessageConverters> messageConverters;

    @Autowired(required = false)
    private List<AnnotatedParameterProcessor> parameterProcessors = new ArrayList<>();

    @Autowired(required = false)
    private List<FeignFormatterRegistrar> feignFormatterRegistrars = new ArrayList<>();

    @Autowired(required = false)
    private Logger logger;

    @Bean
    @ConditionalOnMissingBean
    public Decoder feignDecoder() {
        return new ResponseEntityDecoder(new SpringDecoder(this.messageConverters));
    }

    @Bean
    @ConditionalOnMissingBean
    public Encoder feignEncoder() {
        return new SpringEncoder(this.messageConverters);
    }

    @Bean
    @ConditionalOnMissingBean
    public Contract feignContract(ConversionService feignConversionService) {
        return new SpringMvcContract(this.parameterProcessors, feignConversionService);
    }

    @Bean
    public FormattingConversionService feignConversionService() {
        FormattingConversionService conversionService = new DefaultFormattingConversionService();
        for (FeignFormatterRegistrar feignFormatterRegistrar : feignFormatterRegistrars) {
            feignFormatterRegistrar.registerFormatters(conversionService);
        }
        return conversionService;
    }

    @Configuration
    @ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class })
    protected static class HystrixFeignConfiguration {
        @Bean
        @Scope("prototype")
        @ConditionalOnMissingBean
        @ConditionalOnProperty(name = "feign.hystrix.enabled", matchIfMissing = false)
        public Feign.Builder feignHystrixBuilder() {
            return HystrixFeign.builder();
        }
    }

    @Bean
    @ConditionalOnMissingBean
    public Retryer feignRetryer() {
        return Retryer.NEVER_RETRY;
    }

    @Bean
    @Scope("prototype")
    @ConditionalOnMissingBean
    public Feign.Builder feignBuilder(Retryer retryer) {
        return Feign.builder().retryer(retryer);
    }

    @Bean
    @ConditionalOnMissingBean(FeignLoggerFactory.class)
    public FeignLoggerFactory feignLoggerFactory() {
        return new DefaultFeignLoggerFactory(logger);
    }
}

Spring Cloud中如何优雅的使用Feign调用接口
https://segmentfault.com/a/1190000012496398

OpenFeign/feign
https://github.com/OpenFeign/feign


上一篇 阿里巴巴Java开发手册

下一篇 Apache-Commons-IO使用笔记

阅读
评论
7,270
阅读预计32分钟
创建日期 2019-03-30
修改日期 2020-09-18
类别
目录
  1. Spring Boot 2.x中使用feign
  2. @EnableFeignClients
    1. value/basePackage
    2. defaultConfiguration
    3. clients
  3. @FeignClient注解
    1. name 指定客户端名字
    2. url 手动指定服务地址
    3. configuration feign配置类
    4. 设置header
  4. Hystrix断路器(fallback)
    1. 微服务之熔断降级
    2. feign 中开启 hystrix断路器
    3. 指定fallback类
    4. 指定fallbackFactory工厂
    5. Feign中使用Hystrix实现熔断降级实例
      1. 降级为0值
      2. 降级为静态固定值
      3. 降级为从缓存和数据库查询
      4. Hystrix配置示例
    6. 解决HystrixRuntimeException XxClient#xxMethod() failed and no fallback available
      1. 增加Hystrix全局超时时间
      2. 全局关闭Hystrix超时熔断
      3. 禁用 feign 的 hystrix
      4. 针对某个接口的某个方法关闭超时降级
      5. 针对某个接口的某个方法设置超时时间
    7. feign中hystrix命令的写法
    8. hystrix同步主线程的MDC
    9. Hystrix隔离策略
      1. THREAD 线程池隔离
      2. SEMAPHORE 信号量隔离
      3. 线程池隔离和信号量隔离优缺点对比
    10. Hystrix断路器打开条件
    11. Hystrix失败回退触发逻辑
    12. 配置Hystrix线程池大小
      1. HystrixRuntimeException: XxClient#xxMethod short-circuited and no fallback available
      2. HystrixRuntimeException: XxClient#xxMethod could not be queued for execution and no fallback available
    13. fallback.isolation.semaphore.maxConcurrentRequests
      1. HystrixRuntimeException: XxClient#xxMethod fallback execution rejected
    14. Hystrix circuit short-circuited and is OPEN
  5. FeignClient 配置
    1. Encoder object转http_request
      1. Map参数
      2. feign支持form-url-encoded
    2. Decoder http_response转object
    3. Retryer 重试策略
    4. FeignLoggerFactory 日志工厂
    5. Logger.Level 日志级别
    6. Client http客户端
      1. HttpURLConnection
      2. HttpClient
      3. OKHttp
        1. IOException:unexpected end of stream on Connection
    7. Request.Options 每个请求的超时时间
    8. ErrorDecoder
    9. RequestInterceptor 请求拦截器
    10. Contract
    11. FeignClientsConfiguration 源码

页面信息

location:
protocol:
host:
hostname:
origin:
pathname:
href:
document:
referrer:
navigator:
platform:
userAgent:

评论