Apache HttpClient 5.x 再利用(使いまわし) 接続設定 ポイント まとめ

# 概要

Apache HttpClient 5.x を使うときのポイントをまとめる。以下の公式ドキュメントはあっさりしているので、コードを読むしかなかった...どこかに詳細があるんだとは思いつつ見つけられなかったのでメモを残す。
また、 Apache HttpClient 4.x と仕組みは似ているので、そのあたりの記事も参考にした。

[Apache HttpComponents – HttpClient Overview](https://hc.apache.org/httpcomponents-client-5.1.x/index.html)

前提として、`PoolingHttpClientConnectionManager`のコードで得た知識なので、他のManagerと設定値の扱いが若干ことなるかもしれない。現時点の最新のライブラリのバージョンは、5.1.3だが、読んだコードはもう少し進んでいるのでその辺の差異も注意したい。

## 参考

- [Apache HttpClient Connection Management | Baeldung](https://www.baeldung.com/httpclient-connection-management)
- [apache HttpClient の eviction について](https://nisenabe.hatenablog.com/entry/2016/12/29/155457)

Apache HttpClient 4.x 系であれば上の記事がとても参考になる


# ポイント

- http client インスタンスは使い回す
- EntityUtils.consume を使ってレスポンスボディを確実に消費(使い切る)する
- maxConnTotal, maxConnPerRouteを設定する
- 必要に応じて timeToLive(永続化の最大期間)を設定する

# 詳細

## http client インスタンスは使い回す

基本はこれ。簡単なサンプルコードでは、リクエストごとにインスタンスを生成するように（みえるように）書かれているが、コネクションの再利用するために http client のインスタンスを使い回すことが必要になる

### メリット

 - http client の生成にかかる CPU やメモリの節約になる(リクエスト量にもよるが割と変わってくるので試してほしい)
 - コネクション取得にかかる時間が節約できる (特にSSL(https)通信の場合は数十msの違いがでる)

### 注意点

- 利用が終わったら `client.close()` を用いて接続を切る

[HttpClient Quick Start](https://hc.apache.org/httpcomponents-client-5.1.x/quickstart.html)では、1インスタンスで2回リクエストを呼ぶようなサンプルにはなっているので、ぎりぎり意図は感じることができる

また、サンプルコードのコメントに接続の再利用に対する注意が書かれている(次のポイントとなる` EntityUtils.consume`にも関連する)

```
The underlying HTTP connection is still held by the response object to allow the response content to be streamed directly from the network socket. In order to ensure correct deallocation of system resources the user MUST call CloseableHttpResponse#close() from a finally clause. Please note that if response content is not fully consumed the underlying connection cannot be safely re-used and will be shut down and discarded by the connection manager.
```

## EntityUtils.consume を使ってレスポンスボディを確実に消費(使い切る)する

http client のインスタンスを使いまわしているのに、コネクションの取得時間が改善されない場合はこれが原因の可能性がある。コネクションの再利用には、リクエストで使い終わったあとに、レスポンスボディを使い切ってゴミ？が残らないようにしなければいけない（でなければ、コネクションは切断される）。 

### EntityUtils.consume　を使う方法

getEntity でレスポンスボディを取得したあとに、`EntityUtils.consume`を呼び出す

```java:Quick Startより引用
try (CloseableHttpResponse response1 = httpclient.execute(httpGet)) {
    HttpEntity entity1 = response1.getEntity();
    // ここで処理する
    EntityUtils.consume(entity1);
}
```

CloseableHttpClientの実装を見ると、例外発生時もリクエストボディを消費している。例外発生時もコネクションを切断しなくない場合は、以下のようにするとよさそう

```java:CloseableHttpClientの実装を参考
try (CloseableHttpResponse response = httpclient.execute(httpGet)) {
    try {
        final HttpEntity entity = response.getEntity();
        // ここで処理する
        EntityUtils.consume(entity);
    } catch (final HttpException t) {
        final HttpEntity entity = response.getEntity();
        try {
            EntityUtils.consume(entity);
        } catch (final Exception t2) {
            LOG.warn("Error consuming content after an exception.", t2);
        }
        throw new ClientProtocolException(t);
    }
}
```

### HttpClientResponseHandler を使う方法

HttpClientResponseHandlerを使ってレスポンスを処理する場合は、`execute`メソッド内で`EntityUtils.consume`を呼んでいるため、リクエストボディを個別に消費するコードが不要になる。HttpClientResponseHandlerは、別クラスで定義してもよいし、無名クラスで定義してもよいが、別クラスで定義する場合は、リクエストごとにインスタンスを生成する必要はないので注意する(しても大してかわらないかもしれない...不明)

```java:Migration from Apache HttpClient 4.x APIsより引用
JsonNode responseData = client.execute(httpPost, response -> {
    if (response.getStatusLine().getStatusCode() >= 300) {
        throw new ClientProtocolException(Objects.toString(response.getStatusLine()));
    }
    final HttpEntity responseEntity = response.getEntity();
    if (responseEntity == null) {
        return null;
    }
    try (InputStream inputStream = responseEntity.getContent()) {
        return objectMapper.readTree(inputStream);
    }
});
```

## maxConnTotal, maxConnPerRoute を設定する

PoolingHttpClientConnectionManagerのデフォルトでは、maxConnTotalは25、maxConnPerRouteは5に設定されている。それぞれの設定の意味は以下のとおり。
このデフォルト値で足りない場合に`ConnectionRequestTimeoutException`が出るが、十分にコネクションをプールしておかないとコネクション取得に時間がかかってしまう問題がある。

| 項目 | 意味 | 注意 |
|:-|:-|:-|
| maxConnTotal  | コネクションプールの最大コネクション数  | PoolConcurrencyPolicyがLAXの場合は無効  |
|  maxConnPerRoute | 宛先(ホストとポート番号(80,443など)とスキーマ(http or https))単位の最大のコネクション数  |   |

## 必要に応じて timeToLive(永続化の最大期間)を設定する

`timeToLive`はコネクションの永続化の最大期間で、コネクションが作成された時間から切断されるまでの最長の期間を設定できる。例えば、`keepalive`の設定でコネクションが維持された場合も、この`timeToLive`を設定しておけば、コネクションの維持期間を設定できる(もちろん、無限にも設定できる)。
コネクションを定期的に再接続させることで、コネクションの接続維持中に接続先のホストでコネクションが意図せず破棄されるなどのケースで、通信に失敗することを抑制できる

この設定は、コネクションプールからコネクションを取得したときに毎回チェックされている(少なくとも最新の実装では...)が、これとは別に`HttpClientBuilder`で`evictExpiredConnections`や`evictIdleConnections`を設定している場合は、定期的(デフォルト5秒)で`timeToLive`のチェックをコネクションプールのコネクションに実施してくれる(maxIdleTime指定時は、アイドル状態のコネクションの破棄もしてくれる)。こうすることでコネクションプールからコネクションを取り出したときのチェックや再取得のコストが減るということらしい。レイテンシの要件が厳しいアプリケーションでは有効かもしれない。


## その他の時間に関する設定値

### ConnectionConfig

| 項目 | 意味 | 説明 |
|:-|:-|:-|
| connectTimeout | コネクション確立時のタイムアウト値 | RequestConfigのconnectTimeoutよりも優先される模様 |
| socketTimeout | ソケットタイムアウト | コネクション確立時に使用されるようだが、デフォルト値としてレスポンス受付時のソケットタイムアウト値としても利用されるんだろうな |
| validateAfterInactivity | コネクションが未使用になったあとに、コネクションが利用できるかをチェックするまでの期間 | 今の`PoolingHttpClientConnectionManager`の実装だとデフォルト値が3秒になっているので、短すぎる場合は長めに設定するのもよさそう。ただし、どこまで性能に寄与するかは謎。この値にマイナス値を指定するとこのチェックは行われないので、リトライ処理などが実装されていて、多少の接続不良が許容できるならそれでもいいのかもしれない。 |
| timeToLive | 永続化の最大期間 | 上の章で説明済 |


### RequestConfig

| 項目 | 意味 | 説明 |
|:-|:-|:-|
| connectionRequestTimeout | コネクション取得時のタイムアウト値 | コネクションプールからコネクションを取り出すときにこのタイムアウト値を超えるとエラーになる(デフォルトは3分)  |
| connectTimeout | コネクション確立時のタイムアウト値 | connectionRequestTimeoutの方を長く取らないと辻褄が合わなくなる。connectionConfigのconnectTimeoutが優先されるようです(未検証) |
| responseTimeout | レスポンスを受け取るときのソケットタイムアウト値 | 完全にレスポンスを受け取るまでの時間のように見えるが、接続先のソケットタイムアウトに設定している。これを指定しない場合は、connectionConfigのsocketTimeoutが有効になっているんだろうな |
| connectionKeepAlive | http keep alive タイムアウト | コネクションがアイドル状態になったあとに維持される最大期間  |