写点什么

SpringCloud 技术专题 -Hystrix 以及 FeginClient 异常

发布于: 2021 年 03 月 31 日
SpringCloud技术专题-Hystrix以及FeginClient异常

问题 1

使用 Feign 调用服务,当触发熔断机制时,遇到了以下问题:

异常形如:TestService#addRecord(ParamVO) failed and no fallback available.;




  • 获取不到服务提供方抛出的原始异常信息;

  • 实现某些业务方法不进入熔断,直接往外抛出异常;

  • 接下来将一一解决上述问题。



failed and no fallback available.这种异常信息,是因为项目开启了熔断:


feign.hystrix.enabled: true
复制代码


当调用服务时抛出了异常,却没有定义 fallback 方法,就会抛出上述异常。由此引出了第一个解决方式。当调用服务时抛出了异常,却没有定义 fallback 方法,就会抛出上述异常。由此引出了第一个解决方式。



  • @FeignClient 加上 fallback 方法,并获取异常信息。

  • @FeignClient 修饰的接口加上 fallback 方法有两种方式,由于要获取异常信息,所以使用 fallbackFactory 的方式:

@FeignClient(name = "serviceId", fallbackFactory = TestServiceFallback.class)public interface TestService {    @RequestMapping(value = "/get/{id}", method = RequestMethod.GET)    Result get(@PathVariable("id") Integer id);    }
复制代码

@FeignClient注解中指定fallbackFactory,上面例子中是TestServiceFallback

import feign.hystrix.FallbackFactory;import org.apache.commons.lang3.StringUtils;@Componentpublic class TestServiceFallback implements FallbackFactory<TestService> {    private static final Logger LOG = LoggerFactory.getLogger(TestServiceFallback.class);    public static final String ERR_MSG = "Test接口暂时不可用: ";    @Override    public TestService create(Throwable throwable) {        String msg = throwable == null ? "" : throwable.getMessage();        if (!StringUtils.isEmpty(msg)) {            LOG.error(msg);        }        return new TestService() {            @Override            public String get(Integer id) {                return ResultBuilder.unsuccess(ERR_MSG + msg);            }        };    }}
复制代码

通过实现FallbackFactory,可以在create方法中获取到服务抛出的异常。但是请注意,这里的异常是被Feign封装过的异常,不能直接在异常信息中看出原始方法抛出的异常。这时得到的异常信息形如:

status 500 reading TestService#addRecord(ParamVO); content:{"success":false,"resultCode":null,"message":"/ by zero","model":null,"models":[],"pageInfo":null,"timelineInfo":null,"extra":null,"validationMessages":null,"valid":false}
复制代码

说明一下,本例子中,服务提供者的接口返回信息会统一封装在自定义类Result中,内容就是上述的content

{"success":false,"resultCode":null,"message":"/ by zero","model":null,"models":[],"pageInfo":null,"timelineInfo":null,"extra":null,"validationMessages":null,"valid":false}
复制代码

因此,异常信息我希望是message的内容:/ by zero,这样打日志时能够方便识别异常。

保留原始异常信息

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

