背景
他部署が提供するhttpベースのサービスを呼び出す業務があり、1日の呼び出し量は数千万件。その業務を完了させるためにhttpclientが使われていました。qpsが上がらないので、ビジネスコードを見て、いくつかの最適化を行いました。
最適化前と最適化後を比較:最適化された、平均実行時間は250ミリ秒であり、最適化後、平均実行時間は80ミリ秒であり、3分の2の消費量を削減し、コンテナは、もはやアラームスレッドの枯渇に移動していない、リフレッシュ〜!
分析
プロジェクトの元の実装は比較的粗いです、つまり、各リクエストはhttpclientを初期化し、httpPostオブジェクトを生成し、実行し、文字列として保存されたENTITYを取り出すために、戻り結果から、最後に明示的に応答とクライアントを閉じます:
httpclient繰り返し生成されるオーバーヘッド
httpclient はスレッドセーフなクラスなので、使用するたびに各スレッドで作成する必要はありません。
tcpコネクションを繰り返し生成するオーバーヘッド
tcpの3回のハンドシェイクと4回のハンドシェイクは、2つの主要なラップアラウンドプロセスであり、高頻度のリクエストに対してあまりにも多くの時間を消費します。もし各リクエストがネゴシエーション処理に5msを費やす必要があるとすると、100の単一システムのqpsでは、ハンドシェイクとウェーブに500msを費やすために1秒を費やすことになります。もしあなたが上級リーダーでないなら、プログラマーはそんな大げさなことをせず、コネクションの再利用を達成するためにキープアライブメソッドに変更するべきです!
重複エンティティキャッシュのオーバーヘッド
HttpEntity entity = httpResponse.getEntity();
String response = EntityUtils.toString(entity);
元のhttpResponseはまだコンテンツのコピーを保持しながら、ここでは、文字列にコンテンツの追加コピーに相当する、消費される必要がある、高い並行性とコンテンツが非常に大きい場合には、メモリの多くを消費します。
実装
上記の分析によると、3つの主なものがあります:1つは、クライアントの単一のインスタンスであり、第二は、ライブ接続をキャッシュすることであり、第三は、より良い戻り結果を処理することです。一つは、2つを言うために、言うことはありません。
接続キャッシュといえば、データベース接続プールを思い浮かべるのが簡単です。httpclient4は、接続プールとしてPoolingHttpClientConnectionManagerを提供します。次のステップは、以下の手順に従って最適化することです:
キープアライブ戦略の定義
keep-aliveについては、この記事では詳しく触れませんが、keep-aliveを使うかどうかはビジネスの状況次第であり、万能ではないという一点だけ触れておきます。もうひとつ、keep-aliveとtime_wait/close_waitの間にはかなり深い話があります。
このビジネスシナリオでは、少数の固定クライアントが非常に高い頻度で長時間サーバーにアクセスすることに相当し、キープアライブを有効にすることは非常に適切です。
余談ですが、httpのkeep-aliveはtcpのKEEPALIVEとは別物です。本文に戻って、戦略を次のように定義します:
ConnectionKeepAliveStrategy myStrategy = new ConnectionKeepAliveStrategy() {
@Override
public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
HeaderElementIterator it = new BasicHeaderElementIterator
(response.headerIterator(HTTP.CONN_KEEP_ALIVE));
while (it.hasNext()) {
HeaderElement he = it.nextElement();
String param = he.getName();
String value = he.getValue();
if (value != null && param.equalsIgnoreCase
("timeout")) {
return Long.parseLong(value) * 1000;
}
}
return ;//合意がない場合、デフォルトのタイムアウトは60秒である。
}
};
PoolingHttpClientConnectionManager の設定
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
connectionManager.setMaxTotal(500);
connectionManager.setDefaultMaxPerRoute(50);//例えば、ルートごとのデフォルトの最大同時接続数は50である。
また、各ルートごとに同時実行数を設定することもできます。
httpclientの生成
httpClient = HttpClients.custom()
.setConnectionManager(connectionManager)
.setKeepAliveStrategy(kaStrategy)
.setDefaultRequestConfig(RequestConfig.custom().setStaleConnectionCheckEnabled(true).build())
.build();
注意: setStaleConnectionCheckEnabled メソッドを使用して閉じたリンクを破棄することは推奨され ません。以下のように、closeExpiredConnections メソッドと closeIdleConnections メソッドを一定間隔で実行するスレッドを手動で有効にする方がよい方法です。
public static class IdleConnectionMonitorThread extends Thread {
private final HttpClientConnectionManager connMgr;
private volatile boolean shutdown;
public IdleConnectionMonitorThread(HttpClientConnectionManager connMgr) {
super();
this.connMgr = connMgr;
}
@Override
public void run() {
try {
while (!shutdown) {
synchronized (this) {
wait(5000);
// Close expired connections
connMgr.closeExpiredConnections();
// Optionally, close connections
// that have been idle longer than 30 sec
connMgr.closeIdleConnections(30, TimeUnit.SECONDS);
}
}
} catch (InterruptedException ex) {
// terminate
}
}
public void shutdown() {
shutdown = true;
synchronized (this) {
notifyAll();
}
}
}
httpclient でメソッドを実行する際のオーバーヘッドの削減
ここで注意すべきことは、接続を閉じないことです。
内容を取得する一つの可能な方法は、ENTITYにある内容のコピーを作成することです:
res = EntityUtils.toString(response.getEntity(),"UTF-8");
EntityUtils.consume(response1.getEntity());
しかし、より推奨される方法は、ResponseHandlerを定義することで、例外をキャッチしたりストリームを閉じたりするのを簡単に止めることができます。関連するソースコードをご覧ください:
public <T> T execute(final HttpHost target, final HttpRequest request,
final ResponseHandler<? extends T> responseHandler, final HttpContext context)
throws IOException, ClientProtocolException {
Args.notNull(responseHandler, "Response handler");
final HttpResponse response = execute(target, request, context);
final T result;
try {
result = responseHandler.handleResponse(response);
} catch (final Exception t) {
final HttpEntity entity = response.getEntity();
try {
EntityUtils.consume(entity);
} catch (final Exception t2) {
// Log this exception. The original exception is more
// important and will be thrown to the caller.
this.log.warn("Error consuming content after an exception.", t2);
}
if (t instanceof RuntimeException) {
throw (RuntimeException) t;
}
if (t instanceof IOException) {
throw (IOException) t;
}
throw new UndeclaredThrowableException(t);
}
// Handling the response was successful. Ensure that the content has
// been fully consumed.
final HttpEntity entity = response.getEntity();
EntityUtils.consume(entity);//こちらを参照のこと。
return result;
}
ご覧のように、resultHandlerを使ってexecuteメソッドを実行すると、自動的に以下のようなconsumeメソッドが呼び出されます:
public static void consume(final HttpEntity entity) throws IOException {
if (entity == null) {
return;
}
if (entity.isStreaming()) {
final InputStream instream = entity.getContent();
if (instream != null) {
instream.close();
}
}
}
最終的に入力ストリームが閉じられるのがわかります。
その他
以上の手順で、基本的に高い並行性をサポートする httpclient を書くことができました:
httpclientタイムアウト設定の一部
CONNECTION_TIMEOUTは接続タイムアウト、SO_TIMEOUTはソケットタイムアウトで、それぞれ異なります。コネクションタイムアウトはリクエストを開始するまでの待ち時間で、ソケットタイムアウトはデータ待ちのタイムアウトです。
HttpParams params = new BasicHttpParams();
//接続タイムアウトを設定する
Integer CONNECTION_TIMEOUT = 2 * 1000; //リクエストタイムアウトを2秒に設定する 業務に応じて調整する
Integer SO_TIMEOUT = 2 * 1000; //データ待ちのタイムアウトを2秒に設定する。
//ClientConnectionManagerからManagedClientConnectionインスタンスを取得する際に使用するミリ秒単位のタイムアウトを定義する。
//このパラメータはjavaの.lang.Longtypeの値。このパラメータが設定されていない場合、デフォルトはCONNECTIONに等しい。_TIMEOUT,なので、必ず設定しておくこと。
Long CONN_MANAGER_TIMEOUT = 500L; //httpclient4では.2.3オブジェクトに変更され、ロングを直接使用するとエラーになったと記憶しているが、その後元に戻された。
params.setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, CONNECTION_TIMEOUT);
params.setIntParameter(CoreConnectionPNames.SO_TIMEOUT, SO_TIMEOUT);
params.setLongParameter(ClientPNames.CONN_MANAGER_TIMEOUT, CONN_MANAGER_TIMEOUT);
//リクエストを送信して、接続の可用性をテストする。
params.setBooleanParameter(CoreConnectionPNames.STALE_CONNECTION_CHECK, true);
//さらに、httpクライアントの再試行回数を設定する。デフォルトは3回で、現在は無効になっている。
httpClient.setHttpRequestRetryHandler(new DefaultHttpRequestRetryHandler(0, false));
nginxが設定されている場合、nginxも両端に対してkeep-aliveを設定する必要があります。
nginxはデフォルトでクライアント側には長いコネクションを開き、サーバ側には短いリンクを使用します。クライアント側のkeepalive_timeoutパラメータとkeepalive_requestsパラメータ、上流側のkeepaliveパラメータに注意してください。