HttpClient遭遇Connection Reset異常,如何正確配置?


最近工作中使用的HttpClient工具遇到的Connection Reset異常。在客戶端和服務端配置不對的時候容易出現問題,下面就是記錄一下如何解決這個問題的過程。

出現Connection Reset的原因

1.客戶端在讀取數據,服務端不再發送新數據(服務器主動關閉了連接)

為什么會出現服務端主動關閉連接?

經過排查線上服務器配置,發現當一個連接空閑時間超過60s,服務器就會將其關閉。如果剛好客戶端在使用該連接則客戶端就會收到來自服務端的連接復位標志通知

既然明白了服務端關閉的連接的原因,那為什么客戶端會使用空閑時間為60s的連接呢?

排查了HttpClient的配置后發現,項目中的HttpClient使用連接池,雖然設置了池的最大連接數,但是沒有配置空閑連接驅逐器(IdleConnectionEvictor)。到這里原因就已經很明朗了,就是httpClient的配置有問題。

解決思路:

如果說服務端會吧空閑時間超過60s的空閑連接關閉掉,導致了connection reset 異常。要解決這個問題,那只要客戶端在服務器關閉連接之前把連接關閉掉那就不會出現了。所以按着這個思路我對httpClient的配置進行了修改。

解決方案1:

為HttpClient添加空閑連接驅逐器配置

新加了evictIdleConnections(40, TimeUnit.SECONDS)配置

HttpClients
  .custom()
  // 默認請求配置
  .setDefaultRequestConfig(customRequestConfig())
  // 自定義連接管理器
  .setConnectionManager(poolingHttpClientConnectionManager())
  // 刪除空閑連接時間
  .evictIdleConnections(40, TimeUnit.SECONDS)
  .disableAutomaticRetries(); // 關閉自動重試

正常情況下到這里問題就解決了,但是現實是線上再次出現了Connection Reset異常。繼續排查...

思考:雖然更新配置后再次出現“連接重置”異常,不過出現頻率相較於沒改之前還是要低不少。所以改的配置還有用的,肯定是什么地方沒有配好。為了一探究竟,查了HttpClient關於IdleConnectionEvictor驅逐器的源碼發現了問題所在。

源碼解讀:

源碼1:

// org.apache.http.impl.client.HttpClientBuilder
public class HttpClientBuilder {
  // .....省略無關代碼....
  // 關注build方法,這這個方法里面啟動了空閑連接驅逐器
  public CloseableHttpClient build() {
    // 。。。。省略代碼。。。。
       if (!this.connManagerShared) {
            if (closeablesCopy == null) {
                closeablesCopy = new ArrayList<Closeable>(1);
            }
            final HttpClientConnectionManager cm = connManagerCopy;

            if (evictExpiredConnections || evictIdleConnections) {
              // 在這里實例化了IdleConnectionEvictor。maxIdleTime和maxIdleTimeUnit就是我們在配置httpclient時
              // 傳入的 40 和 TimeUnit.SECONDS
                final IdleConnectionEvictor connectionEvictor = new IdleConnectionEvictor(cm,
                        maxIdleTime > 0 ? maxIdleTime : 10, maxIdleTimeUnit != null ? maxIdleTimeUnit : TimeUnit.SECONDS,
                        maxIdleTime, maxIdleTimeUnit);
                closeablesCopy.add(new Closeable() {

                    @Override
                    public void close() throws IOException {
                        connectionEvictor.shutdown();
                        try {
                            connectionEvictor.awaitTermination(1L, TimeUnit.SECONDS);
                        } catch (final InterruptedException interrupted) {
                            Thread.currentThread().interrupt();
                        }
                    }

                });
              // 調用start()發放啟動了線程驅逐器
                connectionEvictor.start();
            }
            closeablesCopy.add(new Closeable() {

                @Override
                public void close() throws IOException {
                    cm.shutdown();
                }

            });
        }
        // 。。。。省略無關代碼。。。。。
  }
}
  1. evictIdleConnections(40, TimeUnit.SECONDS)配置的參數在HttpClientBuilder.builder方法中用於實例化IdleConnectionEvictor對象的構造參數

  2. 調用了connectionEvictor.start()方法啟動了線程驅逐器

源碼2:

// org.apache.http.impl.client.IdleConnectionEvictor
public final class IdleConnectionEvictor {
  // 。。。。省略無關代碼。。。。
  // HttpClientBuilder.build()內實例化IdleConnectionEvictor調用了該構造方法
    public IdleConnectionEvictor(
            final HttpClientConnectionManager connectionManager,
            final long sleepTime, final TimeUnit sleepTimeUnit,
            final long maxIdleTime, final TimeUnit maxIdleTimeUnit) {
        this(connectionManager, null, sleepTime, sleepTimeUnit, maxIdleTime, maxIdleTimeUnit);
    }
  // 。。。。省略無關代碼。。。。
}
關鍵的參數列表
  1. sleepTime:延時檢查時間
  2. maxIdleTime:最多空閑時間

結合源碼1和源碼2,可以看到在構造IdleConnectionEvictorsleepTimemaxIdleTime為同一個值40秒,在這里還看不出什么問題,繼續。

源碼3:

// org.apache.http.impl.client.IdleConnectionEvictor
public final class IdleConnectionEvictor {
  // 省略無關代碼
  // 重載的構造方法
    public IdleConnectionEvictor(
            final HttpClientConnectionManager connectionManager,
            final ThreadFactory threadFactory,
            final long sleepTime, final TimeUnit sleepTimeUnit,
            final long maxIdleTime, final TimeUnit maxIdleTimeUnit) {
        this.connectionManager = Args.notNull(connectionManager, "Connection manager");
        this.threadFactory = threadFactory != null ? threadFactory : new DefaultThreadFactory();
        this.sleepTimeMs = sleepTimeUnit != null ? sleepTimeUnit.toMillis(sleepTime) : sleepTime;
        this.maxIdleTimeMs = maxIdleTimeUnit != null ? maxIdleTimeUnit.toMillis(maxIdleTime) : maxIdleTime;
      // 使用threadFactory線程構造器構造了一個守護線程
        this.thread = this.threadFactory.newThread(new Runnable() {
            @Override
            public void run() {
                try {
                    while (!Thread.currentThread().isInterrupted()) {
                      // 掛起線程時間是我們傳入的時間40秒
                        Thread.sleep(sleepTimeMs);
                      // 執行檢查代碼,關閉過期連接
                        connectionManager.closeExpiredConnections();
                        if (maxIdleTimeMs > 0) {
                          // 關閉超過空閑時間的空閑連接,參數傳入我們配置的40秒
                            connectionManager.closeIdleConnections(maxIdleTimeMs, TimeUnit.MILLISECONDS);
                        }
                    }
                } catch (final Exception ex) {
                    exception = ex;
                }

            }
        });
    }
  
  // HttpClientBuilder中調用的start()方法
    public void start() {
        thread.start();
    }
}

通過源碼3我們可以看到,檢查線程的執行周期時間和最大過期時間都是我們傳入的40秒。在這里停頓一下思考一下,服務器的空閑連接關閉時間是60s,我們配置的時間是40s,那這樣配置會不有出現什么問題?

線程相隔40s執行一下回收任務,相當於80秒的的周期內會做兩次回收動作。但是60s在其中最多只能回收掉一次,還是可能存在回收不掉的情況,在不執行回收任務停止的40秒里面出了connection reset異常了怎么吧?問題就明了了。

問題復現時序:
  1. 00:00:00 --- 啟動IdleConnectionEvictor.start(),掛起檢查線程,不執行檢查代碼
  2. 00:00:10 --- 10秒后的連接池新建了一個連接
  3. 00:00:12 --- 連接耗時2s,用完后返回線程池,假設之后都沒有再被使用了
  4. 00:00:40 --- 第一次sleep掛起時間到期,執行檢查任務。發現沒有過期連接,下一次回收任務發生在 00:01:20
  5. 00:01:12 --- 這時恰好客戶端使用那個空閑的連接,服務端關閉了該連接。在這里發生了connection reset 異常
  6. 00:01:20 --- 第二次sleep掛起時間到期,執行檢查任務。

結論:

服務端空閑連接關閉時間是60s,我們客戶端配置的最大空閑時間值應該小於30s才能避免這個問題

解決方案2:

在解決方案1的基礎上,把40s時間改為20s,順利解決了該問題。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM