崔世宁
16年毕业加入 Qunar 金融事业部,两年过去了,仍然是一只萌新。
一、背景
由于工作上的业务本人经常与第三方系统交互,所以经常会使用 HttpClient 与第三方进行通信。对于交易类的接口,订单状态是至关重要的。
这就牵扯到一系列问题:
HttpClient 是否有默认的重试策略?重试策略原理?如何禁止重试?
接下来,本文将从源码中探讨这些问题。
源码下载地址:
二、一般使用方法
一般而言,获得 HttpClient 实例的方法有两种:
第一种方法用来定制一些 HttpClient 的属性,比如 https 证书,代理服务器, http 过滤器,连接池管理器等自定义的用法。
第二种方法用来获得一个默认的 HttpClient 实例。
这两种方法获得都是 CloseableHttpClient 实例,且都是通过 HttpClientBuilder 的 build() 构建的。
可以看到,上面的两种用法最终都得到了一个 InternalHttpClient ,是抽象类 CloseableHttpClient 的一种实现。
这里有很多配置化参数,这里我们重点关注一下 execChain 这个执行链。
可以看到执行链有多种实现,比如:
RedirectExec 执行器的默认策略是,在接收到重定向错误码301与307时会继续访问重定向的地址;
以及我们关注的 RetryExec 可以重试的执行器。这么多执行器,是怎么用到了重试执行器呢?
可以看到在 build() httpclient 实例的时候,判断了是否关闭了自动重试,这个 AutomaticRetriesDisabled 类型是 boolean ,默认值是 false ,所以 if 这里是满足的。
即如果没有指定执行链,就是用 RetryExec 执行器,默认的重试策略是 DefaultHttpRequestRetryHandler 。
前面已经看到我们使用的 HttiClient 本质上是 InternalHttpClient ,这里看下他的执行发送数据的方法。
最后一行可以看到,最终的执行 execute 方式使用的是 execChain 的执行方法,而 execChain 是通过 InternalHttpClient 构造器传进来的,就是上面看到的 RetryExec 。
所以, HttpClient 有默认的执行器 RetryExec ,其默认的重试策略是 DefaultHttpRequestRetryHandler 。
四、重试策略分析
4.1 是否需要重试的判断在哪里?
http 请求是执行器执行的,所以先看 RetryExec 发送请求的部分。
关于 RetryExec 执行器的执行过程,做一个阶段小结:
RetryExec 在执行 http 请求的时候使用的是底层的基础代码 MainClientExec ,并记录了发送次数;
当发生 IOException 的时候,判断是否要重试;
首先是根据重试策略 DefaultHttpRequestRetryHandler 判断,如果可以重试就继续;
判断当前 request 是否还可以再次发起;
如果重试策略判断不可以重试了,就抛相应异常并退出。
4.2 DefaultHttpRequestRetryHandler 的重试策略
在上文我们看到了默认的重试策略是 DefaultHttpRequestRetryHandler.INSTANCE 。
通过构造器可以看到,默认的重试策略是:
重试3次;
如果请求被成功发送过,就不再重试了;
InterruptedIOException、UnknownHostException、ConnectException、SSLException ,发生这4中异常不重试。
说句题外话,这是一个单例模式,属于饿汉模式。 饿汉模式的缺点是,这个类在被加载的时候就会初始化这个对象,对内存有占用。不过这个对象维护的 filed 比较小,所以对内存的影响不大。 另外由于这个类所有的 field 都是 final 的,所以是一个不可变的对象,是线程安全的。
关于默认的重试策略,做一个阶段小结:
如果重试超过3次,则不再重试;
几种特殊异常及其子类,不进行重试;
同一个请求在异步任务重已经被终止,则不进行重试;
幂等的方法可以进行重试,比如 Get ;
如果请求没有发送成功,可以进行重试。
那么关键问题来了,如何判断请求是否已经发送成功了呢?
可看到如果当前的 httpContext 中的 http.request_sent 属性为 true ,则认为已经发送成功,否则认为还没有发送成功。
那么就剩下一个问题了,一次正常的 http 请求中 http.request_sent 属性是如果设置的?
上面有提到过, RetryExec 在底层通信使用了 MainClientExec ,而 MainCLientExec 底层调用了 HttpRequestExecutor.doSendRequest()
上面是一个完成的 http 通信部分,步骤如下:
开始前将 http.request_sent 置为 false ;
通过流 flush 数据到服务端;
然后将 http.request_sent 置为 true 。
显然,对于 conn.flush() 这一步是会发生异常的,这种情况下就认为没有发送成功。
说句题外话,上面对 coon 的操作都是基于连接池的,每次都是从池中拿到一个可用连接。
五、重试策略对业务的影响
5.1 我们的业务重试了吗?
对于我们的场景应用中的 get 与 post ,可以总结为:
只有发生IOExecetion时才会发生重试;
InterruptedIOException、UnknownHostException、ConnectException、SSLException ,发生这4中异常不重试;
get 方法可以重试3次, post 方法在 socket 对应的输出流没有被 write 并 flush 成功时可以重试3次。
首先分析下不重试的异常:
InterruptedIOException ,线程中断异常;
UnknownHostException ,找不到对应 host ;
ConnectException ,找到了 host 但是建立连接失败;
SSLException , https 认证异常。
另外,我们还经常会提到两种超时,连接超时与读超时:
java.net.SocketTimeoutException: Read timed out;
java.net.SocketTimeoutException: connect timed out 这两种超时都是 SocketTimeoutException ,继承自 InterruptedIOException ,属于上面的第1种线程中断异常,不会进行重试。
5.2 哪些场景会进行重试?
对于大多数系统而言,很多交互都是通过 post 的方式与第三方交互的。
所以,我们需要知道有哪些情况 HttpClient 给我们进行了默认重试。
我们关心的场景转化为, post 请求在输出流进行 write 与 flush 的时候,会发生哪些除了 InterruptedIOException、UnknownHostException、ConnectException、SSLException 以外的 IOExecetion 。
可能出问题的一步在于 HttpClientConnection.flush() 的一步,跟进去可以得知其操作的对象是一个 SocketOutputStream ,而这个类的 flush 是空实现,所以只需要看 wirte 方法即可。
可以看到,这个方法会抛出 IOExecption ,代码中对 SocketException 异常进行了加工。从之前的分析中可以得知, SocketException 是不在可以忽略的范围内的。
所以从上面代码上就可以分析得出对于传输过程中 socket 被重置或者关闭的时候, httpclient 会对 post 请求进行重试。
以及一些其他的 IOExecption 也会进行重试,不过范围过广不好定位。
六、如何禁止重试?
回到 HttpClientBuilder 中,其 build() 方法中之所以选择了 RetryExec 执行器是有前置条件的,即没有手动禁止。
所以我们在构建 HttpClient 实例的时候手动禁止掉即可。
七、本文总结
通过本文分析,可以得知 HttpClient 默认是有重试机制的,其重试策略是:
只有发生 IOExecetion 时才会发生重试;
InterruptedIOException、UnknownHostException、ConnectException、SSLException ,发生这4中异常不重试;
get 方法可以重试3次, post 方法在 socket 对应的输出流没有被 write 并 flush 成功时可以重试3次;
读/写超时不进行重试;
socket 传输中被重置或关闭会进行重试;
以及一些其他的 IOException ,暂时分析不出来。
领取专属 10元无门槛券
私享最新 技术干货