import feign.Response;import feign.Util;import feign.codec.ErrorDecoder;/** * @Author: CipherCui * @Description: 保留 feign 服务异常信息 * @Date: Created in 1:29 2018/6/2 */public class KeepErrMsgConfiguration {    @Bean    public ErrorDecoder errorDecoder() {        return new UserErrorDecoder();    }    /**     * 自定义错误     */    public class UserErrorDecoder implements ErrorDecoder {        private Logger logger = LoggerFactory.getLogger(getClass());        @Override        public Exception decode(String methodKey, Response response) {            Exception exception = null;            try {                // 获取原始的返回内容                String json = Util.toString(response.body().asReader());                exception = new RuntimeException(json);                // 将返回内容反序列化为Result,这里应根据自身项目作修改                Result result = JsonMapper.nonEmptyMapper().fromJson(json, Result.class);                // 业务异常抛出简单的 RuntimeException,保留原来错误信息                if (!result.isSuccess()) {                    exception = new RuntimeException(result.getMessage());                }            } catch (IOException ex) {                logger.error(ex.getMessage(), ex);            }            return exception;        }    }}
复制代码

上面是一个例子,原理是根据response.body()反序列化为自定义的Result类,提取出里面的message信息,然后抛出RuntimeException,这样当进入到熔断方法中时,获取到的异常就是我们处理过的RuntimeException

注意上面的例子并不是通用的,但原理是相通的,大家要结合自身的项目作相应的修改,要使上面代码

发挥作用,还需要在@FeignClient注解中指定configuration

@FeignClient(name = "serviceId", fallbackFactory = TestServiceFallback.class, 	configuration = {KeepErrMsgConfiguration.class})public interface TestService {    @RequestMapping(value = "/get/{id}", method = RequestMethod.GET)    String get(@PathVariable("id") Integer id);}
复制代码

不进入熔断,直接抛出异常

有时我们并不希望方法进入熔断逻辑,只是把异常原样往外抛。这种情况我们只需要捉住两个点:不进入熔断原样

原样就是获取原始的异常,上面已经介绍过了,而不进入熔断,需要把异常封装成HystrixBadRequestException,对于HystrixBadRequestExceptionFeign会直接抛出,不进入熔断方法。

因此我们只需要在上述KeepErrMsgConfiguration的基础上作一点修改即可:

public class NotBreakerConfiguration {    @Bean    public ErrorDecoder errorDecoder() {        return new UserErrorDecoder();    }    /**     * 自定义错误     */    public class UserErrorDecoder implements ErrorDecoder {        private Logger logger = LoggerFactory.getLogger(getClass());        @Override        public Exception decode(String methodKey, Response response) {            Exception exception = null;            try {                String json = Util.toString(response.body().asReader());                exception = new RuntimeException(json);                Result result = JsonMapper.nonEmptyMapper().fromJson(json, Result.class);                // 业务异常包装成 HystrixBadRequestException,不进入熔断逻辑                if (!result.isSuccess()) {                    exception = new HystrixBadRequestException(result.getMessage());                }            } catch (IOException ex) {                logger.error(ex.getMessage(), ex);            }            return exception;        }    }}
复制代码

总结

为了更好的达到熔断效果,我们应该为每个接口指定fallback方法。而根据自身的业务特点,可以灵活的配置上述的KeepErrMsgConfigurationNotBreakerConfiguration,或自己编写Configuration

问题 2

com.netflix.hystrix.exception.HystrixRuntimeException:XXXXXXXXX could not be queued for execution and no fallback available.
复制代码

隔离服务的线程池的堆积队列满了

说明:

1、由于线程池的最大数量导致的,官方说随着线程池的数量越大,资源开销也就越大,所以调整时要慎重。

2、Hystrix 默认是 10 个线程,超过就会报这个异常。


hystrix:  threadpool:    default:      coreSize: 200 ##并发执行的最大线程数,默认10      maxQueueSize: 200 ##BlockingQueue的最大队列数      queueSizeRejectionThreshold: 50 ##即使maxQueueSize没有达到,达到queueSizeRejectionThreshold该值后,请求也会被拒绝    default:      execution:        timeout:          enabled: true        isolation:          strategy: THREAD          semaphore:            maxConcurrentRequests: 1000          thread:            timeoutInMilliseconds: 30000
复制代码


com.netflix.hystrix.exception.HystrixRuntimeException:XXXXXXXX timed-out and no fallback available.
复制代码

超时,并且没有 fallback 方法

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

默认 1000ms

hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=10000,可以修改大点,如果不确定超时时间,或者可以关闭,都不超时了

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


 abstract class HystrixThreadPoolProperties {    static int default_coreSize = 10;    static int default_maximumSize = 10;    static int default_keepAliveTimeMinutes = 1;    static int default_maxQueueSize = -1;    static boolean default_allow_maximum_size_to_diverge_from_core_size = false;    static int default_queueSizeRejectionThreshold = 5;    static int default_threadPoolRollingNumberStatisticalWindow = 10000;    static int default_threadPoolRollingNumberStatisticalWindowBuckets = 10; }
复制代码


Feign 自定义 Configuration 和 root 容器有效隔离:

  • 用 @Configuration 注解

  • 不能在主 @ComponentScan (or @SpringBootApplication)范围内,从其包名上分离

  • 注意避免包扫描重叠,最好的方法是明确的指定包名

Spring Cloud Netflix 提供了默认的 Bean 类型:

  • Decoder feignDecoder: ResponseEntityDecoder (which wraps a SpringDecoder)

  • Encoder feignEncoder: SpringEncoder

  • Logger feignLogger: Slf4jLogger

  • Contract feignContract: SpringMvcContract

  • Feign.Builder feignBuilder: HystrixFeign.Builder

Spring Cloud Netflix 没有默认值,可以在 feign 上下文配置:

  • Logger.Level

  • Retryer

  • ErrorDecoder

  • Request.Options

  • Collection<RequestInterceptor>

自定义 feign 的消息编码:

不要在如下代码中 getObject 方法内 new 对象,外部会频繁调用 getObject 方法。

ObjectFactory<HttpMessageConverters> messageConvertersObjectFactory = new ObjectFactory<HttpMessageConverters>() {    @Override    public HttpMessageConverters getObject() throws BeansException {        return httpMessageConverters;    }};
复制代码

Feign 配置

#Hystrix支持,如果为true,hystrix库必须在classpath中feign.hystrix.enabled=false#请求和响应GZIP压缩支持feign.compression.request.enabled=truefeign.compression.response.enabled=true#支持压缩的mime typesfeign.compression.request.enabled=truefeign.compression.request.mime-types=text/xml,application/xml,application/jsonfeign.compression.request.min-request-size=2048# 日志支持logging.level.project.user.UserClient: DEBUG
复制代码

Logger.Level 支持

必须为每一个 Feign Client 配置来告诉 Feign 如何输出日志,可选:

  • 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.

FeignClient.fallback 正确的使用方法

配置的 fallback class 也必须在 FeignClient Configuration 中实例化,否则会报

java.lang.IllegalStateException: No fallback instance of type class异常。

例子:

    @FeignClient(name = "hello", fallback = HystrixClientFallback.class)    public interface HystrixClient {        @RequestMapping(method = RequestMethod.GET, value = "/hello")        Hello iFailSometimes();    }    public class HystrixClientFallback implements HystrixClient {        @Override        public Hello iFailSometimes() {            return new Hello("fallback");        }    }    @Configuration    public class FooConfiguration {        @Bean        @Scope("prototype")        public Feign.Builder feignBuilder() {            return Feign.builder();        }        @Bean        public HystrixClientFallback fb(){            return new HystrixClientFallback();        }     }
复制代码

使用 Feign Client 和 @RequestMapping 时,注意事项

当前工程中有和 Feign Client 中一样的 Endpoint 时,Feign Client 的类上不能用 @RequestMapping 注解否则,当前工程该 endpoint http 请求且使用 accpet 时会报 404.

有一个 Controller

@RestController@RequestMapping("/v1/card")public class IndexApi {
@PostMapping("balance") @ResponseBody public Info index() { Info.Builder builder = new Info.Builder(); builder.withDetail("x", 2); builder.withDetail("y", 2); return builder.build(); }}
复制代码


有一个 Feign Client

@FeignClient(        name = "card",        url = "http://localhost:7913",        fallback = CardFeignClientFallback.class,        configuration = FeignClientConfiguration.class)@RequestMapping(value = "/v1/card")public interface CardFeignClient {    @RequestMapping(value = "/balance", method = RequestMethod.POST,                                produces = MediaType.APPLICATION_JSON_VALUE)    Info info();}
复制代码

if @RequestMapping is used on class, when invoke http /v1/card/balance, like this :

如果 @RequestMapping 注解被用在 FeignClient 类上,当像如下代码请求/v1/card/balance 时,注意有 Accept header:

Content-Type:application/jsonAccept:application/jsonPOST http://localhost:7913/v1/card/balance
复制代码

那么会返回 404。

如果不包含 Accept header 时请求,则是 OK:

Content-Type:application/jsonPOST http://localhost:7913/v1/card/balance
复制代码

或者像下面不在 Feign Client 上使用 @RequestMapping 注解,请求也是 ok,无论是否包含 Accept:

@FeignClient(        name = "card",        url = "http://localhost:7913",        fallback = CardFeignClientFallback.class,        configuration = FeignClientConfiguration.class)public interface CardFeignClient {    @RequestMapping(value = "/v1/card/balance", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)    Info info();}
复制代码

https://github.com/Netflix/Hystrix/issues/1428

https://github.com/Netflix/Hystrix/wiki/Configuration#allowmaximumsizetodivergefromcoresize


用户头像

我们始于迷惘,终于更高的迷惘。 2020.03.25 加入

一个酷爱计算机技术、健身运动、悬疑推理的极客狂人,大力推荐安利Java官方文档:https://docs.oracle.com/javase/specs/index.html

评论

发布
暂无评论
SpringCloud技术专题-Hystrix以及FeginClient异常