图片中解释了Apache Http客户端和异步HTTP客户端超时

I recently had to introduce a colleague to the wonderful and exciting world of timeouts in Apache HttpClient. As the usual explanation that "the connection timeout is the maximum time to establish a connection to the server" is not the most descriptive one, let's try to explain with a couple of pictures what each timeout actually means.

即使我们将讨论Apache的HttpClient,以下说明对于任何基于TCP的通信(包括大多数JDBC驱动程序)也很有用。

作为参考,这是您的所有超时必须配置是否要健康的生产服务:

  • 连接超时读取超时

如果使用微服务,则还需要配置连接池和以下超时:

  • 连接池超时连接池生存时间(TTL)

You will find here how to configure these timeout outs in Java. In our examples we will use clj-http which is a simple wrapper over Apache’s HttpClient. We will also compare how timeouts work in Asynchronous HTTP Client.

All the code, including a docker compose environment to test the settings can be found at https://github.com/dlebrero/apache-httpclient-timeouts.

Connection timeout

在HTTP客户端开始与服务器交换信息之前,必须在两者之间建立通信路径(或道路或管道)。

这是通过握手完成的:

phone handshake

交换之后,您和您的伴侣可以开始对话,即交换数据。

用TCP术语来说,这称为三向握手:

TCP handshake

连接超时控制您愿意多长时间进行一次握手。

让我们使用不可路由的IP地址对其进行测试:

;; Without connection timeout
(time
    (try
      (client/get "http://10.255.255.1:22220/")
      (catch Exception e)))
"Elapsed time: 75194.7148 msecs"

;; With connection timeout
(time
    (try
      (client/get "http://10.255.255.1:22220/"
        {:connection-timeout 2000})
      (catch Exception e
        (log/info (.getClass e) ":" (.getMessage e)))))
"Elapsed time: 2021.1883 msecs"
INFO  a.http-client - java.net.SocketTimeoutException : connect timed out

请注意不同的经过时间,打印的异常以及异常中的消息。

Read timeout

建立连接后,并且您正在与服务器愉快地交谈,您可以使用读取超时来指定愿意等待多长时间从服务器收到回音:

Apache Http Client read timeout

Let’s test it, using this time around an Nginx server, with a Toxiproxy in the middle to mess around with the response times:


;; With no socket timeout
(time
  (try
    (client/get "http://local.toxiproxy:22220/")
    (catch Exception e (.printStackTrace e))))
"Elapsed time: 240146.6273 msecs"

;; Same url, with socket timeout
(time
    (try
      (client/get "http://local.toxiproxy:22220/"
        {:socket-timeout 2000})
      (catch Exception e
        (log/info (.getClass e) ":" (.getMessage e)))))
"Elapsed time: 2017.7835 msecs"
INFO  a.http-client - java.net.SocketTimeoutException : Read timed out

请注意,默认套接字超时取决于系统。 请注意不同的经过时间,打印的异常以及异常中的消息。

The ToxiProxy configuration can be found here.

Pub quiz

有了这两个超时,您应该在下一次IT Pub测验锦标赛中轻松为您的团队得分:

如果将HTTP客户端配置为具有10秒的连接超时和1秒的读取超时,那么在最坏的情况下,发出HTTP请求后线程将被阻塞多长时间?

您猜对了! 无穷! 为您的团队加分!

? 您没有无限回答吗? 很明显(讽刺)。

让我们再次给您的一个朋友打电话,问他有关Pi的问题,但是这次我们将给那些高精度的smartass朋友之一打电话:

Apache http client read timeout retry

到底是怎么回事?

If you read carefully the previous explanation about the read timeout or even better, the javadoc about it you will notice that the read timeout is reset each time we hear from the server, so if the response is too big, the connection is too slow, the server is choking, or anything between the client and the server is having trouble, your client thread will be there hanging for a very long time.

让我们来看看它的实际效果。 首先,我们在代理Nginx响应时将Toxiproxy配置为非常慢(每秒2个字节):

