HTTP/3とQUICデバッグ実践:パケットキャプチャからパフォーマンス分析までの5つのプロダクションパターン

网络协议

なぜHTTP/3デバッグはHTTP/2より10倍難しいのか

HTTP/3はQUICプロトコル上でUDP経由で動作するため、過去20年間のTCPデバッグ経験はほぼ無効になります。TCPパケットはWiresharkで直接読め、HTTP/2ストリームはChrome DevToolsで一目瞭然——しかしQUICトラフィックは暗号化されており、パケットが読めません。HTTP/3のストリーム多重化はトランスポート層で行われるため、アプリケーション層ツールからは完全に見えません。

2026年、HTTP/3の世界的採用率は45%を超えています(Cloudflare Radarデータ)が、開発者が問題に遭遇した場合、「接続失敗」という4文字しか見えず、手の打ちようがないことがよくあります。本記事では、パケットキャプチャの復号からリアルタイム監視まで、プロダクションで検証された5つのデバッグパターンをまとめ、完全なHTTP/3デバッグツールチェーンの構築を支援します。

デバッグ次元 HTTP/2ツール HTTP/3ツール 難易度変化
パケットキャプチャ Wireshark(直接読み取り可能) Wireshark + SSLKEYLOG 鍵の復号が必要
プロトコルログ 標準なし qlog(標準化フォーマット) 新概念
クライアントデバッグ Chrome DevTools Chrome NetLog より低レイヤー
CLIデバッグ curl -v curl --http3 + 環境変数 コンパイルが必要
プロダクション監視 TCPメトリクス QUIC専用メトリクス メトリクス体系が異なる

Pattern 1:Wireshark QUICパケットキャプチャと復号

コア問題:QUICは完全に暗号化されている

QUICはTLS 1.3をプロトコルに直接統合しています。ヘッダーを含むすべてのフレームが暗号化されます。WiresharkでキャプチャしたQUICパケットは、UDPペイロードしか表示できず、内部のHTTP/3フレームを解析できません。QUICトラフィックを復号するには、TLSセッション鍵を取得する必要があります。

SSLKEYLOGFILEメカニズム

SSLKEYLOGFILEはTLSデバッグの標準メカニズムです。これをサポートするクライアント(Chrome、Firefox、curl、Go)は、TLSセッション鍵を指定されたファイルに書き込みます。Wiresharkはこのファイルを読み取ってトラフィックを復号します。

# SSLKEYLOGFILE環境変数の設定
export SSLKEYLOGFILE=/tmp/sslkeys.log

# curlでHTTP/3トラフィックをキャプチャ(鍵は自動書き込み)
curl --http3 https://example.com -v

# Chromeでアクセス(鍵は自動書き込み)
google-chrome --ssl-key-log-file=/tmp/sslkeys.log

# Firefoxでアクセス
export MOZ_LOG="ssl:5"
export SSLKEYLOGFILE=/tmp/sslkeys.log
firefox

Wireshark QUIC復号設定

# 1. Wiresharkキャプチャ開始(QUICトラフィックフィルタ)
# キャプチャフィルタ:
udp port 443

# 2. Wiresharkで鍵ログファイルを設定
# Edit -> Preferences -> Protocols -> TLS -> (Pre)-Master-Secret log filename
# 入力:/tmp/sslkeys.log

# 3. QUIC復号の成功を確認
# 復号前:QUICパケットは "Protected Payload, PKN: ..." と表示
# 復号後:HTTP/3フレームが表示される(HEADERS, DATA, SETTINGSなど)

tsharkコマンドラインパケット分析

# QUICトラフィックをキャプチャして復号
tshark -i eth0 -f "udp port 443" \
  -o "tls.keylog_file:/tmp/sslkeys.log" \
  -Y "quic" \
  -T fields \
  -e quic.packet_type \
  -e quic.frame_type \
  -e ip.src \
  -e ip.dst \
  -e quic.stream_id

# HTTP/3リクエストヘッダーの抽出
tshark -i eth0 -f "udp port 443" \
  -o "tls.keylog_file:/tmp/sslkeys.log" \
  -Y "http3" \
  -T fields \
  -e http3.header \
  -e http3.stream_id

# QUIC接続ハンドシェイク情報の分析
tshark -i eth0 -f "udp port 443" \
  -o "tls.keylog_file:/tmp/sslkeys.log" \
  -Y "quic.connection_id" \
  -T fields \
  -e quic.connection_id \
  -e quic.version

# QUICパケットロスと再送の分析
tshark -i eth0 -f "udp port 443" \
  -o "tls.keylog_file:/tmp/sslkeys.log" \
  -Y "quic.frame_type == 0x02 || quic.frame_type == 0x03" \
  -T fields \
  -e quic.frame_type \
  -e quic.ack_range_count \
  -e quic.ack_delay

Goサーバー鍵エクスポート

package main

import (
	"crypto/tls"
	"fmt"
	"log"
	"net/http"
	"os"
)

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Hello HTTP/3!")
	})

	tlsConfig := &tls.Config{
		Certificates: []tls.Certificate{loadCert()},
		NextProtos:   []string{"h3"},
		KeyLogWriter: keyLogWriter(),
	}

	server := &http.Server{
		Addr:      ":443",
		Handler:   mux,
		TLSConfig: tlsConfig,
	}

	log.Fatal(server.ListenAndServeTLS("", ""))
}

func keyLogWriter() tls.KeyLogWriter {
	f, err := os.OpenFile("/tmp/server-sslkeys.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600)
	if err != nil {
		log.Printf("Failed to open keylog file: %v", err)
		return nil
	}
	return f
}

func loadCert() tls.Certificate {
	cert, err := tls.LoadX509KeyPair("server.crt", "server.key")
	if err != nil {
		log.Fatal(err)
	}
	return cert
}

QUICバージョン識別

# 一般的なQUICバージョン番号
# RFC 9000 (QUIC v1):        0x00000001
# RFC 9369 (QUIC v2):        0x6b3343cf
# Google QUIC (gQUIC):       0x51303039 (Q039)
# IETF Draft 29:             0xff00001d

# tsharkで特定バージョンをフィルタ
tshark -i eth0 -f "udp port 443" \
  -Y "quic.version == 0x00000001" \
  -T fields \
  -e quic.connection_id \
  -e quic.version

Pattern 2:qlog分析と可視化

qlogとは

qlogはIETF標準化のQUIC/HTTP/3ログフォーマット(RFC 9657)です。統一されたイベントモデルを定義し、異なるQUIC実装間の相互運用性を可能にします。各社独自のログフォーマットとは異なり、qlogはquiche、lsquic、quic-go、ngtcp2など、異なる実装のログを同じツールで分析できます。

{
  "qlog_version": "0.3",
  "title": "QUIC Connection Debug Log",
  "description": "HTTP/3 connection from client to example.com",
  "trace": [
    {
      "vantage_point": {
        "type": "client"
      },
      "title": "Client trace",
      "event_fields": [
        "relative_time",
        "category",
        "event_type",
        "data"
      ],
      "events": [
        [0, "transport", "packet_sent", {
          "packet_type": "initial",
          "header": {
            "packet_number": 0,
            "version": "0x00000001",
            "scid": "0x8394c8f03e515708",
            "dcid": "0x06b8a0b3a1914fc2"
          },
          "frames": [
            {
              "frame_type": "crypto",
              "offset": 0,
              "length": 387
            },
            {
              "frame_type": "padding"
            }
          ]
        }],
        [0.536, "transport", "packet_received", {
          "packet_type": "initial",
          "header": {
            "packet_number": 0
          },
          "frames": [
            {
              "frame_type": "crypto",
              "offset": 0,
              "length": 1256
            }
          ]
        }],
        [1.024, "http", "frame_created", {
          "stream_id": 0,
          "frame": {
            "frame_type": "headers",
            "headers": [
              {"name": ":method", "value": "GET"},
              {"name": ":path", "value": "/"},
              {"name": ":authority", "value": "example.com"},
              {"name": ":scheme", "value": "https"}
            ]
          }
        }]
      ]
    }
  ]
}

サーバー側qlog収集

# Nginx QUIC qlog設定(1.27+)
http {
    quic_log_dir /var/log/nginx/quic;
    quic_log_level debug;

    server {
        listen 443 quic reuseport;
        server_name example.com;

        access_log /var/log/nginx/quic/access.log quic_format;
    }
}

quic-go qlog収集

package main

import (
	"context"
	"encoding/json"
	"log"
	"net"
	"os"
	"time"

	"github.com/quic-go/quic-go"
	"github.com/quic-go/quic-go/logging"
)

type qlogTracer struct {
	file *os.File
}

func newQlogTracer(filepath string) (*qlogTracer, error) {
	f, err := os.Create(filepath)
	if err != nil {
		return nil, err
	}
	return &qlogTracer{file: f}, nil
}

func (t *qlogTracer) Trace() *logging.ConnectionTracer {
	return &logging.ConnectionTracer{
		StartedConnection: func(local, remote net.Addr, srcConnID, destConnID logging.ConnectionID) {
			event := map[string]interface{}{
				"time":     time.Now().Format(time.RFC3339Nano),
				"category": "transport",
				"event":    "connection_started",
				"data": map[string]interface{}{
					"src_cid":  srcConnID.String(),
					"dest_cid": destConnID.String(),
				},
			}
			t.writeEvent(event)
		},
		ReceivedPacket: func(hdr *logging.PacketHeader, size logging.ByteCount, frames []logging.Frame) {
			event := map[string]interface{}{
				"time":     time.Now().Format(time.RFC3339Nano),
				"category": "transport",
				"event":    "packet_received",
				"data": map[string]interface{}{
					"packet_type": hdr.Type.String(),
					"packet_size": size,
					"frame_count": len(frames),
				},
			}
			t.writeEvent(event)
		},
		SentPacket: func(hdr *logging.PacketHeader, size logging.ByteCount, frames []logging.Frame) {
			event := map[string]interface{}{
				"time":     time.Now().Format(time.RFC3339Nano),
				"category": "transport",
				"event":    "packet_sent",
				"data": map[string]interface{}{
					"packet_type": hdr.Type.String(),
					"packet_size": size,
					"frame_count": len(frames),
				},
			}
			t.writeEvent(event)
		},
	}
}

func (t *qlogTracer) writeEvent(event map[string]interface{}) {
	data, _ := json.Marshal(event)
	t.file.Write(data)
	t.file.Write([]byte("\n"))
}

func main() {
	tracer, err := newQlogTracer("/tmp/quic-connection.qlog")
	if err != nil {
		log.Fatal(err)
	}
	defer tracer.file.Close()

	tlsConfig := &tls.Config{
		NextProtos: []string{"h3"},
	}

	quicConfig := &quic.Config{
		Tracer: func(ctx context.Context, p logging.Perspective, ci logging.ConnectionID) logging.ConnectionTracer {
			return *tracer.Trace()
		},
	}

	conn, err := quic.DialAddr(context.Background(), "example.com:443", tlsConfig, quicConfig)
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

	log.Printf("Connected via QUIC, version: %s", conn.ConnectionState().Version)
}

qlog可視化ツール

# qlog可視化:qvis
# オンラインツール:https://qvis.quictools.info/
# .qlogファイルをアップロードして可視化

# qlog-converterのインストール
npm install -g qlog-converter

# バイナリqlogをJSONフォーマットに変換
qlog-converter -i binary.qlog -o json.qlog --format JSON

# JSON qlogを読み取り可能なテキストに変換
qlog-converter -i json.qlog -o readable.txt --format TEXT

# 可視化レポートの生成
npm install -g qlog-visualizer
qlog-visualizer -i connection.qlog -o report.html

qlog主要イベント分析

# ハンドシェイクタイミングの分析
cat connection.qlog | python3 -c "
import json, sys
data = json.load(sys.stdin)
events = data['trace'][0]['events']
handshake_events = [e for e in events if e[1] == 'transport' and 'packet' in e[2]]
for e in handshake_events[:10]:
    print(f't={e[0]:.3f}s  {e[2]}  type={e[3].get(\"packet_type\", \"?\")}')
"

# パケットロスと再送の分析
cat connection.qlog | python3 -c "
import json, sys
data = json.load(sys.stdin)
events = data['trace'][0]['events']
loss_events = [e for e in events if 'loss' in str(e[2]).lower() or 'retransmit' in str(e[2]).lower()]
for e in loss_events:
    print(f't={e[0]:.3f}s  {e[2]}  data={e[3]}')
print(f'Total loss events: {len(loss_events)}')
"

# ストリームレベルタイミングの分析
cat connection.qlog | python3 -c "
import json, sys
data = json.load(sys.stdin)
events = data['trace'][0]['events']
stream_events = [e for e in events if e[1] == 'http' and 'stream' in str(e[3])]
for e in stream_events:
    stream_id = e[3].get('stream_id', '?')
    print(f't={e[0]:.3f}s  stream={stream_id}  {e[2]}')
"

Pattern 3:Chrome NetLog HTTP/3デバッグ

DevToolsでは不十分な理由

Chrome DevToolsのNetworkパネルはアプリケーション層の情報(リクエストヘッダー、レスポンスボディ、タイミング)しか表示できません。QUICトランスポート層の詳細(コネクションマイグレーション、0-RTTステータス、ストリーム優先度、ロスリカバリ)は見えません。HTTP/3のトランスポート問題をデバッグするには、Chrome NetLogを使用する必要があります。

Chromeを起動してNetLogをキャプチャ

# 方法1:コマンドラインフラグ
google-chrome \
  --enable-logging=netlog \
  --net-log-capture-mode=Everything \
  --net-log=/tmp/chrome-netlog.json

# 方法2:chrome://net-internalsリアルタイム表示
# 1. chrome://net-internals/#export にアクセス
# 2. "Start logging to disk" をクリック
# 3. デバッグしたい操作を実行
# 4. "Stop logging" をクリック

# 方法3:Chrome DevTools Protocol
google-chrome --remote-debugging-port=9222

# CDP経由でNetLogをトリガー
curl -s http://localhost:9222/json/version | python3 -m json.tool

NetLogイベント分析

# NetLog出力はJSONフォーマットで全ネットワークイベントを含む
# 典型的な構造:
# {
#   "constants": { ... },
#   "events": [
#     {"time": ..., "type": "HTTP3_SESSION_INITIALIZED", ...},
#     {"time": ..., "type": "QUIC_SESSION_PACKET_SENT", ...},
#     ...
#   ]
# }

# HTTP/3関連イベントの抽出
cat /tmp/chrome-netlog.json | python3 -c "
import json, sys
data = json.load(sys.stdin)
events = data.get('events', [])
http3_events = [e for e in events if 'HTTP3' in e.get('type', '') or 'QUIC' in e.get('type', '')]
for e in http3_events[:50]:
    print(f't={e.get(\"time\",0)/1000000:.3f}s  type={e.get(\"type\",\"?\")}')
"

# QUIC接続確立タイミングの分析
cat /tmp/chrome-netlog.json | python3 -c "
import json, sys
data = json.load(sys.stdin)
events = data.get('events', [])
connect_events = [e for e in events if any(k in e.get('type','') for k in ['QUIC_CONNECT', 'QUIC_SESSION_CONNECT', 'HTTP3_SESSION'])]
for e in connect_events:
    t = e.get('time', 0) / 1000000
    print(f't={t:.3f}s  {e.get(\"type\",\"?\")}')
    params = e.get('params', {})
    if params:
        for k, v in params.items():
            if k in ['host', 'quic_version', 'connection_id', 'error', 'is_alternative_service']:
                print(f'  {k}={v}')
"

# 0-RTTステータスの確認
cat /tmp/chrome-netlog.json | python3 -c "
import json, sys
data = json.load(sys.stdin)
events = data.get('events', [])
zrtt_events = [e for e in events if 'ZERO_RTT' in e.get('type', '') or 'early_data' in str(e.get('params', {})).lower()]
for e in zrtt_events:
    print(f't={e.get(\"time\",0)/1000000:.3f}s  {e.get(\"type\")}  params={e.get(\"params\",{})}')
if not zrtt_events:
    print('No 0-RTT events found')
"

# コネクションマイグレーションの分析
cat /tmp/chrome-netlog.json | python3 -c "
import json, sys
data = json.load(sys.stdin)
events = data.get('events', [])
migration_events = [e for e in events if 'MIGRATION' in e.get('type', '') or 'CONNECTION_MIGRATION' in e.get('type', '')]
for e in migration_events:
    print(f't={e.get(\"time\",0)/1000000:.3f}s  {e.get(\"type\")}  params={e.get(\"params\",{})}')
if not migration_events:
    print('No connection migration events found')
"

chrome://net-internalsリアルタイムデバッグ

chrome://net-internals 主要ページ:

#h3              - HTTP/3セッションリスト
#quic            - QUIC接続リストと設定
#sockets         - UDPソケット状態
#dns             - DNS解決(HTTPSレコード含む)
#httpCache       - キャッシュ状態
#altSvc          - Alt-Svcキャッシュ内容
# chrome://net-internals/#quic で確認:
# - 現在アクティブなQUIC接続
# - 各接続のバージョン、CID、ステータス
# - QUIC設定パラメータ
# - 接続エラー情報

# chrome://net-internals/#h3 で確認:
# - HTTP/3セッション状態
# - ストリームの作成とクローズ
# - 優先度依存関係
# - プッシュ状態

Alt-Svcキャッシュデバッグ

# Alt-Svcキャッシュの表示
# chrome://net-internals/#altSvc

# よくある問題:Alt-Svcキャッシュの期限切れや誤り
# Alt-Svcキャッシュのクリア:
# 1. chrome://net-internals/#altSvc を開く
# 2. "Clear alt-svc cache" をクリック
# 3. 対象ウェブサイトに再アクセス

# HTTP/3の強制使用(Alt-Svc発見をスキップ)
google-chrome \
  --origin-to-force-quic-on=example.com:443 \
  --net-log=/tmp/chrome-forced-h3.json

Pattern 4:curl HTTP/3デバッグ

HTTP/3対応curlのコンパイル

# Ubuntu 22.04+ でHTTP/3対応curlをコンパイル
# 方法:boringssl + nghttp3 + ngtcp2

# 依存関係のインストール
sudo apt-get install -y build-essential cmake git

# boringsslのコンパイル
git clone https://boringssl.googlesource.com/boringssl
cd boringssl
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=on .
make -j$(nproc)
cd ..

# ngtcp2のコンパイル
git clone https://github.com/ngtcp2/ngtcp2
cd ngtcp2
autoreconf -fi
./configure --with-boringssl=$(pwd)/../boringssl \
  BORINGSSL_CFLAGS="-I$(pwd)/../boringssl/include" \
  BORINGSSL_LIBS="-L$(pwd)/../boringssl/build/ssl -L$(pwd)/../boringssl/build/crypto -lssl -lcrypto"
make -j$(nproc)
sudo make install
cd ..

# nghttp3のコンパイル
git clone https://github.com/ngtcp2/nghttp3
cd nghttp3
autoreconf -fi
./configure
make -j$(nproc)
sudo make install
cd ..

# HTTP/3対応curlのコンパイル
git clone https://github.com/curl/curl
cd curl
autoreconf -fi
./configure --with-openssl=$(pwd)/../boringssl \
  --with-ngtcp2=$(pwd)/../ngtcp2 \
  --with-nghttp3=$(pwd)/../nghttp3 \
  LDFLAGS="-Wl,-rpath,$(pwd)/../boringssl/build/ssl:$(pwd)/../boringssl/build/crypto"
make -j$(nproc)
sudo make install
cd ..

# 検証
curl --version | grep -i http3
# 出力に含まれるべき:Features: ... HTTP3 ...

基本的なHTTP/3リクエストデバッグ

# HTTP/3リクエストの送信(詳細出力)
curl --http3 https://example.com -v

# 出力例:
# *   Trying 93.184.216.34:443...
# * Connected to example.com (93.184.216.34) port 443
# * QUIC handshake successful
# * Connection #0 to host example.com left intact
# > GET / HTTP/3
# > Host: example.com
# > user-agent: curl/8.7.1
# > accept: */*
# >
# < HTTP/3 200
# < content-type: text/html; charset=UTF-8
# < date: Mon, 16 Jun 2026 10:00:00 GMT

# HTTP/3のみを強制使用(フォールバックなし)
curl --http3-only https://example.com -v

# 0-RTTのテスト
# 最初のリクエスト:通常ハンドシェイク
curl --http3 https://example.com -w "time_connect: %{time_connect}\ntime_appconnect: %{time_appconnect}\ntime_total: %{time_total}\n" -o /dev/null -s

# 2回目のリクエスト:0-RTT(セッション再利用)
curl --http3 https://example.com -w "time_connect: %{time_connect}\ntime_appconnect: %{time_appconnect}\ntime_total: %{time_total}\n" -o /dev/null -s

curlタイミング分析

# 完全なタイミング出力
curl --http3 https://example.com \
  -w "\n=== Timing Breakdown ===\n\
namelookup:    %{time_namelookup}s\n\
connect:       %{time_connect}s\n\
appconnect:    %{time_appconnect}s\n\
pretransfer:   %{time_pretransfer}s\n\
starttransfer: %{time_starttransfer}s\n\
total:         %{time_total}s\n\
\n=== Connection Info ===\n\
remote_ip:     %{remote_ip}\n\
remote_port:   %{remote_port}\n\
scheme:        %{scheme}\n\
http_version:  %{http_version}\n\
" -o /dev/null -s

# HTTP/2 vs HTTP/3接続時間の比較
echo "=== HTTP/2 ==="
curl --http2 https://example.com \
  -w "appconnect: %{time_appconnect}s  starttransfer: %{time_starttransfer}s  total: %{time_total}s\n" \
  -o /dev/null -s

echo "=== HTTP/3 ==="
curl --http3 https://example.com \
  -w "appconnect: %{time_appconnect}s  starttransfer: %{time_starttransfer}s  total: %{time_total}s\n" \
  -o /dev/null -s

# バッチテスト(10回実行、平均値)
for proto in h2 h3; do
  total=0
  for i in $(seq 1 10); do
    t=$(curl --http${proto} https://example.com -w "%{time_total}" -o /dev/null -s 2>/dev/null)
    total=$(echo "$total + $t" | bc)
  done
  avg=$(echo "scale=3; $total / 10" | bc)
  echo "HTTP/${proto} average total time: ${avg}s"
done

curl環境変数デバッグ

# QUIC内部ログの有効化
export SSLKEYLOGFILE=/tmp/curl-quic-keys.log

# ngtcp2詳細ログの有効化
export NGTCP2_DEBUG_LOG=1

# nghttp3詳細ログの有効化
export NGHTTP3_DEBUG_LOG=1

# リクエストの送信
curl --http3 https://example.com -v 2>&1 | tee /tmp/curl-h3-debug.log

# ログの分析
grep -i "handshake\|0-rtt\|stream\|frame" /tmp/curl-h3-debug.log

# 特定のQUICバージョンのテスト
curl --http3 https://example.com -v \
  --quic-version v1    # RFC 9000 (QUIC v1)

curl --http3 https://example.com -v \
  --quic-version v2    # RFC 9369 (QUIC v2)

curlネットワーク問題のシミュレーション

# 高遅延のシミュレーション
curl --http3 https://example.com -v \
  --limit-rate 100k \
  --connect-timeout 5 \
  --max-time 30

# 接続タイムアウト処理のテスト
curl --http3-only https://unreachable.example.com -v \
  --connect-timeout 3 \
  --max-time 10

# Alt-Svc発見のテスト
# まずHTTP/2でAlt-Svcヘッダーを取得
curl --http2 https://example.com -v -I 2>&1 | grep -i alt-svc

# 次にHTTP/3で手動接続
curl --http3 https://example.com -v

# 大容量ファイルの送信でフロー制御をテスト
dd if=/dev/urandom bs=1M count=100 2>/dev/null | \
  curl --http3 https://example.com/upload -v \
  -X POST \
  -H "Content-Type: application/octet-stream" \
  --data-binary @-

Pattern 5:プロダクションQUIC監視

Prometheus QUICメトリクス収集

package main

import (
	"log"
	"net/http"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"
)

var (
	quicConnectionsTotal = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "quic_connections_total",
			Help: "Total number of QUIC connections",
		},
		[]string{"version", "status"},
	)

	quicConnectionDuration = prometheus.NewHistogramVec(
		prometheus.HistogramOpts{
			Name:    "quic_connection_duration_seconds",
			Help:    "QUIC connection duration in seconds",
			Buckets: prometheus.ExponentialBuckets(0.1, 2, 15),
		},
		[]string{"version"},
	)

	quicHandshakeDuration = prometheus.NewHistogramVec(
		prometheus.HistogramOpts{
			Name:    "quic_handshake_duration_seconds",
			Help:    "QUIC handshake duration in seconds",
			Buckets: prometheus.ExponentialBuckets(0.001, 2, 15),
		},
		[]string{"version", "zero_rtt"},
	)

	quicStreamsTotal = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "quic_streams_total",
			Help: "Total number of QUIC streams",
		},
		[]string{"direction", "stream_type"},
	)

	quicPacketsLost = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "quic_packets_lost_total",
			Help: "Total number of QUIC packets lost",
		},
		[]string{"packet_type"},
	)

	quicRetransmitPackets = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "quic_retransmit_packets_total",
			Help: "Total number of QUIC retransmit packets",
		},
		[]string{"packet_type"},
	)

	quicBytesTransferred = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "quic_bytes_transferred_total",
			Help: "Total bytes transferred over QUIC",
		},
		[]string{"direction"},
	)

	quicConnectionMigrations = prometheus.NewCounter(
		prometheus.CounterOpts{
			Name: "quic_connection_migrations_total",
			Help: "Total number of QUIC connection migrations",
		},
	)

	quicZeroRTTAccepts = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "quic_zero_rtt_accepts_total",
			Help: "Total number of 0-RTT connection attempts",
		},
		[]string{"status"},
	)

	quicCongestionWindow = prometheus.NewGaugeVec(
		prometheus.GaugeOpts{
			Name: "quic_congestion_window_bytes",
			Help: "Current QUIC congestion window size in bytes",
		},
		[]string{"connection_id"},
	)

	quicRtt = prometheus.NewHistogramVec(
		prometheus.HistogramOpts{
			Name:    "quic_rtt_seconds",
			Help:    "QUIC round trip time in seconds",
			Buckets: prometheus.ExponentialBuckets(0.001, 2, 15),
		},
		[]string{"rtt_type"},
	)
)

func init() {
	prometheus.MustRegister(
		quicConnectionsTotal,
		quicConnectionDuration,
		quicHandshakeDuration,
		quicStreamsTotal,
		quicPacketsLost,
		quicRetransmitPackets,
		quicBytesTransferred,
		quicConnectionMigrations,
		quicZeroRTTAccepts,
		quicCongestionWindow,
		quicRtt,
	)
}

func main() {
	http.Handle("/metrics", promhttp.Handler())

	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		if r.ProtoMajor == 3 {
			quicConnectionsTotal.WithLabelValues("v1", "active").Inc()
		}
		w.WriteHeader(http.StatusOK)
		w.Write([]byte("Hello HTTP/3!"))
	})

	log.Println("Starting server on :443 with metrics on :9090/metrics")

	go func() {
		log.Fatal(http.ListenAndServe(":9090", nil))
	}()

	log.Fatal(http.ListenAndServeTLS(":443", "server.crt", "server.key", mux))
}

Nginx QUICメトリクスエクスポート

# nginx.conf - QUICメトリクスをstub_statusで公開
http {
    server {
        listen 443 quic reuseport;
        listen 443 ssl;
        server_name example.com;

        location /nginx_status {
            stub_status;
            allow 127.0.0.1;
            deny all;
        }

        log_format quic_metrics
            '$remote_addr '
            '$quic_connection_id '
            '$quic_version '
            '$request_time '
            '$upstream_response_time '
            '$bytes_sent '
            '$bytes_received '
            '$status';

        access_log /var/log/nginx/quic-metrics.log quic_metrics;
    }
}
# nginx-prometheus-exporterでNginxメトリクスを収集
docker run -d --name nginx-exporter \
  -p 9113:9113 \
  nginx/nginx-prometheus-exporter:1.3 \
  --nginx.scrape-uri=http://nginx:80/nginx_status

# カスタムQUICログパーサー(Python)
cat << 'EOF' > /usr/local/bin/quic-log-parser.py
import sys
from prometheus_client import Counter, Gauge, start_http_server

quic_connections = Counter('nginx_quic_connections', 'Nginx QUIC connections')
quic_request_duration = Gauge('nginx_quic_request_duration_seconds', 'QUIC request duration')

for line in sys.stdin:
    parts = line.strip().split()
    if len(parts) >= 8:
        quic_connections.inc()
        try:
            duration = float(parts[3])
            quic_request_duration.set(duration)
        except ValueError:
            pass

start_http_server(9114)
EOF

Grafanaダッシュボード設定

{
  "dashboard": {
    "title": "HTTP/3 & QUIC Production Monitoring",
    "panels": [
      {
        "title": "QUIC Connection Rate",
        "type": "timeseries",
        "targets": [
          {
            "expr": "rate(quic_connections_total[5m])",
            "legendFormat": "{{version}} {{status}}"
          }
        ]
      },
      {
        "title": "Handshake Duration",
        "type": "timeseries",
        "targets": [
          {
            "expr": "histogram_quantile(0.50, rate(quic_handshake_duration_seconds_bucket[5m]))",
            "legendFormat": "p50 {{version}} 0-rtt={{zero_rtt}}"
          },
          {
            "expr": "histogram_quantile(0.99, rate(quic_handshake_duration_seconds_bucket[5m]))",
            "legendFormat": "p99 {{version}} 0-rtt={{zero_rtt}}"
          }
        ]
      },
      {
        "title": "Packet Loss Rate",
        "type": "timeseries",
        "targets": [
          {
            "expr": "rate(quic_packets_lost_total[5m]) / rate(quic_connections_total[5m])",
            "legendFormat": "{{packet_type}} loss rate"
          }
        ]
      },
      {
        "title": "0-RTT Success Rate",
        "type": "gauge",
        "targets": [
          {
            "expr": "rate(quic_zero_rtt_accepts_total{status=\"accepted\"}[5m]) / rate(quic_zero_rtt_accepts_total[5m]) * 100",
            "legendFormat": "0-RTT Accept %"
          }
        ]
      },
      {
        "title": "Connection Migrations",
        "type": "stat",
        "targets": [
          {
            "expr": "rate(quic_connection_migrations_total[1h])",
            "legendFormat": "migrations/hour"
          }
        ]
      },
      {
        "title": "QUIC RTT Distribution",
        "type": "timeseries",
        "targets": [
          {
            "expr": "histogram_quantile(0.50, rate(quic_rtt_seconds_bucket[5m]))",
            "legendFormat": "p50 {{rtt_type}}"
          },
          {
            "expr": "histogram_quantile(0.95, rate(quic_rtt_seconds_bucket[5m]))",
            "legendFormat": "p95 {{rtt_type}}"
          }
        ]
      },
      {
        "title": "Throughput",
        "type": "timeseries",
        "targets": [
          {
            "expr": "rate(quic_bytes_transferred_total{direction=\"send\"}[5m]) * 8 / 1000000",
            "legendFormat": "Upload Mbps"
          },
          {
            "expr": "rate(quic_bytes_transferred_total{direction=\"receive\"}[5m]) * 8 / 1000000",
            "legendFormat": "Download Mbps"
          }
        ]
      },
      {
        "title": "Retransmit Rate",
        "type": "timeseries",
        "targets": [
          {
            "expr": "rate(quic_retransmit_packets_total[5m])",
            "legendFormat": "{{packet_type}} retransmits/s"
          }
        ]
      }
    ]
  }
}

