【実務・中級編】 HTTP/2のヘッダー圧縮(HPACK) – ネットワーク基礎とWebセキュリティ実践ガイド

こんにちは、シニアネットワークエンジニアの私だ。これまで数々のWebアプリケーションのパフォーマンスチューニングや、深夜の障害対応をくぐり抜けてきた。

APIのレスポンスタイムを削るために、データベースのインデックスを見直したり、Redisでキャッシュ層を厚くしたり……そんな努力を重ねているエンジニアは多い。だが、ちょっと待ってほしい。君たちが毎日何気なく叩いているそのAPI、HTTP/2の「ヘッダー圧縮(HPACK)」の仕組みを意識したことはあるだろうか?

「ブラウザが勝手にやってくれるやつでしょ?」なんて思っているなら、インフラ・バックエンドエンジニアとしての引き出しが少しもったいない。Web APIの設計や、マイクロサービス間の通信でこの挙動を知っているかいないかで、高負荷時のネットワーク帯域の削られ方や、レイテンシーの詰まり方が劇的に変わってくる。

今回は、パケットの裏側でHPACKがどのように動き、どうやって帯域を節約しているのか、現場の泥臭い知見を交えながら徹底的に解説しよう。

—

なぜHTTP/2のヘッダー圧縮(HPACK)が必要だったのか?

HTTP/1.1の時代を思い出してほしい。ブラウザが画像を読み込むために、あるいはSPA(Single Page Application)が細かなAPIリクエストを大量に投げるたびに、私たちは毎回数キロバイトにも及ぶHTTPヘッダーをテキストのままワイヤー(回線)に流し込んでいた。

Cookie、User-Agent、Accept-Language、Authorizationトークン……。これらはリクエストごとにほとんど変化しないにもかかわらず、毎回転送されている。特に、昨今のモダンなWebアプリケーションやAPIでは、セキュリティ要件やセッション管理の肥大化に伴い、ヘッダーサイズがペイロード(ボディ)のサイズを優に超えるなんて本末転倒な現象が日常茶飯事だった。

「この無駄なテキストの重複送信をどうにかしよう」と立ち上がったのが、HTTP/2におけるヘッダー圧縮、すなわち HPACK(RFC 7541) である。

—

HPACKのメカニズム:静的テーブルと動的テーブルの連携

HPACKの本質は、一言で言えば「あらかじめ定義された辞書、および通信中に学習する辞書を使って、文字列を小さなインデックス番号(整数)に置き換えて送る」ことだ。

パケットのキャプチャ(Wiresharkなど)を覗いたことがある人なら分かると思うが、HTTP/2のバイナリフレーム内では、人間が読める content-type: application/json のような文字列は、巧みにバイナリのインデックスにエンコードされている。

HPACKが持つ辞書は、大きく分けて2種類ある。

1. 静的テーブル(Static Table)

RFC 7541の仕様書(Appendix A)で厳格に定義された、全HTTP/2実装が共通で持っている不変の辞書だ。
例えば、以下のようなよく使われるヘッダーフィールドとインデックスが1対1で紐づけられている。

  • インデックス 2: :method: GET
  • インデックス 4: :path: /
  • インデックス 32: cookie
  • インデックス 38: user-agent

これらは固定なので、クライアントとサーバーの間でわざわざ「こういう意味だよ」と事前にやり取りする必要すらない。インデックス番号を指定するだけで、瞬時に元の文字列へ復元できる。

2. 動的テーブル(Dynamic Table)

ここがHPACKの最もクールな部分だ。静的テーブルに載っていないカスタムヘッダーや、リクエストごとに変わるが連続して同じ値が使われるもの(例えば authorization: Bearer eyJhbGciOi... や x-request-id: uuid-... など)を、通信セッション(コネクション)ごとに動的に蓄積していく辞書である。

1. クライアントが初めて独自のカスタムヘッダーを送信する。
2. サーバー側でそれをデコードし、動的テーブルに追加する。
3. 次回のリクエストからは、そのカスタムヘッダーをフル文字列ではなく「動的テーブルの何番目」という短いデータとして送信する。

これにより、同じセッション内での通信が続けば続くほど、ヘッダーサイズは極限まで小さくなっていく。

—

実務で知るべき通信フローとオーバーヘッドの罠

「じゃあ、無限に動的テーブルを大きくすれば最強じゃないか」と思うかもしれないが、ここにインフラエンジニアの腕の見せ所、すなわちトレードオフの罠がある。

動的テーブルのサイズ制御

動的テーブルは、メモリ(RAM)を消費する。サーバー側もクライアント側も、接続ごとにテーブルを保持しなければならない。そのため、HPACKでは SETTINGS_HEADER_TABLE_SIZE というHTTP/2のSETTINGSフレームを使い、動的テーブルの最大サイズをネゴシエーションする。

もしリバースプロキシ(NginxやEnvoyなど)やAPI Gatewayのチューニングを怠ると、大量の同時接続(Concurrent Streams)が来た際にメモリを圧迫し、OOM(Out of Memory)Killerの餌食になる。高負荷環境を扱うインフラエンジニアなら、このパラメータのデフォルト値を疑う習慣を持っておくべきだ。