(client/post "http://local.toxiproxy:8474/proxies/proxied.nginx/toxics"
    {:form-params {:attributes {:delay 1000000
                                :size_variation 1
                                :average_size 2}
                   :toxicity 1.0
                   :stream "downstream"
                   :type "slicer"}
     :content-type :json})

现在,我们发出与以前完全相同的请求,两秒钟超时:

(time
    (try
      (client/get "http://local.toxiproxy:22220/"
        {:socket-timeout 2000})
      (catch Exception e
        (log/info (.getClass e) ":" (.getMessage e)))))
"Elapsed time: 310611.8366 msecs"

超过五分钟! 幸运的是,它只有600个字节。

只是读取标头的第一个字节,这就是HttpClient日志的样子:

Apache http client slow logs

看起来很慢。 当然,这将永远不会发生在您身上(更多嘲讽在这里)。

我们将在底部看到如何避免此问题。

Connection Pool

在讨论什么是连接池超时之前,让我们看一下使用示例创建连接池的意义所在。

假设有两个对Mordor Stocks特别感兴趣的证券交易员(代码:M $)。 两者都在观看同一个新闻频道,但是一个正在使用连接池(一个在右侧),而另一个不在使用:

Why use a http client connection pool

如您所见,具有连接池的交易者将电话置于电话旁,经纪人等待更多订单。

When, quite unexpectedly, a one metre humanoid manages to travel 2900 km across several war zones and inhospitable areas, and deliver the only existing nuke to the only existing weak spot of Sauron, the trader can very quickly sell all of his Mordor Stocks, while the trader without the connection pool is doomed.

因此,如果您要多次调用同一服务器(这是微服务架构所特有的),则您将希望避免创建与服务器的新连接的开销,因为这可能是一个非常昂贵的操作(从几毫秒到几百美元不等) 的毫秒数)。

This is especially true if you are using HTTPS. See the ŤLS handshake.

Connection pool timeout and TTL

与任何其他资源一样,连接池非常棒,您需要限制要维护的打开连接的最大数量,这意味着从池中获取连接时可能存在三种情况。

小号ide note: for a very good talk about how to size your connection pool see "Stop Rate Limiting! Capacity Management Done Right" by Jon Moore.

Scenario 1. Free connections.

假定最大连接池为三个,则第一种情况是:

HTTP Connection pool new connection

因此,有一些电话可用,但很麻烦。 您将需要承受额外的连接建立延迟。

Scenario 2. Connection pooled.

第二种情况:

HTTP Connection pool connection available

有一部电话可以随时使用。 在这种情况下,还有另外两种情况:

  1. 连接是新的,创建的次数少于配置的TTL。 您无需承受额外的连接建立延迟。连接陈旧,创建的次数超过配置的TTL。 您将需要承受额外的连接建立延迟。

让我们测试一下:

;; Create a new connection pool, with a TTL of one second:
(def cp (conn-manager/make-reusable-conn-manager
        {:timeout 1 ; in seconds. This is called TimeToLive in PoolingHttpClientConnectionManager
         }))
;; Make ten calls, two per second:
(dotimes [_ 10]
  (log/info "Send Http request")
  (client/get "http://local.nginx/" {:connection-manager cp})
  (Thread/sleep 500))

查看日志:

16:56:24.905 INFO  - Send Http request
16:56:24.914 DEBUG - Connection established 172.24.0.4:51984<->172.24.0.2:80
16:56:25.416 INFO  - Send Http request
16:56:25.926 INFO  - Send Http request
16:56:25.933 DEBUG - Connection established 172.24.0.4:51986<->172.24.0.2:80
16:56:26.434 INFO  - Send Http request
16:56:26.942 INFO  - Send Http request
16:56:26.950 DEBUG - Connection established 172.24.0.4:51988<->172.24.0.2:80
16:56:27.452 INFO  - Send Http request
16:56:27.960 INFO  - Send Http request
16:56:27.967 DEBUG - Connection established 172.24.0.4:51990<->172.24.0.2:80
16:56:28.468 INFO  - Send Http request