アラートルール

# QUIC用Prometheusアラートルール
groups:
  - name: quic_alerts
    rules:
      - alert: QUICHighPacketLoss
        expr: rate(quic_packets_lost_total[5m]) / rate(quic_connections_total[5m]) > 0.05
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "QUICパケットロス率が5%を超過"
          description: "QUICパケットロス率は {{ $labels.packet_type }} で {{ $value | humanizePercentage }}"

      - alert: QUICHandshakeTimeout
        expr: histogram_quantile(0.99, rate(quic_handshake_duration_seconds_bucket[5m])) > 1
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "QUICハンドシェイクp99レイテンシが1sを超過"
          description: "QUIC {{ $labels.version }} ハンドシェイクp99は {{ $value }}s"

      - alert: QUICZeroRTTRejectionHigh
        expr: rate(quic_zero_rtt_accepts_total{status="rejected"}[5m]) / rate(quic_zero_rtt_accepts_total[5m]) > 0.3
        for: 15m
        labels:
          severity: warning
        annotations:
          summary: "QUIC 0-RTT拒否率が30%を超過"
          description: "0-RTT拒否率は {{ $value | humanizePercentage }}"

      - alert: QUICConnectionFailureSpike
        expr: rate(quic_connections_total{status="failed"}[5m]) > rate(quic_connections_total{status="active"}[5m]) * 0.1
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "QUIC接続失敗率が10%を超過"
          description: "QUIC接続失敗は成功接続の {{ $value | humanizePercentage }}"

      - alert: QUICHighRetransmitRate
        expr: rate(quic_retransmit_packets_total[5m]) / rate(quic_bytes_transferred_total[5m]) > 0.1
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "QUIC再送率が10%を超過"
          description: "QUIC再送率は {{ $value | humanizePercentage }}"

