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 資料),但開發者遇到問題時,往往只能看到「連線失敗」四個字,無從下手。本文總結了 5 種經過生產驗證的除錯模式,從抓封包解密到即時監控,幫你建立完整的 HTTP/3 除錯工具鏈。
| 除錯維度 | HTTP/2 工具 | HTTP/3 工具 | 難度變化 |
|---|---|---|---|
| 抓封包分析 | Wireshark(直接可讀) | Wireshark + SSLKEYLOG | 需要金鑰解密 |
| 協定日誌 | 無標準 | qlog(標準化格式) | 新概念 |
| 客戶端除錯 | Chrome DevTools | Chrome NetLog | 更底層 |
| 命令列除錯 | 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 流量)
# 在 Wireshark 捕獲過濾器中使用:
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
# 使用 qlog-visualizer 產生視覺化報告
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 位址列輸入 chrome://net-internals/#export
# 2. 點擊 "Start logging to disk"
# 3. 執行需要除錯的操作
# 4. 點擊 "Stop logging"
# 方式3:透過 Chrome DevTools Protocol
# 啟動 Chrome 時開啟遠端除錯
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 socket 狀態
#dns - DNS 解析(含 HTTPS 記錄)
#httpCache - 快取狀態
#altSvc - Alt-Svc 快取內容
# 透過 chrome://net-internals/#quic 查看:
# - 目前活躍的 QUIC 連線
# - 每個連線的版本、CID、狀態
# - QUIC 設定參數
# - 連線錯誤資訊
# 透過 chrome://net-internals/#h3 查看:
# - HTTP/3 工作階段狀態
# - 串流的建立和關閉
# - 優先級依賴關係
# - 推送(Push)狀態
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+ 編譯 curl with HTTP/3
# 方式: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 ..
# 編譯 curl with HTTP/3
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
# 第二次請求: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 整合 | 命令列、可腳本化 | 需要編譯、功能有限 | 低 |
| 生產監控 | 線上告警、趨勢分析 | 即時、全域視角 | 需要埋點、無法回溯細節 | 中高 |
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 視覺化分析 loss detection 和 recovery 事件 |
| 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 狀態碼查詢:使用 /zh-TW/network/http-status 查詢 QUIC/HTTP3 相關狀態碼含義
- Base64 編碼:處理憑證和 token 時,使用 /zh-TW/encode/base64 進行編解碼
- 雜湊計算:驗證 QUIC 設定完整性時,使用 /zh-TW/encode/hash 計算雜湊值
總結:HTTP/3 除錯的核心挑戰在於 QUIC 的全程加密和 UDP 傳輸。5 種生產除錯模式各有側重:Wireshark 抓封包適合協定級深度分析(需 SSLKEYLOGFILE 解密),qlog 適合跨實作的標準化日誌分析,Chrome NetLog 適合客戶端即時除錯,curl 適合快速驗證和 CI 整合,Prometheus+Grafana 適合生產環境持續監控。建議從 curl 快速驗證開始,遇到深層問題再用 Wireshark/qlog 定位,生產環境務必建立完整的 QUIC 指標監控和告警體系。
本站提供瀏覽器本地工具,免註冊即可試用 →