哈喽,大家好,我是强哥。
今天同事问我,使用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,今天就水到这里。对于我们遇到的陌生的问题,其实,如果网上找不到答案,最快的办法就是自己打断点走源码来获取解决办法啦。