我叫你不要重试,你非得重试。这下玩坏了吧? (下)

本文涉及的产品
应用型负载均衡 ALB,每月750个小时 15LCU
网络型负载均衡 NLB,每月750个小时 15LCU
传统型负载均衡 CLB,每月750个小时 15LCU
简介: 我叫你不要重试,你非得重试。这下玩坏了吧? (下)

HttpClient 的重试


在 HttpClients 里面,其实也是有重试的功能,且和 Dubbo 一样,默认是开启的。

但是我们这里为什么两种异常都没有进行重试呢?

如果它可以重试,那么默认重试几次呢?

我们带着疑问,还是去源码中找找答案。

答案就藏在这个源码中,org.apache.http.impl.client.DefaultHttpRequestRetryHandler

DefaultHttpRequestRetryHandler 是 Apache HttpClients 的默认重试策略。

从它的构造方法可以看出,其默认重试 3 次:


image.png

从该构造方法的注释和代码可以看出,对于这四类异常是不会进行重试的:

  • 一:InterruptedIOException
  • 二:UnknownHostException
  • 三:ConnectException
  • 四:SSLException

而我们前面说的 ConnectTimeoutException 和 SocketTimeOutException 都是继承自 InterruptedIOException 的:


image.png

可以看到,经过 if 判断,会返回 false ,则不会发起重试。

为了模拟重试的情况,我们就得改造一下 HttpPostUtils ,来一个自定义 HttpRequestRetryHandler:

public class HttpPostUtils {
    public static String retryPostJson(String uri) throws Exception {
        HttpRequestRetryHandler httpRequestRetryHandler = new HttpRequestRetryHandler() {
            @Override
            public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {
                System.out.println("开始第" + executionCount + "次重试!");
                if (executionCount > 3) {
                    System.out.println("重试次数大于3次,不再重试");
                    return false;
                }
                if (exception instanceof ConnectTimeoutException) {
                    System.out.println("连接超时,准备进行重新请求....");
                    return true;
                }
                HttpClientContext clientContext = HttpClientContext.adapt(context);
                HttpRequest request = clientContext.getRequest();
                boolean idempotent = !(request instanceof HttpEntityEnclosingRequest);
                if (idempotent) {
                    return true;
                }
                return false;
            }
        };
        HttpPost post = new HttpPost(uri);
        RequestConfig config = RequestConfig.custom()
                .setConnectTimeout(1000)
                .setConnectionRequestTimeout(1000)
                .setSocketTimeout(1000).build();
        post.setConfig(config);
        String responseContent = null;
        CloseableHttpResponse response = null;
        CloseableHttpClient client = null;
        try {
            client = HttpClients.custom().setRetryHandler(httpRequestRetryHandler).build();
            response = client.execute(post, HttpClientContext.create());
            if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
                responseContent = EntityUtils.toString(response.getEntity(), Consts.UTF_8.name());
            }
        } finally {
            if (response != null) {
                response.close();
            }
            if (client != null) {
                client.close();
            }
        }
        return responseContent;
    }
}

在我们的自定义 HttpRequestRetryHandler 里面,对于 ConnectTimeoutException ,我进行了放行,让请求可以重试。

当我们不启动 Controller 接口时,程序会自动重试 3 次:

image.png

上面给大家演示了 Apache HttpClients 的默认重试策略。上面的代码大家可以直接拿出来运行一下。

如果想知道整个调用流程,可以在 debug 的模式下看调用链路:

image.png

HttpClients 的自动重试,同样是默认开启的,但是我们在使用过程中是无感知的。

因为它的重试条件也是比较苛刻的,针对网络层面的重试,没有侵入到业务中。


谨慎谨慎再谨慎。


对于需要重试的功能,我们在开发过程中一定要谨慎谨慎再谨慎。

image.png

比如 Dubbo 的默认重试,我觉得它的出发点是为了保证服务的高可用。

正常来说我们的微服务至少都有两个节点。当其中一个节点不提供服务的时候,集群容错策略就会去自动重试另外一台。

但是对于服务调用超时的情况,Dubbo 也认为是需要重试的,这就相当于侵入到业务里面了。

前面我们说了服务调用超时是针对客户端的。即使客户端调用超时了,服务端还是在正常执行这次请求。

所以官方文档中才说“通常用于读操作”:

http://dubbo.apache.org/zh/docs/v2.7/user/examples/fault-tolerent-strategy/

image.png

读操作,含义是默认幂等。所以,当你的接口方法不是幂等时请记得设置 retries=0。

这个东西,我给你举一个实际的场景。

假设你去调用了微信支付接口,但是调用超时了。

这个时候你怎么办?

直接重试?请你回去等通知吧。

肯定是调用查询接口,判断当前这个请求对方是否收到了呀,从而进行进一步的操作吧。

对于 HttpClients,它的自动重试没有侵入到业务之中,而是在网络层面。

所以绝大部分情况下,我们系统对于它的自动重试是无感的。

甚至需要我们在程序里面去实现自动重试的功能。

由于你的改造是在最底层的 HttpClients 方法,这个时候你要注意的一个点:你要分辨出来,这个请求异常后是否支持重试。

不能直接无脑重试。

对于重试的框架,大家可以去了解一下 Guava-Retry 和 Spring-Retry。


奇闻异事



我知道大家最喜欢的就是这个环节了。

image.png

2020 年提交了两次。时间间隔还挺短的。

2 月 9 日的提交,是针对编号为 5686 的 issue 进行的修复。

image.png

而在这个 issue 里面,针对编号为 5684 和 5654 进行了修复:

https://github.com/apache/dubbo/issues/5654

image.png

它们都指向了一个问题:

多注册中心的负载均衡不生效。

官方对这个问题修复了之后,马上就带来另外一个大问题:

2.7.6 版本里面 failfast 负载均衡策略失效了。

你想,我知道我一个接口不能失败重试,所以我故意改成了 failfast 策略。

但是实际框架用的还是 failover,进行了重试 2 次?

微信图片_20220427211152.png2.7.6 版本里面 failfast 负载均衡策略失效了。

你想,我知道我一个接口不能失败重试,所以我故意改成了 failfast 策略。

但是实际框架用的还是 failover,进行了重试 2 次?

image.png

而实际情况更加糟糕, 2.7.6 版本里面负载均衡策略只支持 failover 了。

这玩意就有点坑了。


image.png

而这个 bug 一直到 2.7.8 版本才修复好。

所以,如果你使用的 Dubbo 版本是 2.7.5 或者 2.7.6 版本。一定要注意一下,是否用了其他的集群容错策略。如果用了,实际上是没有生效的。

可以说,这确实是一个比较大的 bug。

但是开源项目,共同维护。

我们当然知道 Dubbo 不是一个完美的框架,但我们也知道,它的背后有一群知道它不完美,但是仍然不言乏力、不言放弃的工程师。

他们在努力改造它,让它趋于完美。

我们作为使用者,我们少一点"吐槽",多一点鼓励,提出实质性的建议。

只有这样我才能骄傲的说,我们为开源世界贡献了一点点的力量,我们相信它的明天会更好。

向开源致敬,向开源工程师致敬。

总之,牛逼。

好了,这次的文章就到这里了。

才疏学浅,难免会有纰漏,如果你发现了错误的地方,可以提出来,我对其加以修改。

感谢您的阅读,我坚持原创,十分欢迎并感谢您的关注。

我是 why,一个被代码耽误的文学创作者,一个又暖又有料的四川好男人。

还有,欢迎关注我呀。


相关实践学习
SLB负载均衡实践
本场景通过使用阿里云负载均衡 SLB 以及对负载均衡 SLB 后端服务器 ECS 的权重进行修改,快速解决服务器响应速度慢的问题
负载均衡入门与产品使用指南
负载均衡(Server Load Balancer)是对多台云服务器进行流量分发的负载均衡服务,可以通过流量分发扩展应用系统对外的服务能力,通过消除单点故障提升应用系统的可用性。 本课程主要介绍负载均衡的相关技术以及阿里云负载均衡产品的使用方法。
目录
相关文章
|
前端开发 安全 程序员
测试大姐趁我下班点又提了个bug!前端你咋多个options请求?
测试大姐趁我下班点又提了个bug!前端你咋多个options请求?
88 0
|
Cloud Native Go
如何处理面试拒绝:失败并不是终点
如何处理面试拒绝:失败并不是终点
87 0
|
数据采集 Python
python爬虫怎么解决超时timeou错误
python爬虫怎么解决超时timeou错误
|
测试技术 内存技术
|
设计模式 Kubernetes 算法
leader说用下httpclient的重试,但我没用,因为我有更好的方案。
leader说用下httpclient的重试,但我没用,因为我有更好的方案。
195 0
|
SQL 运维 监控
一个诡异的MySQL查询超时问题,居然隐藏着存在了两年的BUG
一个诡异的MySQL查询超时问题,居然隐藏着存在了两年的BUG
195 0
|
JSON Java 程序员
写了这么久的业务连异常都不知道怎么处理吗
前言 文本已收录至我的GitHub仓库,欢迎Star:github.com/bin39232820… 种一棵树最好的时间是十年前,其次是现在
220 0
|
负载均衡 Dubbo 应用服务中间件
我叫你不要重试,你非得重试。这下玩坏了吧? (中)
我叫你不要重试,你非得重试。这下玩坏了吧? (中)
97 0
我叫你不要重试,你非得重试。这下玩坏了吧? (中)
|
XML Dubbo 应用服务中间件
我叫你不要重试,你非得重试。这下玩坏了吧? (上)
我叫你不要重试,你非得重试。这下玩坏了吧? (上)
120 0
我叫你不要重试,你非得重试。这下玩坏了吧? (上)
|
Java Spring 容器
从0到1带你手撸一个请求重试组件,不信你学不会!
从0到1带你手撸一个请求重试组件,不信你学不会!
从0到1带你手撸一个请求重试组件,不信你学不会!