强哥叨逼叨

V1

2022/03/16阅读:15主题:萌绿

如何实现Feign超过重试次数后的告警操作

哈喽,大家好,我是强哥。

今天同事问我,使用Feign进行Http请求,当出现网络问题进行重试,假如超过了重试次数后想要发起一个告警要怎么做?

强哥被问到的时候,也突然懵了一下,之前使用Feign配置Retryer的时候,都是使用Feign的默认实现Retryer.Default,配置好重试次数和时间之后,就不管了。也没遇到要处理超过重试次数如何发起告警的问题。

那么,针对这个问题我们要怎么解决呢?

简单的在网上找了下,呵呵,全是关于怎么配置重试次数的,重试失败后的额外操作一个也没有多说。

没法,看着同事含情脉脉的眼神,不给她解决下有点不好意思。那要怎么搞的?看源码呗。

Feign触发请求调用的核心代码在SynchronousMethodHandler下的invoke方法:

  @Override
  public Object invoke(Object[] argv) throws Throwable {
    RequestTemplate template = buildTemplateFromArgs.create(argv);
    Retryer retryer = this.retryer.clone();
    while (true) {
      try {
        return executeAndDecode(template);
      } catch (RetryableException e) {
        retryer.continueOrPropagate(e);
        if (logLevel != Logger.Level.NONE) {
          logger.logRetry(metadata.configKey(), logLevel);
        }
        continue;
      }
    }
  }

这里我们可以看到,executeAndDecode方法是核心,进去看看(代码有点长,强哥这里就挑有用的展示):

 Object executeAndDecode(RequestTemplate template) throws Throwable {
    //根据Feign配置的请求拦截器进行请求构建
    Request request = targetRequest(template);

    Response response;
    try {
      //发送请求获取结果
      response = client.execute(request, options);
    } catch (IOException e) {
      //网络异常走这里
      if (logLevel != Logger.Level.NONE) {
        logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));
      }
      //执行Retryer
      throw errorExecuting(request, e);
    }

    boolean shouldClose = true;
    try {
      //省略部分代码
      ……
      
      //调用接口后返回结果
      if (response.status() >= 200 && response.status() < 300) {
        if (void.class == metadata.returnType()) {
          return null;
        } else {
          return decode(response);
        }
      } else if (decode404 && response.status() == 404 && void.class != metadata.returnType()) {
        return decode(response);
      } else {
        throw errorDecoder.decode(metadata.configKey(), response);
      }
    } catch (IOException e) {
      if (logLevel != Logger.Level.NONE) {
        logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime);
      }
      throw errorReading(request, response, e);
    } finally {
      if (shouldClose) {
        ensureClosed(response.body());
      }
    }
  }
  

先讲讲上面代码的执行流程:

  • 拼接Request
  • 发起请求,获取结果
  • 异常判断:1、如果是网络问题,则进入重试;2、如果发起请求后有获取到响应码,则根据响应码进行对应的处理。

那么,Feign什么时候请求失败会走Retryer呢?没错,就是throw errorExecuting(request, e);这句代码:

static FeignException errorExecuting(Request request, IOException cause) {
    return new RetryableException(
        format("%s executing %s %s", cause.getMessage(), request.method(), request.url()), cause,
        null);
  }

在抛出RetryableException异常后,有细看代码的小伙伴应该发现了,这个异常会被最开头的invoke方法捕获,然后通过retryer.continueOrPropagate(e);进入Retryer的continueOrPropagate方法。

那么重点就在continueOrPropagate方法里了(这里直接给出Retryer.Default的实现代码)。

    public void continueOrPropagate(RetryableException e) {
      if (attempt++ >= maxAttempts) {
        throw e;
      }

      long interval;
      if (e.retryAfter() != null) {
        interval = e.retryAfter().getTime() - currentTimeMillis();
        if (interval > maxPeriod) {
          interval = maxPeriod;
        }
        if (interval < 0) {
          return;
        }
      } else {
        interval = nextMaxInterval();
      }
      try {
        Thread.sleep(interval);
      } catch (InterruptedException ignored) {
        Thread.currentThread().interrupt();
      }
      sleptForMillis += interval;
    }

代码逻辑很简单,就是判断当前重试次数是否大于最大重试次数,不是就等待一会然后再到最开始贴出的invoke方法的循环里再次发起请求(invoke方法里有一个while(true)的循环来重复发起请求);如果超过了重试次数,就直接抛异常了:

if (attempt++ >= maxAttempts) {
    throw e;
}

哈哈,那么重点也就是这个超过重试次数,要抛出异常的地方了。从上面的代码可以看出,抛出的是RetryableException类型的异常。

也就意味着,如果Feign在发起请求后,重试次数达到了最大重试次数还是失败的话,就会抛出RetryableException异常。

这里强哥重点强调是为了让小伙伴们明白:我们其实只要在自己的业务代码使用Feign发起请求的地方,前后添加上try catch相关的代码捕获这个异常就可以了。

给出一个强哥的解决方式:

try {
    //发起Feign请求
    Object feignResult = feignService.getUserId(json);
}catch (HystrixRuntimeException e) {
    if (e.getCause() instanceof RetryableException) {
        //告警代码
        weChatNoticeUtil.sendWxNoticeMsg("请求重试出错啦,看看是不是服务再重启或者断网咯");
    }else {
        log.warn("正常访问出错,看看是不是服务地址变更啦", e);
    }
}

捕获异常后在catch中进行对应的告警操作就可以啦。

这里catch捕获的是HystrixRuntimeException类型的异常,且在catch的处理代码中,又对请求异常的类型进行了判断,这是为什么呢?

对请求类型的判断是因为:前面有说过,并不是所有的请求都会走Retryer发起重试,如果请求能正常发起,并获取到返回码不管成功失败都是不会走Retryer的,比如请求404错误的话就不会走重试机制。

一般都是网络有问题才会走Retryer。而从上面源码的分析我们可以看出Retryer的报错类型是RetryableException,所以专门针对它进行了特殊处理。

至于catch捕获的是HystrixRuntimeException类型,其实是框架对应实现抛出来的,具体怎么知道是HystrixRuntimeException。其实只要先进行try catch(Exception e)来捕获异常,在异常捕获得地方打上断点就能知道具体是什么类型的异常啦:

好啦,所以整个问题,其实用一个try catch就解决啦。

OK,今天就水到这里。对于我们遇到的陌生的问题,其实,如果网上找不到答案,最快的办法就是自己打断点走源码来获取解决办法啦。

分类:

后端

标签:

后端

作者介绍

强哥叨逼叨
V1

欢迎关注公众号:强哥叨逼叨