5つのデバッグパターン比較

パターン ユースケース 強み 制限 学習コスト
Wiresharkキャプチャ プロトコルレベルの問題、ハンドシェイク分析 最も深く、最も完全 鍵が必要、リアルタイム分析不可
qlog分析 実装間比較、最適化 標準化、可視化可能 実装のサポートが必要
Chrome NetLog クライアント問題、コネクションマイグレーション ブラウザネイティブ、リアルタイム Chromeのみ、データ量大
curlデバッグ クイック検証、CI統合 CLI、スクリプト可能 コンパイルが必要、機能限定
プロダクション監視 本番アラート、トレンド分析 リアルタイム、グローバルビュー 計装が必要、詳細の遡及不可 中高

10の一般的なデバッグシナリオクイックリファレンス

# シナリオ 推奨パターン 主要コマンド/ツール
1 QUICハンドシェイク失敗 Wireshark tshark -f "udp port 443" -o "tls.keylog_file:keys.log" -Y "quic.packet_type==initial"
2 0-RTT拒否 Chrome NetLog chrome://net-internals/#quic ZERO_RTTイベントを検索
3 コネクションマイグレーション不動作 Chrome NetLog --origin-to-force-quic-on + NetLog MIGRATIONイベント
4 ロスリカバリが遅い qlog qvisでロス検出とリカバリイベントを可視化
5 ストリーム優先度の問題 Wireshark 復号後にHTTP/3 PRIORITYフレームを分析
6 Alt-Svcが動作しない curl curl -I https://example.com Alt-Svcヘッダーを確認
7 パフォーマンス比較 curl curl --http2 vs curl --http3 タイミング比較
8 本番パケットロス率異常 Prometheus rate(quic_packets_lost_total[5m]) アラート
9 ハンドシェイクレイテンシ上昇 Prometheus histogram_quantile(0.99, quic_handshake_duration_seconds)
10 0-RTTリプレイリスク Wireshark + qlog キャプチャで0-RTTデータに非冪等リクエストが含まれていないか確認

おすすめツール

HTTP/3デバッグ中、以下のツールが分析と検証に役立ちます:

  • HTTPステータスコード検索/ja/network/http-status でQUIC/HTTP3関連ステータスコードの意味を検索
  • Base64エンコーダー/ja/encode/base64 で証明書やトークンのエンコード/デコード
  • ハッシュ計算/ja/encode/hash でQUIC設定の整合性を検証

まとめ:HTTP/3デバッグの核心的な課題は、QUICの完全暗号化とUDP転送にあります。5つのプロダクションデバッグパターンはそれぞれ異なる焦点を持っています:Wiresharkキャプチャはプロトコルレベルの深い分析に(SSLKEYLOGFILE復号が必要)、qlogは標準化された実装間ログ分析に、Chrome NetLogはクライアント側リアルタイムデバッグに、curlはクイック検証とCI統合に、Prometheus+Grafanaはプロダクションの継続監視に適しています。curlでのクイック検証から始め、深い問題にはWireshark/qlogにエスカレートし、本番では必ず完全なQUICメトリクス監視とアラート体系を構築してください。

ブラウザローカルツールを無料で試す →

#HTTP/3调试#QUIC抓包#qlog#网络分析#Chrome NetLog#2026#网络协议