如预期的那样,我们可以在重新创建连接之前发出两个请求。

相同的情况,但TTL为20秒:

16:59:19.562 INFO  - Send Http request
16:59:19.570 DEBUG - Connection established 172.24.0.4:51998<->172.24.0.2:80
16:59:20.073 INFO  - Send Http request
16:59:20.580 INFO  - Send Http request
16:59:21.086 INFO  - Send Http request
16:59:21.593 INFO  - Send Http request
16:59:22.100 INFO  - Send Http request
16:59:22.607 INFO  - Send Http request
16:59:23.114 INFO  - Send Http request
16:59:23.623 INFO  - Send Http request
16:59:24.134 INFO  - Send Http request

因此,所有请求都使用相同的连接。

但是为什么我们需要TTL? 通常是因为防火墙倾向于在不告知任何涉及的部分的情况下放弃长期连接(尤其是空闲连接),这导致客户端花了一些时间才意识到该连接不再可用。

Scenario 3. All connections in use.

最后一种情况:

HTTP Connection pool full

所有电话都忙,所以您必须等待。 连接池超时是您愿意等待多少电话空闲时间。

请注意,如果电话在连接池超时之前变得可用,那么您将回到第二种情况。 在一些不幸的时机,您还需要建立一个新的新连接。

Let's look at an example. First we make the Nginx very slow, taking up to 20 seconds to respond.

然后,我们创建一个最多包含三个连接的连接池,并发送四个HTTP请求:

  (def cp-3 (conn-manager/make-reusable-conn-manager
              {:timeout 100
               :threads 3           ;; Max connections in the pool.
               :default-per-route 3 ;; Max connections per route (~ max connection to a server)
               }))

  (dotimes [_ 4]
    (future
      (time
        (client/get "http://local.toxiproxy:22220/" {:connection-manager cp-3}))))

"Elapsed time: 20017.1325 msecs"
"Elapsed time: 20016.9246 msecs"
"Elapsed time: 20020.9474 msecs"
"Elapsed time: 40024.5604 msecs"

如您所见,最后一个请求花费40秒,其中20个等待连接可用。

添加一秒钟的连接池超时:

(dotimes [_ 4]
  (future
    (time
      (try
        (client/get "http://local.toxiproxy:22220/"
          {:connection-manager cp-3
           :connection-request-timeout 1000 ;; Connection pool timeout in millis
           })
        (catch Exception e
          (log/info (.getClass e) ":" (.getMessage e)))))))

"Elapsed time: 1012.2696 msecs"
"2019-12-08 08:59:04.073 INFO  - org.apache.http.conn.ConnectionPoolTimeoutException : Timeout waiting for connection from pool"
"Elapsed time: 20014.1366 msecs"
"Elapsed time: 20015.3828 msecs"
"Elapsed time: 20015.962 msecs"

一秒钟后,无法从池中获得连接的线程放弃,并抛出ConnectionPoolTimeoutException。

Are we done yet?

不幸的是,即使最需要调整的是连接超时,读取超时,连接池超时和连接池TTL,您也应该意识到:

  • DNS resolution: it cannot be explicitly configure it in Java, system dependant. Good to also know how it is cached.
  • Hosts with multiple IPs: In case of a connection timeout, HTTP client will try to each of them.
  • TIME_WAIT and SO_LINGER: closing a connection is not immediate and under very high load it can cause issues.

All together!

综合所有超时,我们有:

All Apache HTTP client timeouts

在所有这些超时中,要知道一个HTTP请求实际要花费多长时间是一个很大的挑战,因此,如果您有任何SLA或担​​心应用程序的稳定性,则不能仅仅依靠正确设置超时。

If you want to setup just some simple timeout for the whole request, you should be using Hystrix Thread 一世solation, Apache HTTP Client's FutureRequestExecutionService (never used this one myself) or maybe use a different HTTP client.