リモート・ハフマン符号化(Huffman Coding)

HPACKは、インデックス化できない新しい文字列を送る際にも、ハフマン符号化というアルゴリズムで圧縮をかける。頻出する文字(アルファベットの e や a など)には短いビット列を割り当て、出現頻度の低い文字には長いビット列を割り当てることで、生のASCII文字列よりもデータ量を削り取る。

—

実践:PythonによるHTTP/2通信とヘッダーの確認

理論はこのあたりにして、実際にコードを書いてその挙動を確かめてみよう。
Pythonの httpx ライブラリは、標準でHTTP/2をサポートしている優秀なクライアントだ。これを使って、実際にHTTP/2でリクエストを投げるコードを見てみよう。

import httpx

# HTTP/2をサポートするエンドポイントに対してクライアントを初期化
# httpxはデフォルトでHTTP/2を有効化できる(h2ライブラリが必要)
with httpx.Client(http2=True) as client:
    
    # カスタムヘッダーを付与してリクエストを送信
    headers = {
        "x-api-version": "v2",
        "x-custom-track-id": "trace-99887766",
        "user-agent": "NetworkEngineer-Debug-Client/1.0"
    }
    
    response = client.get("https://httpbin.org/headers", headers=headers)
    
    print(f"使用されたプロトコル: {response.http_version}")
    print(f"レスポンスステータス: {response.status_code}")
    print("--- レスポンスボディ ---")
    print(response.text)

このスクリプトを実行すると、HTTP/2 (HTTP/2.0) で通信が行われる。
ここで注目してほしいのは、x-custom-track-id のようなカスタムヘッダーだ。最初のリクエストではフル文字列として流れるが、同じコネクション上で連続してリクエストを送る場合、HPACKの動的テーブルにこれが登録され、2回目以降のパケットサイズが劇的に小さくなる。

—

トラブルシューティング:HPACK起因の「見えない障害」

現場で実際に遭遇したトラブルの共有をしておこう。
ある日、マイクロサービス間で「突発的にレスポンスが極端に遅くなる、あるいは接続が切断される」という現象が発生した。原因を追っていくと、なんと「HTTP/2の圧縮コンテキストの破損(HPACK Compression Error)」だった。

障害の背景

プロキシサーバー(Envoy)とバックエンドのAPIサーバーの間で、ロードバランサー(ALBなど)のタイムアウトやコネクションプールの切断タイミングがズレていた。
HPACKの動的テーブルは、クライアントとサーバーの間で完全に同期されていることが前提のステートフルな仕組みだ。片方がコネクションをリセットしたり、動的テーブルのサイズ変更を見誤ったりしてインデックスのズレ(Desync)が生じると、デコード側で復元ができなくなる。

結果として、HTTP/2のコネクション全体がエラー(COMPRESSION_ERROR)として強制切断され、クライアント側でリトライの嵐が起きたのだ。

現場のTips

もしパケット解析ツール(Wiresharkやtcpdump)で RST_STREAM フレームや GOAWAY フレームが頻発し、エラーコードに COMPRESSION_ERROR が出ていたら、アプリケーションのバグを疑う前に以下をチェックしてほしい。

1. プロキシとバックエンド間の Keep-Alive 設定
コネクションが意図せず途中で切断されていないか。
2. ヘッダーテーブルサイズの上限設定
クライアント側とサーバー側で SETTINGS_HEADER_TABLE_SIZE の値が極端に乖離していないか。
3. 巨大なCookieやヘッダーの乱用
動的テーブルの許容量を超えるような巨大なカスタムヘッダーを毎回送りつけていないか(これによりテーブルが頻繁にフラッシュされ、圧縮効率が落ちるだけでなくオーバーヘッドになる)。

—

まとめ

HTTP/2のヘッダー圧縮(HPACK)は、単なる「パケットを小さくする便利な技術」ではない。
静的テーブルと動的テーブルというステートフルな仕組みを持つがゆえに、ネットワークのレイヤー、プロキシの挙動、そしてアプリケーション層のヘッダー設計までが密接に絡み合う、インフラエンジニアの基礎教養とも言える領域だ。

Web APIの設計で「とりあえず何でもかんでもカスタムヘッダーに詰めて送る」という設計をしているなら、ちょっと待ったと言いたい。その設計は、本当にHPACKの動的テーブルの恩恵を受けられているだろうか? あるいは、逆にメモリやコネクションの維持に悪影響を与えていないだろうか?

パケットが光の速さでルーターを駆け抜け、カーネルのネットワークスタックを通り、アプリケーション層でデコードされる――その一連の流れを頭の中でイメージできるようになれば、君も立派なネットワーク・セキュリティスペシャリストだ。日々のインフラ運用やAPI設計に、ぜひこの視点を取り入れてみてほしい。

コメント

タイトルとURLをコピーしました