20 Sep 2017

Feign Fallback 线上问题

今在线上遇到了一个比较奇怪的问题, 现象是:

两个服务P, C分别注册到Eureka中, P为服务提供方, C为调用方, 上线升级完成后, C调用P某个接口失败, 并且没有异常日志, 也没有经过ErrorDecoder, 而是进入Fallback方法中, 调查过程如下:

首先, 我直接调用了P提供接口, 发现返回结果正常, 说明不是P服务的问题.

接下来, 继续查看同事的代码, 看看Fallback以及日志切面中是否存在有价值的信息, 可惜Fallback只是简单的记录了调用失败.

然后, 我通过单独启动一个P服务, 将register-with-eureka配置为false, 使其不会被gateway以及其他服务发现, 从而达到不影响系统整体调用的效果, 有了这样一个服务, 本地就可以进行debug了, 经过简单的调试发现, 问题出在接口返回值的转型上, 由于POM文件中依赖版本问题, 生产环境jar中少了一个DTO的类, 而出问题的接口的DTO中恰好持有了一个该类的集合引用: List<SomeDTO>, 导致DecodeException.

将问题修复后, 我又继续查看了为什么没有经过ErrorDecoder的原因:

feign调用时, 会经过一个核心方法SynchronousMethodHandler.executeAndDecode, 在此方法中, 会执行feignClient的请求, 并进行结果的转换, 在本文的bug情况里, feignClient的请求的返回是正常的, status200, 因此并不会触发errorDecoder.decode, 而我们的异常则会在decode(response)是被抛出:

Object executeAndDecode(RequestTemplate template) throws Throwable {
  /*
    ...
  */
  try {
    response = client.execute(request, options);
    // ensure the request is set. TODO: remove in Feign 10
    response.toBuilder().request(request).build();
  } catch (IOException e) {
    /*
      ...
    */
  }
  /*
    ...
  */
  try {
    /*
      ...
    */
    // 本文的情况, 会进入第一个分支, 而不是第三个(errorDecoder的处理)
    if (response.status() >= 200 && response.status() < 300) {
      if (void.class == metadata.returnType()) {
        return null;
      } else {
        return decode(response);
      }
    } else if (decode404 && response.status() == 404) {
      return decoder.decode(response, metadata.returnType());
    } else {
      throw errorDecoder.decode(metadata.configKey(), response);
    }
  }
  /*
    ...
  */
}

这种情况, 单纯的在fallback方法里是没有办法看到异常原因的, 因为fallback方法内并没有被动态注入任何异常参数, 经过对spring-cloud-netflix的查看, 发现这个问题之前已经有人提出过, 并且已经解决, 原理是支持了fallbackFactory的自定义配置, 因为在HystrixClient创建时, 我们是可以拿到异常信息的:

@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 HystrixClientWithFallBackFactory() {
      @Override
      public Hello iFailSometimes() {
        return new Hello("fallback; reason was: " + cause.getMessage());
      }
    };
  }
}

Tags:
0 comments