はじめに:ブラウザの裏側で何が起きているか、その「本当のレイテンシ」を見極めろ
Web APIの設計や、高スループットが求められるインフラのチューニングに携わっていると、必ず直面するのが「レイテンシ(遅延)の壁」です。
「なぜか初回リクエストだけがやたらと遅い」
「地理的に離れたリージョンへのAPI呼び出しで、どうしても数百ミリ秒のオーバーヘッドが削れない」
こうした現場のトラブルシューティングにおいて、アプリ側のコードばかりをプロファイリングしても泥沼にハマるだけです。パケットキャプチャを開き、TCPの3-way handshake(3ウェイハンドシェイク)から、その直後に始まるTLS(Transport Layer Security)ハンドシェイク、そしてアプリケーション層のHTTPリクエストに至るまでの「正確なシーケンス」を解剖できなければ、真のパフォーマンスチューニングなど語れません。
今回は、TCPとTLSが織りなす接続プロセスの全貌を、パケットの動きそのものを可視化しながら、実務で役立つTipsとともに徹底的に紐解いていきます。
—
1. TCP 3-way HandshakeとTLSハンドシェイクの全体像
私たちが普段何気なく叩いている https://api.example.com/v1/data というURL。この背後では、アプリケーション層のデータ(HTTPリクエスト)が流れる前に、OSのカーネルとネットワークスタックが激しい「儀式」を行っています。
全体のシーケンスは、大きく分けて2つのフェーズで構成されています。
1. TCP Connection Establishment (3-way Handshake): 信頼性のあるトランスポート層の確立
2. TLS Handshake: 暗号化されたセキュアなセッションの確立(TLS 1.3を基準)
まずは、この2つがどのように連続して実行されるのか、全体のシーケンス図を見てみましょう。
Client (Browser / App) Server (API Gateway)
| |
| -------- [1] SYN -------------> | <-- TCP 3-way Handshake開始
| <------- [2] SYN-ACK ---------- |
| -------- [3] ACK (TCP確立) ---> | <-- ここでTCPコネクション確立
| |
| -------- [4] Client Hello ----> | <-- TLSハンドシェイク開始 (TLS 1.3)
| (Key Share, Ciphers) |
| |
| <------- [5] Server Hello ----- |
| (Key Share, Certificate) | <-- サーバー側で暗号鍵生成完了
| [Change Cipher Spec] |
| [Finished] |
| |
| <------- [6] Finished --------- | <-- クライアント側で検証完了・暗号化通信可能
| |
| -------- [7] Encrypted HTTP Req -> | <-- アプリケーションデータ送信開始
現場のエンジニアとして特に意識してほしいのは、「TCPが完全に繋がるまで、一文字たりともTLSのデータは送れない」という物理的(かつ論理的)な制約です。
—
2. 各ハンドシェイクフェーズの詳細とパケットの挙動
フェーズ1:TCP 3-way Handshake(確実な道づくり)
信頼性のある通信路を確保するため、お互いの生存確認とシーケンス番号の同期を行います。
SYN: クライアントが「通信を始めたいです。初期シーケンス番号はこれです」と通知。SYN-ACK: サーバーが「了解です。こちら側の初期シーケンス番号はこれ、あなたのSYNも受け取りました」と返答。ACK: クライアントが「サーバーからのSYN-ACKを受け取りました」と返答し、これにてTCPコネクションが確立(ESTABLISHED状態)します。
この3つのパケットが往復するだけで、物理的な距離に応じた1往復半(1.5 RTT: Round Trip Time)の遅延が確実に発生します。
フェーズ2:TLSハンドシェイク(鍵の共有と身元証明)
TCPが確立した瞬間、TLSのネゴシエーションが始まります。現代のデファクトスタンダードである TLS 1.3 では、このプロセスが劇的に洗練され、高速化されています。
従来(TLS 1.2以前)は鍵交換アルゴリズムのネゴシエーションに余分な往復が必要でしたが、TLS 1.3ではクライアント側の ClientHello に予測可能な暗号スイートや鍵共有(Key Share)の候補を最初から含めることで、ハンドシェイクのラウンドトリップを最小限に抑えています。
ClientHello:
クライアントがサポートするTLSバージョン、暗号スイート(暗号化アルゴリズムの組み合わせ)、そして自身の公開鍵パラメータ(key_share)を送信します。「私はこの暗号化手法が使えます。鍵の種はこれです」と最初に手を挙げるわけです。
ServerHello/ 署名・証明書 /Finished:
サーバーは ClientHello を受け取ると、自身の証明書(Certificate)、サーバー側の公開鍵パラメータ、そして選択した暗号スイートを返します。サーバーはこの時点で共通鍵の導出が可能なため、自身の Finished メッセージまでを一気に送信します。
Finished(Client):
クライアントはサーバーからのパラメータを受け取り、共通鍵を生成してハンドシェイクの完了を告げます。
これによって、TLS 1.3では1 RTTでセキュアなハンドシェイクが完了します。TCPの1.5 RTTと合わせると、実際にアプリケーションデータが流れるまでに最低でも 2.5 RTT のレイテンシが必ず発生するという計算になります。
—
3. 実務で直面するレイテンシ要因とパフォーマンス・チューニング
インフラ設計やAPI開発において、「なぜかレイテンシが落ちない」というボトルネックの多くは、このハンドシェイクの回数とネットワークの物理的距離に起因します。現場で使える具体的なチューニング手法を見ていきましょう。
① TLS 1.3の強制と古いプロトコルの排除
いまだに TLS 1.2 をフォールバックとして残しているシステムを見かけますが、TLS 1.2のフルハンドシェイクは 2 RTT かかります。現代のインフラストラクチャにおいては、クライアントとサーバーの両方で TLS 1.3 を強制、あるいは優先させる設定が必須です。
NginxでTLS 1.3のみを許可し、不要なオーバヘッドを削ぎ落とす設定例は以下の通りです。
server {
listen 443 ssl http2;
server_name api.example.com;
# 古い脆弱なプロトコルを完全に排除し、TLS 1.3のみを強要する
ssl_protocols TLSv1.3;
# パフォーマンスに優れたモダンな暗号スイートを指定
ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:DHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers on;
# セッション再開(Session Resumption)を有効化し、2回目以降のハンドシェイクをゼロRttに近づける
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1h;
# 乱数生成の最適化
ssl_buffer_size 4k; # パケットのフラグメント化を防ぎ、初速のレスポンスを改善する
location / {
proxy_pass http://backend_upstream;
}
}
② 0-RTT Resumption(早期データ送信)の検討
TLS 1.3には、一度接続したことのあるクライアントに対して、2回目以降の接続時にハンドシェイクを待たずにアプリケーションデータを送り始める 0-RTT Resumption という強力な機能があります。
ただし、リプレイアタック(悪意ある攻撃者が過去のパケットを再送する攻撃)に対して脆弱性を持つため、冪等性(Idempotency)が保証されていない POST リクエストなどでの利用には細心の注意が必要です。GETメソッド中心の静的なAPIやCDNエッジでの終端処理においては、極めて有効な選択肢となります。
—
4. デバッグと実検証:コマンドラインからハンドシェイクを暴く
理屈はわかりましたが、実際に目の前のサーバーがどのようなハンドシェイクを行っているのか、自分の手でパケットを覗いてみましょう。実務の現場で即座に使えるデバッグコマンドを紹介します。
curlでハンドシェイクの詳細とTLSバージョンを暴く
APIのレスポンスタイムだけでなく、どのプロトコルで接続が確立されたのかを正確に計測するには、curl のフォーマット機能が重宝します。
# TLSのバージョン、ハンドシェイクにかかった時間、証明書の検証結果を詳細に確認する
curl -Iv https://api.ipify.org \
--write-out "\n--- 統計情報 ---\nTCP Connection Time: %{time_connect}s\nTLS Handshake Time: %{time_appconnect}s\nTotal Time: %{time_total}s\n" \
-o /dev/null
このコマンドを実行すると、次のような出力が得られます。
* Trying 1.2.3.4:443...
* Connected to api.ipify.org (1.2.3.4) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
* CAfile: /etc/ssl/cert.pem
* CApath: none
* TLSv1.3 (OUT), TLS handshake, Client Hello (1):
* TLSv1.3 (IN), TLS handshake, Server Hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
...
--- 統計情報 ---
TCP Connection Time: 0.045s
TLS Handshake Time: 0.082s
Total Time: 0.120s
time_connect がTCP確立までの時間、time_appconnect がTLSハンドシェイク完了までの時間です。この数値を見るだけで、インフラの物理的な遅延(ネットワーク起因)なのか、サーバー側の処理遅延なのかが一発で切り分けられます。
Python (requests / urllib3) での接続挙動の確認
アプリケーションコード側からTLSの挙動を厳密に制御したい場合、例えばカスタムのCA証明書や特定のTLSバージョンを強制したい場合は、次のように記述します。
import ssl
import requests
from urllib3.poolmanager import PoolManager
from urllib3.util import ssl_
# TLS 1.3を強制し、セキュアな通信を行うためのカスタムAdapterクラス
class TLS13Adapter(requests.adapters.HTTPAdapter):
def init_poolmanager(self, connections, maxsize, block=False, **kwargs):
# 明示的にTLSコンテキストを作成し、最小プロトコルバージョンをTLS 1.3に固定
context = ssl_.create_urllib3_context(
minimum_version=ssl.TLSVersion.TLSv1_3
)
kwargs['ssl_context'] = context
return super().init_poolmanager(connections, maxsize, block, **kwargs)
# セッションにカスタムアダプターをバインド
session = requests.Session()
session.mount("https://", TLS13Adapter())
try:
# APIリクエストの実行
response = session.get("https://api.ipify.org?format=json", timeout=5.0)
print(f"ステータスコード: {response.status_code}")
print(f"レスポンスボディ: {response.json()}")
except requests.exceptions.SSLError as e:
print(f"TLSハンドシェイクエラーが発生しました: {e}")
except requests.exceptions.RequestException as e:
print(f"通信エラーが発生しました: {e}")
このコードでは、Pythonの標準ライブラリである ssl モジュールをラップし、urllib3 のレイヤーで強制的に TLS 1.3 を使わせることで、レガシーな暗号化通信の混入をアプリ層からブロックしています。セキュリティ要件が厳しい金融系やヘルスケア系のAPIクライアントを実装する際には、こうした明示的なバージョン制御がエンジニアの武器となります。
—
おわりに
HTTPS通信の裏側で行われているTCP 3-way handshakeとTLSハンドシェイクは、一見すると「ブラックボックスな暗号化の儀式」に見えるかもしれません。しかし、パケットレベルの挙動を分解し、それぞれのステップがどのような理由で存在し、どれだけのレイテンシを消費しているのかを把握していれば、インフラの設計ミスやパフォーマンス低下の兆候を誰よりも早く察知できるようになります。
「なぜ遅いのか」と悩んだときは、まずパケットキャプチャを開き、あるいは curl の時間計測機能を使い、TCPとTLSの境界線に目を向けてみてください。そこには、ネットワークの真実が必ず隠されています。
コメント