<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          記一次Apache HttpClient使用不當導致的請求超時問題分享排查過程

          共 27168字,需瀏覽 55分鐘

           ·

          2023-10-31 22:24

          你知道的越多,不知道的就越多,業(yè)余的像一棵小草!

          你來,我們一起精進!你不來,我和你的競爭對手一起精進!

          編輯:業(yè)余草

          來源:juejin.cn/post/7124317267407405070

          推薦:https://t.zsxq.com/135BocxbM

          自律才能自由

          記一次Apache HttpClient使用不當導致的請求超時問題分享排查過程

          背景

          近期負責的線上應用出現(xiàn)調(diào)用失敗的問題,排查后發(fā)現(xiàn)是 HttpClient 相關的問題,特此記錄一下。

          因為涉及線上數(shù)據(jù)安全隱私,模擬生產(chǎn)的應用寫了 demo,模擬一定的并發(fā)請求復現(xiàn)該問題。

          問題介紹

          收到線上告警通知:外部服務調(diào)用某應用 A 大量報錯,平均響應時常很長,經(jīng)常有請求超時的情況。

          問題排查

          查連接情況

          外部請求是 http 請求,所以可以查一下連接的情況。對應 java 進程的連接數(shù):

          具體的連接情況:

          [root@sky ~]# netstat -tanp | grep 8433 | grep 8080 | wc -l
          400

          應用的 8080 端口有 400 個連接。

          查線程情況

          平均響應時常比較長,一般是線程長時間在等待某個步驟完成。查一下應用的線程執(zhí)行情況:

          執(zhí)行命令 jstack 8433 | less

          image-20220731111745397

          jstack 的日志中有很多類似上面圖中的線程,處于 TIME_WAITING狀態(tài),而下面的堆棧中,HttpClientUtils.httpGet()是使用 Apache HttpClient 工具封裝的 http 請求函數(shù),PoolHttpClientConnectionManager.leaseConnection()看上去是在等待獲取連接。大概分析得出:有很多線程在執(zhí)行過程中,等待HttpClient獲取連接。

          查應用異常日志

          查一下應用的日志,尋找線索,在日志中看到許多下面的異常日志:

          2022-07-24 21:47:44.765 ERROR 82381 --- [pool-2-thread-7] com.skyme.TestController                 : error:

          org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool
           at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.leaseConnection(PoolingHttpClientConnectionManager.java:286) ~[httpclient-4.5.2.jar:4.5.2]
           at org.apache.http.impl.conn.PoolingHttpClientConnectionManager$1.get(PoolingHttpClientConnectionManager.java:263) ~[httpclient-4.5.2.jar:4.5.2]
           at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:190) ~[httpclient-4.5.2.jar:4.5.2]
           at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:184) ~[httpclient-4.5.2.jar:4.5.2]
           at org.apache.http.impl.execchain.RetryExec.execute(RetryExec.java:88) ~[httpclient-4.5.2.jar:4.5.2]
           at org.apache.http.impl.execchain.RedirectExec.execute(RedirectExec.java:110) ~[httpclient-4.5.2.jar:4.5.2]
           at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:184) ~[httpclient-4.5.2.jar:4.5.2]
           at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:82) ~[httpclient-4.5.2.jar:4.5.2]
           at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:107) ~[httpclient-4.5.2.jar:4.5.2]
           at com.skyme.HttpClientUtils.httpGet(HttpClientUtils.java:31) ~[classes/:na]
           at com.skyme.TestController.lambda$batchHttpCall$0(TestController.java:47) ~[classes/:na]
           at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) ~[na:1.8.0_73]
           at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) ~[na:1.8.0_73]
           at java.lang.Thread.run(Thread.java:745) ~[na:1.8.0_73]

          初步結(jié)論

          外部有大量請求,很多請求的處理線程在等待 HttpClient 獲取連接,問題應該在 HttpClient。

          進一步排查

          分析 HttpClient

          日志里兩個關鍵信息:

          1. 是 HttpClient 內(nèi)部報錯
          org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool
           at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.leaseConnection(PoolingHttpClientConnectionManager.java:286) ~[httpclient-4.5.2.jar:4.5.2]

          對應的源碼org.apache.http.impl.conn.PoolingHttpClientConnectionManager.leaseConnection(PoolingHttpClientConnectionManager.java:286)

          protected HttpClientConnection leaseConnection(
                  final Future<CPoolEntry> future,
                  final long timeout,
                  final TimeUnit tunit)
           throws InterruptedException, ExecutionException, ConnectionPoolTimeoutException 
          {
              final CPoolEntry entry;
              try {
                  entry = future.get(timeout, tunit); // 該處會拋出TimeoutException
                  if (entry == null || future.isCancelled()) {
                      throw new InterruptedException();
                  }
                  Asserts.check(entry.getConnection() != null"Pool entry with no connection");
                  if (this.log.isDebugEnabled()) {
                      this.log.debug("Connection leased: " + format(entry) + formatStats(entry.getRoute()));
                  }
                  return CPoolProxy.newProxy(entry);
              } catch (final TimeoutException ex) {
                  // 捕獲TimeoutException異常,再拋出ConnectionPoolTimeoutException
                  throw new ConnectionPoolTimeoutException("Timeout waiting for connection from pool");
              }
          }    

          拋出ConnectionPoolTimeoutException是因為觸發(fā)了TimeoutException異常,而TimeoutException異常對應于entry = future.get(timeout, tunit);

          PoolingHttpClientConnectionManager.leaseConnection 是為了獲取連接,分析一下其執(zhí)行流程,獲取 connection 時序圖如下:

          httpclient獲取connection的時序圖

          重點的步驟:

          1. 如果 route 下有空閑連接,則直接返回空閑連接
          2. 如果沒有空閑連接,且該 connPerRoute 及池中 conn 數(shù)均沒有達到最大值,的 conn 數(shù)沒有達到最大值,則創(chuàng)建連接,并放入池中,并返回該連接
          3. 如果沒有空閑連接,且達到了 maxRoute 或 maxPoolsize,則阻塞等待,等待的時常,便是entry = future.get(timeout, tunit);中的timeout時長。如果指定的時間沒有 lease 到 connection ,則 entry = future.get(timeout, tunit); 會拋出 TimeoutException 異常。

          一般 pool 設計都是和上面的類似,我們繼續(xù)分析一下 httpclient pool 的結(jié)構(gòu) :

          PoolEntry<HttpRoute, OperatedClientConnection>: 路由和連接的對應關系。

          routeToPool: 可以多個(圖中僅示例兩個),圖中各隊列大小動態(tài)變化,并不相等。

          maxTotal: 限制的是外層 httpConnPoolleased 集合和 available 隊列的總和的大小,httpConnPoolleasedavailable 的大小沒有單獨限制。

          maxPerRoute: 限制的是每個 routeToPoolleased 集合和 available 隊列的總和的大小。

          一步步跟蹤源碼entry = future.get(timeout, tunit);,定位到核心調(diào)用處 org.apache.http.pool.AbstractConnPool#getPoolEntryBlocking() ,對應源碼

          private E getPoolEntryBlocking(
                  final T route, final Object state,
                  final long timeout, final TimeUnit tunit,
                  final PoolEntryFuture<E> future)

                      throws IOException, InterruptedException, TimeoutException 
          {

              Date deadline = null// 阻塞等待的截止時間
              if (timeout > 0) { 
                  deadline = new Date
                      (System.currentTimeMillis() + tunit.toMillis(timeout));
              }
              // 首先對當前連接池加鎖,當前鎖是可重入鎖ReentrantLockthis.lock.lock();
              this.lock.lock();
              try {
                  // 獲得一個當前HttpRoute對應的連接池,即routeToPool
                  final RouteSpecificPool<T, C, E> pool = getPool(route);
                  E entry = null;
                  while (entry == null) {
                      Asserts.check(!this.isShutDown, "Connection pool shut down");
                      for (;;) {
                          entry = pool.getFree(state);
                          if (entry == null) {
                              break;
                          }
                          // 如果拿到的是過期連接或者已關閉連接,就釋放資源,繼續(xù)循環(huán)獲取
                          if (entry.isExpired(System.currentTimeMillis())) {
                              entry.close();
                          } else if (this.validateAfterInactivity > 0) {
                              if (entry.getUpdated() + this.validateAfterInactivity <= System.currentTimeMillis()) {
                                  if (!validate(entry)) {
                                      entry.close();
                                  }
                              }
                          }
                          if (entry.isClosed()) {
                              this.available.remove(entry);
                              pool.free(entry, false);
                          } else {
                              break;
                          }
                      }
                      // 拿到未在使用中的(available)有效連接就退出循環(huán),直接return對應連接
                      if (entry != null) {
                          this.available.remove(entry);
                          this.leased.add(entry);
                          onReuse(entry);
                          return entry;
                      }

                      // New connection is needed。執(zhí)行到這說明沒有拿到連接,需要新建連接
                      final int maxPerRoute = getMax(route);
                      // Shrink the pool prior to allocating a new connection
                      // pool.getAllocatedCount()=pool.available.size() + pool.leased.size(),可用的加正在租借的即當前池中已有的連接
                      // 如果route對應的連接池的已有連接數(shù)超過了支持的最大連接數(shù)量(加1的目的?),則通過LRU清理掉一些連接。
                      final int excess = Math.max(0, pool.getAllocatedCount() + 1 - maxPerRoute);
                      if (excess > 0) {
                          for (int i = 0; i < excess; i++) {
                              final E lastUsed = pool.getLastUsed();
                              if (lastUsed == null) {
                                  break;
                              }
                              lastUsed.close();
                              this.available.remove(lastUsed);
                              pool.remove(lastUsed);
                          }
                      }

                      // 如果route對應的連接池的已有連接數(shù)未達到最大最大限制
                      if (pool.getAllocatedCount() < maxPerRoute) {
                          final int totalUsed = this.leased.size();
                          // freeCapacity指大池子里除了已用連接,最多能容納的空閑連接數(shù)(包括已經(jīng)創(chuàng)建的available連接和后續(xù)還允許創(chuàng)建的available連接)
                          final int freeCapacity = Math.max(this.maxTotal - totalUsed, 0);
                          if (freeCapacity > 0) {
                              final int totalAvailable = this.available.size();
                              // 此時當前route肯定leased已滿,無available,若大池子里已達到上限,且大池子里還有available的空閑連接,那可以清理其他route下的無available連接,為新連接挪出位置。所以close的是otherpool中l(wèi)astUsed的available連接。若大池子沒有達到上限,則沒必要做清理。
                              if (totalAvailable > freeCapacity - 1) {
                                  if (!this.available.isEmpty()) {
                                      final E lastUsed = this.available.removeLast();
                                      lastUsed.close();
                                      final RouteSpecificPool<T, C, E> otherpool = getPool(lastUsed.getRoute());
                                      otherpool.remove(lastUsed);
                                  }
                              }
                              // 創(chuàng)建一個新連接
                              final C conn = this.connFactory.create(route);
                              // 放入route對應的池子的leased集合
                              entry = pool.add(conn);
                              // 放入大池子的leased集合
                              this.leased.add(entry);
                              // reutrn該連接
                              return entry;
                          }
                      }

                      // 到這里證明沒有從獲得route池中獲得有效連接,并且想要自己建立連接時當前route連接池已經(jīng)到達最大值,即已經(jīng)有連接在使用,但是對當前線程不可用
                      boolean success = false;
                      try {
                          // 將future放入route池中等待,實際就是加入route池的pending集合
                          pool.queue(future);
                          // 放入大池子的pending集合
                          this.pending.add(future);
                          // 如果等待到了信號量的通知,success為true。如果到截止時間還未等到,則success為false
                          success = future.await(deadline);
                      } finally {
                          // In case of 'success', we were woken up by the
                          // connection pool and should now have a connection
                          // waiting for us, or else we're shutting down.
                          // Just continue in the loop, both cases are checked.
                          // fanally,從等待隊列中移除
                          pool.unqueue(future);
                          this.pending.remove(future);
                      }
                      // check for spurious wakeup vs. timeout
                      // 如果沒有等到信號量通知并且當前時間已經(jīng)超時,則退出循環(huán)
                      if (!success && (deadline != null) &&
                          (deadline.getTime() <= System.currentTimeMillis())) {
                          break;
                      }
                  }
                  // 最終也沒有等到信號量通知,沒有拿到可用連接,則拋異常
                  throw new TimeoutException("Timeout waiting for connection");
              } finally {
                  // 釋放對大池子的鎖
                  this.lock.unlock();
              }
          }

          綜合分析:

          • 通過線程池獲取連接要通過 ReetrantLock 加鎖,保證線程安全
          • 不論是大連接池還是小連接池,當超過數(shù)量的時候,都要通過LRU釋放一些連接
          • 如果拿到了可用連接,則返回給上層使用
          • 如果沒有拿到可用連接,HttpClient 會判斷當前 route 連接池是否已經(jīng)超過了最大數(shù)量,沒有到上限就會新建一個連接,并放入池中
          • 如果到達了上限,就排隊等待,等到了信號量,就重新獲得一次,等待不到就拋 TimeoutException

          思考

          池類的設計,不管是連接池,線程池,一般不夠用的場景,都是最大數(shù)設置的不夠大,不足以應對并發(fā)獲取連接、線程等?

          確認HttpClient實際情況

          上面的都是猜想,最終問題定位還是要摸清實際情況。

          應用里是如何創(chuàng)建 HttpClient 實例

          下面是應用中創(chuàng)建 HttpClient 實例的地方,我們從變量 HTTP_CLIENT開始定位。

          private static final CloseableHttpClient HTTP_CLIENT = HttpClients
              .custom()
              .setDefaultRequestConfig(
                      RequestConfig.custom()
                      .setCookieSpec(CookieSpecs.IGNORE_COOKIES)
                      .setConnectionRequestTimeout(30000)
                      .build()
              ).build();

          上面代碼可知:

          1. 沒有設置 maxPerRoute、maxTotal這類的參數(shù)。
          2. 從連接池獲取連接的超時時間的參數(shù) ConnectionRequestTimeout ,設置為30秒。

          arthas 定位實際數(shù)據(jù)

          因為不是本地環(huán)境,不能方便地 Debug,這時可以用 arthas 來查進行內(nèi)存中 HttpClient 的實際數(shù)據(jù)。

          在 arthas 中執(zhí)行:getstatic com.skyme.HttpClientUtils HTTP_CLIENT -x 4,結(jié)果如下:

          routeToPool中,route [https//github.com] 的使用情況是:leased 為 2,avaibable 為 0,pending 為 198。

          leased + avaibable 結(jié)果為 2,怎么這么少?難道是 maxPerRoute 參數(shù)設置的過小?

          在 arthas 結(jié)果中我們找到了 maxPerRoute 相關的配置,maxPerRoute 默認值只有2,因為我們創(chuàng)建 HttpClient 實例時,沒有設置maxPerRoute的數(shù)值,所以每個route 的 maxPerRoute 為2, 這和上面的 leased + avaibable 的結(jié)果對應上了。此外,還有一個重要參數(shù) maxToal 為 20。

          大致結(jié)論:默認參數(shù)太小

          到這里,問題基本定位清楚了。因為應用沒有顯示地設置 maxPerRoute 和 maxToal ,所以取了默認值,默認值比較小,maxPerRoute 為2, maxToal 為20, 導致并發(fā)獲取連接時,連接不夠用,導致了獲取連接的線程一直在等待,等待時間為我們創(chuàng)建 HttpClien 實例時自定義的 30 秒,等待時間過長,導致了外部請求超時。

          解決辦法

          臨時辦法

          可以使用 arthas 的 vmtool 命令將內(nèi)存中的 defaultMaxPerRoute 、maxTotal 參數(shù)修改大一些,該操作過于風騷,一般不符合生產(chǎn)環(huán)境的運維操作規(guī)范。但是測試環(huán)境或者緊急情況可以嘗試使用。

          [arthas@8433]$ vmtool --action getInstances --className org.apache.http.impl.client.CloseableHttpClient --express 'instances[0].connManager.pool.defaultMaxPerRoute=100'
          @Integer[100]
          [arthas@8433]$ vmtool --action getInstances --className org.apache.http.impl.client.CloseableHttpClient --express 'instances[0].connManager.pool.maxTotal=500'

          修改后,連接池中 leased 明顯增多,pending 減少。

          一段時間后,pending 為 0,說明沒有阻塞,連接數(shù)已足夠使用。

          注意:

          1. 該修改不需要應用重啟,但應用重啟后,內(nèi)存的設置將銷毀,依然還會存在問題。
          2. 非常規(guī)操作,謹慎執(zhí)行。
          3. vmtool 命令在 arthas 3.5.1 版本之后才有。

          長期辦法

          修改代碼,配置合理的最大連接數(shù) maxPerRoute、maxTotal、ConnectionRequestTimeout。

          當前 HttpClient 版本為 4.5.2,其他版本可能代碼有所差異。

          private final static PoolingHttpClientConnectionManager POOLING_HTTP_CLIENT_CONNECTION_MANAGER
                  = new PoolingHttpClientConnectionManager();

          static {
              // 每個route默認的最大連接數(shù)
              POOLING_HTTP_CLIENT_CONNECTION_MANAGER.setDefaultMaxPerRoute(100);
              // 總的連接池最大連接數(shù)
              POOLING_HTTP_CLIENT_CONNECTION_MANAGER.setMaxTotal(500);
          }

          private static final CloseableHttpClient HTTP_CLIENT = HttpClients
              .custom()
              .setConnectionManager(POOLING_HTTP_CLIENT_CONNECTION_MANAGER)
              .setDefaultRequestConfig(
                      RequestConfig.custom()
                      .setCookieSpec(CookieSpecs.IGNORE_COOKIES)//
                      // 從連接池獲取連接的超時時間
                      .setConnectionRequestTimeout(10000)
                      .build()
              ).build();

          defaultMaxPerRoute 設置為 100,maxTotal 設置為 500,并將 connectionRequestTimeout 從 30 秒減少到 10 秒,沒有拿到連接快速失敗,防止過多的線程阻塞導致擠壓。

          總結(jié)

          1. 問題原因是 HttpClient 使用時沒有對參數(shù)足夠了解,對于連接池的框架,最好設置最大數(shù),獲取連接的等待超時時間,以及回收的周期,連接是否可用的校驗等等參數(shù)。其他池類的框架同理,例如線程池。
          2. 通過源碼、日志、線程、連接等多方面進行分析,結(jié)論會更準確。
          3. arthas 是神器,可以多嘗試。

          瀏覽 889
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产AV无码专区亚洲A∨毛片 | 国产欧美日韩久久 | 中文字幕第83页 | 国产AV日韩 | 国产精品无码一区二区三区免费 |