Asynchronous HTTP Client

A possible solution to all these timeouts juggling is to use 一种synchronous HTTP Client, which is based on Netty. You can see here all the above scenarios but using the Asynchronous HTTP Client.

两个HTTP客户端之间的一些显着差异:

  1. Asynchronous HTTP clients have their own thread pool to handle the response once it arrives.
  2. No connection pool timeout: if the pool is completely used, an exception is thrown. There is no waiting for a connection to be available. Interestedly, I usually configure my Apache HTTP connection pools to behave the same, as a full connection pool usually means that something is not going working and it is better to bail out early.
  3. Connection pool idle timeout: as we mentioned before, we wanted a connection pool TTL mostly because idle connections. Asynchronous HTTP Client comes with an explicit idle timeout, on top of a TTL timeout.
  4. A new request timeout: a timeout to bound the amount of time it takes to do the DNS lookup, the connection and read the whole response. One single timeout that states how long you are willing to wait for the whole HTTP conversation to be done. Sweet.

因此,异步HTTP客户端的超时如下所示:

Asynchronous HTTP client timeouts

You can see again all the same scenarios but using this new request timeout here, including the Pub Quiz one.

关于最坏情况的推理要容易得多。

Summary

总之,如果您想对分配给HTTP请求/响应的最大时间进行一些控制,则很难配置超时。 除非您使用的是异步HTTP客户端(或其他异步客户端)。

我是否建议您不要使用Apache HTTP Client?

Well, it depends what functionality you are using. Apache HTTP Client is a very mature project with plenty of build-in functionality and hooks to customize it. It even has an async module and the newer 5.0 (beta) version comes with build-in async functionality.

在我们的案例中,在对我的同事进行了长时间的解释之后,考虑到我们的用例,我建议迁移到异步HTTP客户端。

from: https://dev.to//danlebrero/apache-http-client-and-asynchronous-http-client-timeouts-explained-in-pictures-15jo

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
一个工作时写的工具包。实现了Java版的Promise 和 HttpClientHttpClient 支持同步和异步两种方式,也支持多种不同实现。目前有Netty 和 Apache Compoenet两种实现。本次上传移除了Netty实现。主要解决生产环境同步httpclient造成的IO阻塞问题。同步http请求将导致 tomcat 的业务线程被阻塞。一旦某接口网络出现问题,可能会阻塞tomcat业务线程,从而无法处理正常业务。很多公司使用另开线程池的方式进行异步调用来解决tomcat线程阻塞问题。但由于本系统接口网络太不稳定,使用线程池也将导致线程池的线程不断加大,不管使用怎样的线程池策略,最终要么线程池线程全部挂起,要么部分任务被延迟执行,要么丢失部分任务。这在我们的系统仍然不能接受。因此才有了这个组件的开发。该组件是单线程非阻塞式的,类似于JS的ajax请求。都使用单线程异步回调的方式。目前该组件已经初步测试通过。如果大家也需要这样的组件,可以下载尝试一下。所有关键注释都已经写了,如有不明白可以发送邮件 ath.qu.trues@gmail.com 代码分为3个maven模块。 commons-ext : 实现Promise commons-tools: 实现 异步httpclient commons-parent:父模块 测试代码在 commons-tools/src/test/java/HttpTest.java . 要求至少Java 8 版本。 注释已经写好。这里贴出异步http部分测试代码。 /** * 异步方法的Fluent写法 */ public void testAsyncHttpFluent() { SimpleRequest.Get("http://www.baidu.com") .header("h1", "hv1") .header("h2", "hv2") .parameter("p1", "pv1") .parameter("p2", "pv2") .chartUTF8() .build() .asyncExecute() .then(SimpleAsyncHttpClient::asString) .then(html -> { System.out.println(html); }) .catching(Throwable::printStackTrace);//如果有异常,则打印异常 }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值