TLSセッション再利用
はじめに
低遅延アプリケーションでは、ミリ秒が重要です。APIリクエストを行う際の不必要な遅延の一般的な原因の一つは、繰り返されるTLSハンドシェイクプロセスです。アプリケーションがTLSセッションを再利用しない場合、新しい接続ごとに追加の遅延と計算オーバーヘッドが発生します。
これを軽減するために、TLSセッションの再利用と接続プーリングの実装を強く推奨します。このガイドでは、これらの概念を説明し、Java、Python、Node.jsの例を提供します。
TLSセッション再利用の理解
TLSセッション再利用がない場合はどうなるか?
- クライアントがAPIへの接続を開始します。
- 完全なTLSハンドシェイクが行われ、複数の往復が必要です。
- ハンドシェイクは安全な接続を確立しますが、再利用されない場合、次の接続のために新しいハンドシェイクが必要になります。
TLSセッション再利用がどのように役立つか
- ハンドシェイクの遅延を削減: 既存のTLSセッションを再利用することで、クライアントはコストのかかるハンドシェイクプロセスを繰り返すことを避けます。
- CPUとメモリの使用量を低下させる: 各ハンドシェイクはリソースを消費する暗号操作を必要とします。
- リクエストスループットを改善: 永続的な接続は新しいTLSセッションの設定にかかるオーバーヘッドを減少させます。
これを達成するには、接続プーリングを使用し、HTTPクライアントで永続的な接続を有効にします。
Curlを使用したTLSハンドシェイク時間のデバッグ
APIリクエストのTLSハンドシェイク時間を確認するには、次のコマンドを使用します:
curl -o /dev/null -s -w "
DNS Lookup: %{time_namelookup}s
TCP Connect: %{time_connect}s
TLS Handshake: %{time_appconnect}s
Pre-Transfer: %{time_pretransfer}s
Start Transfer: %{time_starttransfer}s
Total Time: %{time_total}s
" https://api.forter-secure.com
これにより、次のような出力が得られます:
DNS Lookup: 0.388898s
TCP Connect: 0.529150s
TLS Handshake: 0.975320s
Pre-Transfer: 0.975733s
Start Transfer: 1.119405s
Total Time: 1.120284s
TLSハンドシェイクは、TLSハンドシェイクが行われるのにかかった時間を示します。上記のリクエストでは、約450ミリ秒かかりました。もしTLSハンドシェイクが一貫して高い場合、クライアントがTLSセッションを再利用していない可能性があります。
TLSセッションの再利用の実装
Javaの例(Apache HttpClientを使用)
package org.forter.tlsreuseexample;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.util.TimeValue;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.HexFormat;
public class Main {
public static void main(String[] args) throws Exception {
HttpGet request = new HttpGet("https://api.forter-secure.com");
var clientContext = HttpClientContext.create();
System.out.println("====== Using 0 keep alive w/o connection reuse configuration ========");
try (CloseableHttpClient client = HttpClients.custom()
.setConnectionReuseStrategy((request1, response, context) -> false)
.setKeepAliveStrategy((response, context) -> TimeValue.ofMilliseconds(0))
.build()) {
executeRequest(request, client, clientContext);
}
System.out.println("====== Using regular pool configuration ========");
try (CloseableHttpClient client = HttpClients.createDefault()) {
executeRequest(request, client, clientContext);
}
}
private static void executeRequest(HttpGet request, CloseableHttpClient client, HttpClientContext clientContext) throws URISyntaxException, IOException, InterruptedException {
System.out.println("Sending request to " + request.getUri());
client.execute(request, clientContext, response -> consumeResponse(response, clientContext));
Thread.sleep(100);
System.out.println("=====================\n");
System.out.println("Sending request to " + request.getUri());
client.execute(request, clientContext, response -> consumeResponse(response, clientContext));
System.out.println("=====================\n");
}
private static Object consumeResponse(ClassicHttpResponse response, HttpClientContext clientContext) throws IOException {
EntityUtils.consume(response.getEntity());
var sslSession = clientContext.getSSLSession();
sslSession.invalidate();
String sessionIdHex = HexFormat.of().formatHex(sslSession.getId());
System.out.println("SSL Session Id: " + sessionIdHex);
response.close();
return null;
}
}
- 接続プーリングを使用して、持続的な接続を維持します。
- TLSセッションを再利用して、ハンドシェイクのオーバーヘッドを最小限に抑えます。
上記のプログラムの出力はこのようになります。デフォルトのプール構成が使用されている2回目の実行では、SSLセッションIDが同じであることがわかります。これはkeepAliveと接続の再利用を使用しています。
====== Using 0 keep alive w/o connection reuse configuration ========
Sending request to https://api.forter-secure.com/
SSL Session Id: 7f60fd646dd1239a9210ad844c4a3e81317e3780dc9b39aaeffd3160c69d99c6
=====================
Sending request to https://api.forter-secure.com/
SSL Session Id: 8df20ed84d6128dd560ccc4abbcd58fb1185198fcae5e23b979d57cba64ca92e
=====================
====== Using regular pool configuration ========
Sending request to https://api.forter-secure.com/
SSL Session Id: 48ab1d57adb15c810cac201d9576475b29bb0f8875838c1de0c94e53e740fe6f
=====================
Sending request to https://api.forter-secure.com/
SSL Session Id: 48ab1d57adb15c810cac201d9576475b29bb0f8875838c1de0c94e53e740fe6f
=====================
Pythonの例(Requestsセッションを使用)
PythonのRequestsライブラリの「Session」を使用する際は、自動的にKeepAliveを行い、http接続を再利用します。
import requests
session = requests.Session()
session.get("https://api.forter-secure.com")
# TLS session reuse will happen here
session.get("https://api.forter-secure.com")
Node.jsの例(HTTPSエージェントを使用)
const https = require("https");
const agent = new https.Agent({
keepAlive: true, // Enables connection reuse
maxSockets: 10, // Limits parallel connections
maxFreeSockets: 5,
});
https.get("https://api.forter-secure.com", { agent }, (res) => {
console.log(`Status Code: ${res.statusCode}`);
});
// TLS session reuse will happen here as the agent is reused
https.get("https://api.forter-secure.com", { agent }, (res) => {
console.log(`Status Code: ${res.statusCode}`);
});
- TLSセッションをリクエスト間で再利用するためにkeepAliveを有効にします。
- リソース消費を効果的に管理するためにオープンソケットを制限します。
結論
TLSセッションの再利用と接続プーリングを実装することで、ハンドシェイクのレイテンシを削減し、リソースの使用を最適化することにより、APIのパフォーマンスが大幅に向上します。提供された例を使用して、アプリケーションが接続を効率的に再利用し、不必要なオーバーヘッドを最小限に抑えることを確認してください。