今回は OSS のリバースプロキシである HAProxy 1 を使って HTTP/2 を試してみる。 HTTP/2 は HTTP/1.1 に存在するパフォーマンス面の課題を解決することを目的に生み出された。 Google が開発していた SPDY というプロトコルがベースになっている。
HTTP/1.1 では、HTTP/1.0 からの改良点として 1 つの TCP コネクション上で複数のリクエスト・レスポンスをやり取りできるようになった。 とはいえ、複数のリクエスト・レスポンスはあくまで順番に処理する必要がある。 そのため、重いコンテンツがあると後続の処理が待たされてしまう問題 (HTTP Head-of-Line Blocking) があった。 シンプルに解決する方法としてはサーバに接続する TCP のコネクションを増やすことが考えられる。 しかし、あまり増やすとサーバ側の負荷につながる。 そこで、HTTP/2 ではプロトコルをバイナリ化した上で、ストリームという単位で通信を多重化できるようになっている。 それぞれのストリームでは、フレームという単位でメッセージがやり取りされる。
ただし、HTTP/2 の登場によって HTTP/1.1 が不要になったかというと、そうではない 2。 あくまで効率を改善するためのプロトコルなので、必ず対応しなければいけないわけでもない。 また、システム全体が HTTP/2 に対応している必要は必ずしもない。 たとえば前段のリバースプロキシや Web サーバが HTTP/2 を喋って、バックエンドとは HTTP/1.1 で通信するパターンが考えられる。 今回試す構成も、このパターンになっている。
使った環境は次のとおり。
$ cat /etc/lsb-release DISTRIB_ID=Ubuntu DISTRIB_RELEASE=22.04 DISTRIB_CODENAME=jammy DISTRIB_DESCRIPTION="Ubuntu 22.04.3 LTS" $ uname -srm Linux 5.15.0-87-generic aarch64 $ haproxy -v HAProxy version 2.4.22-0ubuntu0.22.04.2 2023/08/14 - https://haproxy.org/ Status: long-term supported branch - will stop receiving fixes around Q2 2026. Known bugs: http://www.haproxy.org/bugs/bugs-2.4.22.html Running on: Linux 5.15.0-88-generic #98-Ubuntu SMP Mon Oct 2 15:18:56 UTC 2023 x86_64 $ curl -V curl 7.81.0 (x86_64-pc-linux-gnu) libcurl/7.81.0 OpenSSL/3.0.2 zlib/1.2.11 brotli/1.0.9 zstd/1.4.8 libidn2/2.3.2 libpsl/0.21.0 (+libidn2/2.3.2) libssh/0.9.6/openssl/zlib nghttp2/1.43.0 librtmp/2.3 OpenLDAP/2.5.16 Release-Date: 2022-01-05 Protocols: dict file ftp ftps gopher gophers http https imap imaps ldap ldaps mqtt pop3 pop3s rtmp rtsp scp sftp smb smbs smtp smtps telnet tftp Features: alt-svc AsynchDNS brotli GSS-API HSTS HTTP2 HTTPS-proxy IDN IPv6 Kerberos Largefile libz NTLM NTLM_WB PSL SPNEGO SSL TLS-SRP UnixSockets zstd $ openssl version OpenSSL 3.0.2 15 Mar 2022 (Library: OpenSSL 3.0.2 15 Mar 2022) $ tshark --version TShark (Wireshark) 3.6.2 (Git v3.6.2 packaged as 3.6.2-2)
もくじ
下準備
まずは下準備として、利用するパッケージをインストールする。
$ sudo apt-get -y install haproxy curl python3 iproute2 tshark
ネットワークを作成する
次に Network Namespace を使って仮想的なネットワークを作成していく。
まずは HTTP クライアント、リバースプロキシ、Web サーバに対応する Network Namespace を作成する。
$ sudo ip netns add client $ sudo ip netns add proxy $ sudo ip netns add server
それぞれの Network Namespace をつなぐための Virtual Ethernet デバイスのインターフェイスを作成する。
$ sudo ip link add client-veth0 type veth peer name proxy-veth0 $ sudo ip link add server-veth0 type veth peer name proxy-veth1
作成したインターフェイスを Network Namespace に所属させる。
$ sudo ip link set client-veth0 netns client $ sudo ip link set proxy-veth0 netns proxy $ sudo ip link set proxy-veth1 netns proxy $ sudo ip link set server-veth0 netns server
インターフェイスが利用できるように状態を UP に設定する。
$ sudo ip netns exec client ip link set client-veth0 up $ sudo ip netns exec proxy ip link set proxy-veth0 up $ sudo ip netns exec proxy ip link set proxy-veth1 up $ sudo ip netns exec server ip link set server-veth0 up
また、Web サーバに対応する Network Namespace の server
についてはループバックインターフェースの状態も UP に設定する。
$ sudo ip netns exec server ip link set lo up
最後に、各インターフェイスに IP アドレスを付与する。
$ sudo ip netns exec client ip address add 192.0.2.1/24 dev client-veth0 $ sudo ip netns exec proxy ip address add 192.0.2.254/24 dev proxy-veth0 $ sudo ip netns exec proxy ip address add 198.51.100.254/24 dev proxy-veth1 $ sudo ip netns exec server ip address add 198.51.100.1/24 dev server-veth0
なお、HAProxy は L7 で動作するリバースプロキシなので、L3 のルーティングを有効にする (net.ipv4.ip_forward=1
) 必要はない。
Web サーバ (Python) をセットアップする
今回は HAProxy を使った HTTP/2 の検証が主題になる。 そのため、Web サーバについては簡単のために Python の簡易 Web サーバを利用する。
まずは HTTP でやり取りするコンテンツとして HTML ファイルを用意する。
$ mkdir -p /var/tmp/www $ cat << 'EOF' > /var/tmp/www/index.html <!doctype html> <html> <head> <title>Hello, World</title> </head> <body> <h1>Hello, World</h1> </body> </html> EOF
そして、次のようにして上記のコンテンツを配信する Web サーバを起動する。
$ sudo ip netns exec server python3 -m http.server -b 0.0.0.0 -d /var/tmp/www 80
ひとまず、別のターミナルを開いて curl(1) でアクセスできることを確認しておこう。
$ sudo ip netns exec server curl http://198.51.100.1 <!doctype html> <html> <head> <title>Hello, World</title> </head> <body> <h1>Hello, World</h1> </body> </html>
リバースプロキシ (HAProxy) をセットアップして HTTP/2 を試す
ここからは HAProxy を設定して、実際に HTTP/2 の通信を試していく。
なお、HTTP/2 の通信を開始する方法には、以下の 2 つがある。
- Starting HTTP/2 for "https" URIs
- Starting HTTP/2 with Prior Knowledge
前者の Starting HTTP/2 for "https" URIs は、通信に TLS (Transport Layer Security) を利用することが前提となる。 その上で、HTTP/1.1 と HTTP/2 のいずれを使うかを ALPN (Application-Layer Protocol Negotiation) を使って決定する。
後者の Starting HTTP/2 with Prior Knowledge は、事前にその Web サーバが HTTP/2 のサービスを提供していることを何らかの方法で知っていることが前提となる。 前提知識は必要となるものの、こちらであれば平文で通信をやり取りできる。
なお、過去には HTTP/1.1 のリクエストで Upgrade: h2c
というヘッダを使って HTTP/2 にプロトコルをアップグレードする方法も定義されていた 3。
しかし、このやり方は利用が広がらなかったことから現在は廃止されている。
また、HAProxy もこのやり方を実装していない。
Starting HTTP/2 with Prior Knowledge
まずは平文で通信の内容を確認できる Starting HTTP/2 with Prior Knowledge から試す。
はじめに HAProxy の設定ファイルを次のように用意する。
ポイントは frontend web_proxy
の bind *:80
で proto h2
としているところ。
これで、HAProxy が TCP の 80 番ポートで Starting HTTP/2 with Prior Knowledge なリクエストを受け付けられるようになる。
$ cat << 'EOF' > haproxy.cfg defaults mode http timeout client 1m timeout server 1m timeout connect 10s frontend web_proxy bind *:80 proto h2 default_backend web_servers backend web_servers server s1 198.51.100.1:80 EOF
上記の設定ファイルを使って HAProxy を実行する。
$ sudo ip netns exec proxy haproxy -f haproxy.cfg
この状態で Web クライアントからリバースプロキシにリクエストを送る。
curl(1) では --http2-prior-knowledge
というオプションをつけることで Starting HTTP/2 with Prior Knowledge に対応したリクエストを送ることができる。
$ sudo ip netns exec client curl --include --http2-prior-knowledge http://192.0.2.254 HTTP/2 200 server: SimpleHTTP/0.6 Python/3.10.12 date: Sun, 05 Nov 2023 07:48:57 GMT content-type: text/html content-length: 127 last-modified: Sun, 05 Nov 2023 07:40:23 GMT <!doctype html> <html> <head> <title>Hello, World</title> </head> <body> <h1>Hello, World</h1> </body> </html>
先頭に HTTP/2 200
とあるように、通信が HTTP/2 でやり取りされていることが確認できる。
実際のコンテンツを持っている Python の Web サーバには HTTP/1.1 でリクエストが送られている。 HAProxy は、あくまでアプリケーションレイヤーでのプロキシに過ぎない。
$ sudo ip netns exec server python3 -m http.server -b 0.0.0.0 -d /var/tmp/www 80 198.51.100.254 - - [05/Sun/2023 09:56:13] "GET / HTTP/1.1" 200 -
ちなみに、理屈の上では HTTP/2 (Starting HTTP/2 with Prior Knowledge) と HTTP/1.1 を同じエンドポイントで提供できる。 しかし、HAProxy ではその機能を実装していないため、オプションをつけずにリクエストしてもレスポンスは得られない。
$ sudo ip netns exec client curl --include http://192.0.2.254 curl: (52) Empty reply from server
先に述べたとおり Starting HTTP/2 with Prior Knowledge では平文で通信の内容を観察できる。
試しに tshark(1) を使って通信の内容を確認してみよう。
tshark(1) は -Y http2
オプションを使うことで、表示する内容を HTTP/2 に絞ることができる。
それぞれの行が HTTP/2 のフレームに対応している。
$ sudo ip netns exec proxy tshark -Y http2 -i proxy-veth0 ... 4 0.000122686 192.0.2.1 → 192.0.2.254 HTTP2 90 Magic 5 0.000129571 192.0.2.1 → 192.0.2.254 HTTP2 93 SETTINGS[0] 6 0.000132302 192.0.2.1 → 192.0.2.254 HTTP2 79 WINDOW_UPDATE[0] 8 0.000161449 192.0.2.1 → 192.0.2.254 HTTP2 103 HEADERS[1]: GET / 9 0.000169075 192.0.2.254 → 192.0.2.1 HTTP2 96 SETTINGS[0], SETTINGS[0] 11 0.000190964 192.0.2.1 → 192.0.2.254 HTTP2 75 SETTINGS[0] 12 0.000850319 192.0.2.254 → 192.0.2.1 HTTP2 321 HEADERS[1]: 200 OK, DATA[1] (text/html)
最初にクライアントから送られている Magic
は、これから始める通信が Starting HTTP/2 with Prior Knowledge であることを伝えている。
この通信はコネクションプリフェイス (Connection Preface) と呼ばれる。
4 0.000122686 192.0.2.1 → 192.0.2.254 HTTP2 90 Magic
具体的には PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n
という文字列に対応するバイト列を送っている。
先に述べたとおり、理屈の上では最初に送られてくるのがコネクションプリフェイスかどうかで HTTP/2 と HTTP/1.1 の処理を分岐できる。
しかし HAProxy では、この機能を実装していない。
次の SETTINGS
は、HTTP/2 で通信する上での様々なパラメータを設定したり確認応答 (ACK) を返すためのフレームである。
5 0.000129571 192.0.2.1 → 192.0.2.254 HTTP2 93 SETTINGS[0]
フレームの右端にある [0]
はストリームの番号 (識別子) を表している。
0 番のストリームは制御のために特別に利用される。
次の WINDOW_UPDATE
は HTTP/2 のフロー制御に用いられるフレームである。
データの受信者は、コネクション全体か、あるいは特定のストリームで受信できるデータ量を、このフレームで相手に通知する。
6 0.000132302 192.0.2.1 → 192.0.2.254 HTTP2 79 WINDOW_UPDATE[0]
上記はストリームの番号に 0 番が指定されているため、コネクション全体に対して通知している。
次の HEADERS
は HTTP/1.1 のリクエストに相当する。
HTTP/1.1 ではテキストでやり取りしていたメソッドやパスなどの情報を、HTTP/2 では TLV (Type-Length-Value) 形式のフィールドとして表現している。
8 0.000161449 192.0.2.1 → 192.0.2.254 HTTP2 103 HEADERS[1]: GET /
そして、最後の HEADERS
と DATA
は HTTP/1.1 のレスポンスに相当する。
HTTP のステータスコードや Content-Type などの情報を HEADERS
で返した上で、コンテンツを DATA
で送っている。
12 0.000850319 192.0.2.254 → 192.0.2.1 HTTP2 321 HEADERS[1]: 200 OK, DATA[1] (text/html)
Starting HTTP/2 for "https" URIs
続いては TLS が前提となって ALPN でプロトコルをネゴシエートする Starting HTTP/2 for "https" URIs を試す。
TLS を利用するためには証明書が必要になる。 そこで、まずは動作確認用の自己署名証明書を作成する。
まずは秘密鍵を作る。
$ openssl genpkey -algorithm ed25519 -out private.key
続いて証明書署名要求を作る。 サブジェクトの部分は適当。
$ openssl req -new -key private.key -out cert.csr -subj "/C=JP/ST=Tokyo/L=Chiyoda-ku/O=Example Corp./OU=Example Dept./CN=www.example.jp"
そして自己署名証明書を作る。
$ openssl x509 -signkey private.key -in cert.csr -req -days 365 -out cert.pem
HAProxy は証明書のフォーマットとして秘密鍵も必要とする。
そこで /var/tmp/haproxy/certs
以下に証明書と秘密鍵を連結したものを作成する。
あくまで動作確認用ではあるが、秘密鍵の取り扱いには注意することを示すためにディレクトリのパーミッションを 400
に変更している。
$ mkdir -p /var/tmp/haproxy/certs $ cat cert.pem private.key > /var/tmp/haproxy/certs/haproxy.pem $ sudo chmod -R 400 /var/tmp/haproxy/certs
HAProxy の設定ファイルを作成する。
ポイントは frontend web_proxy
で bind :443
に対して証明書の場所と alpn h2,http/1.1
を指定しているところ。
特に後者の alpn h2,http/1.1
は、ALPN を使って HTTP/2 と HTTP/1.1 をネゴシエートすることを示している。
$ cat << 'EOF' > haproxy.cfg global ssl-default-bind-options ssl-min-ver TLSv1.2 defaults mode http timeout client 1m timeout server 1m timeout connect 10s frontend web_proxy bind :443 ssl crt /var/tmp/haproxy/certs/haproxy.pem alpn h2,http/1.1 default_backend web_servers backend web_servers server s1 198.51.100.1:80 EOF
ssl-default-bind-options ssl-min-ver TLSv1.2
は、TLS 1.0 ~ 1.1 が 2021 年に RFC 8996 で非推奨となったことに対応して入れている。
設定ファイルを元に HAProxy を起動する。
$ sudo ip netns exec proxy haproxy -f haproxy.cfg
自己署名証明書なので --insecure
オプションをつけて curl(1) を実行する。
すると、HTTP/2 でリクエストが処理されることが確認できる。
$ sudo ip netns exec client curl --include --insecure https://192.0.2.254 HTTP/2 200 server: SimpleHTTP/0.6 Python/3.10.12 date: Sun, 05 Nov 2023 09:56:13 GMT content-type: text/html content-length: 127 last-modified: Sun, 05 Nov 2023 06:49:15 GMT <!doctype html> <html> <head> <title>Hello, World</title> </head> <body> <h1>Hello, World</h1> </body> </html>
上記は ALPN によって、使用するプロトコルが HTTP/2 と HTTP/1.x でネゴシエートされた結果である。
そのため、明示的にクライアント側で HTTP/1.x が使いたい旨を指定すれば、そのようにもなる。
たとえば curl(1) で --http1.1
オプションをつけてみよう。
$ sudo ip netns exec client curl --include --http1.1 --insecure https://192.0.2.254 HTTP/1.0 200 OK server: SimpleHTTP/0.6 Python/3.10.12 date: Sun, 05 Nov 2023 10:25:07 GMT content-type: text/html content-length: 127 last-modified: Sun, 05 Nov 2023 06:49:15 GMT connection: keep-alive <!doctype html> <html> <head> <title>Hello, World</title> </head> <body> <h1>Hello, World</h1> </body> </html>
上記から、HTTP/2 の代わりに HTTP/1.0 で結果が返ってくることが確認できる 4 。
まとめ
今回は Network Namespace で作成したネットワーク上で Haproxy を使って HTTP/2 を試してみた。
- HTTP/2 と HTTP/1.x はアプリケーションレイヤーのプロキシでプロトコルを変換できる
- HTTP/2 の通信を始めるには 2 つのやり方がある
- Starting HTTP/2 for "https" URIs
- クライアントは TLS の ALPN で HTTP/2 が利用したい旨を伝える
- Starting HTTP/2 with Prior Knowledge
- クライアントは Connection Preface と呼ばれるバイト列を送ることで HTTP/2 を利用する旨を伝える
- 通信を平文でやり取りできる
- Starting HTTP/2 for "https" URIs
参考文献
- https://www.haproxy.org/↩
- この点は HTTP/3 の登場でも変わらない↩
- https://www.rfc-editor.org/rfc/rfc7540.html#section-3.2↩
- HTTP/1.1 でないのは Python の簡易 Web サーバの実装に依存している↩