Débogage HTTP/3 et QUIC : 5 modèles de production de la capture de paquets à l'analyse de performance
Pourquoi le débogage HTTP/3 est 10x plus difficile que HTTP/2
HTTP/3 fonctionne sur QUIC via UDP, ce qui signifie que 20 ans d'expérience en débogage TCP sont essentiellement inutiles. Les paquets TCP sont directement lisibles dans Wireshark, les flux HTTP/2 sont visibles dans Chrome DevTools — mais le trafic QUIC est chiffré, rendant les paquets illisibles ; le multiplexage de flux HTTP/3 se produit au niveau transport, invisible aux outils de couche applicative.
En 2026, l'adoption mondiale d'HTTP/3 dépasse 45 % (données de Cloudflare Radar). Pourtant, lorsque les développeurs rencontrent des problèmes, ils ne voient souvent rien de plus que « connection failed » sans moyen d'enquêter. Cet article résume 5 modèles de débogage validés en production, de la capture décryptée à la surveillance en temps réel, vous aidant à construire une boîte à outils de débogage HTTP/3 complète.
| Dimension de débogage | Outil HTTP/2 | Outil HTTP/3 | Changement de difficulté |
|---|---|---|---|
| Capture de paquets | Wireshark (lisible directement) | Wireshark + SSLKEYLOG | Nécessite le décryptage des clés |
| Journalisation de protocole | Pas de standard | qlog (format standardisé) | Nouveau concept |
| Débogage client | Chrome DevTools | Chrome NetLog | Niveau plus bas |
| Débogage CLI | curl -v | curl --http3 + variables d'environnement | Nécessite compilation |
| Monitoring de production | Métriques TCP | Métriques spécifiques QUIC | Système de métriques différent |
Modèle 1 : Capture et dissection de paquets QUIC dans Wireshark
Problème central : QUIC est entièrement chiffré
QUIC intègre TLS 1.3 directement dans le protocole. Toutes les trames, y compris les en-têtes, sont chiffrées. Les captures QUIC dans Wireshark ne montrent que des payloads UDP — les trames HTTP/3 internes ne peuvent pas être analysées. Pour décrypter le trafic QUIC, vous devez obtenir les clés de session TLS.
Mécanisme SSLKEYLOGFILE
SSLKEYLOGFILE est le mécanisme standard de débogage TLS. Les clients qui le prennent en charge (Chrome, Firefox, curl, Go) écrivent les clés de session TLS dans un fichier spécifié. Wireshark lit ce fichier pour décrypter le trafic.
# Définir la variable d'environnement SSLKEYLOGFILE
export SSLKEYLOGFILE=/tmp/sslkeys.log
# Capturer le trafic HTTP/3 avec curl (clés écrites automatiquement)
curl --http3 https://example.com -v
# Accéder avec Chrome (clés écrites automatiquement)
google-chrome --ssl-key-log-file=/tmp/sslkeys.log
# Accéder avec Firefox
export MOZ_LOG="ssl:5"
export SSLKEYLOGFILE=/tmp/sslkeys.log
firefox
Configuration du décryptage QUIC dans Wireshark
# 1. Démarrer la capture Wireshark (filtrer le trafic QUIC)
# Filtre de capture :
udp port 443
# 2. Configurer le fichier journal de clés dans Wireshark
# Edit -> Preferences -> Protocols -> TLS -> (Pre)-Master-Secret log filename
# Saisir : /tmp/sslkeys.log
# 3. Vérifier le succès du décryptage QUIC
# Avant décryptage : les paquets QUIC affichent "Protected Payload, PKN: ..."
# Après décryptage : trames HTTP/3 visibles (HEADERS, DATA, SETTINGS, etc.)
Analyse de paquets en ligne de commande avec tshark
# Capturer le trafic QUIC avec décryptage
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
# Extraire les en-têtes de requête 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
# Analyser les informations de handshake de connexion 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
# Analyser la perte de paquets et la retransmission 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
Export des clés sur serveur 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
}
Identification de version QUIC
# Numéros de version QUIC courants
# RFC 9000 (QUIC v1) : 0x00000001
# RFC 9369 (QUIC v2) : 0x6b3343cf
# Google QUIC (gQUIC) : 0x51303039 (Q039)
# IETF Draft 29 : 0xff00001d
# Filtrer une version spécifique avec tshark
tshark -i eth0 -f "udp port 443" \
-Y "quic.version == 0x00000001" \
-T fields \
-e quic.connection_id \
-e quic.version
Modèle 2 : Analyse et visualisation qlog
Qu'est-ce que qlog
qlog est le format de journalisation QUIC/HTTP/3 standardisé par l'IETF (RFC 9657). Il définit un modèle d'événements unifié permettant l'interopérabilité entre différentes implémentations QUIC. Contrairement aux formats propriétaires, qlog permet d'analyser les journaux de quiche, lsquic, quic-go, ngtcp2 et d'autres implémentations avec les mêmes outils.
{
"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"}
]
}
}]
]
}
]
}
Collecte qlog côté serveur
# Nginx QUIC qlog configuration (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;
}
}
Collecte qlog avec quic-go
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)
}
Outils de visualisation qlog
# Visualisation qlog : qvis
# Outil en ligne : https://qvis.quictools.info/
# Téléverser le fichier .qlog pour la visualisation
# Installer qlog-converter
npm install -g qlog-converter
# Convertir un qlog binaire au format JSON
qlog-converter -i binary.qlog -o json.qlog --format JSON
# Convertir un JSON qlog en texte lisible
qlog-converter -i json.qlog -o readable.txt --format TEXT
# Générer un rapport de visualisation
npm install -g qlog-visualizer
qlog-visualizer -i connection.qlog -o report.html
Analyse des événements clés qlog
# Analyser la temporisation du handshake
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\", \"?\")}')
"
# Analyser la perte de paquets et la retransmission
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)}')
"
# Analyser la temporisation au niveau flux
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]}')
"
Modèle 3 : Chrome NetLog pour le débogage HTTP/3
Pourquoi DevTools ne suffit pas
Le panneau Network de Chrome DevTools n'affiche que des informations de niveau applicatif (en-têtes de requête, corps de réponse, temporisation). Il ne peut pas voir les détails de la couche transport QUIC (migration de connexion, état 0-RTT, priorités de flux, récupération de perte). Pour déboguer les problèmes de transport HTTP/3, vous devez utiliser Chrome NetLog.
Lancer Chrome avec capture NetLog
# Méthode 1 : options de ligne de commande
google-chrome \
--enable-logging=netlog \
--net-log-capture-mode=Everything \
--net-log=/tmp/chrome-netlog.json
# Méthode 2 : vue en temps réel chrome://net-internals
# 1. Naviguer vers chrome://net-internals/#export
# 2. Cliquer sur "Start logging to disk"
# 3. Effectuer l'opération à déboguer
# 4. Cliquer sur "Stop logging"
# Méthode 3 : Chrome DevTools Protocol
google-chrome --remote-debugging-port=9222
# Déclencher NetLog via CDP
curl -s http://localhost:9222/json/version | python3 -m json.tool
Analyse des événements NetLog
# La sortie NetLog est au format JSON contenant tous les événements réseau
# Structure typique :
# {
# "constants": { ... },
# "events": [
# {"time": ..., "type": "HTTP3_SESSION_INITIALIZED", ...},
# {"time": ..., "type": "QUIC_SESSION_PACKET_SENT", ...},
# ...
# ]
# }
# Extraire les événements liés à 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\",\"?\")}')
"
# Analyser la temporisation d'établissement de connexion 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}')
"
# Vérifier l'état 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')
"
# Analyser la migration de connexion
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')
"
Débogage en temps réel avec chrome://net-internals
Pages clés de chrome://net-internals :
#h3 - liste des sessions HTTP/3
#quic - liste et configuration des connexions QUIC
#sockets - état des sockets UDP
#dns - résolution DNS (y compris les enregistrements HTTPS)
#httpCache - état du cache
#altSvc - contenu du cache Alt-Svc
# Via chrome://net-internals/#quic voir :
# - Connexions QUIC actives
# - Version, CID, état de chaque connexion
# - Paramètres de configuration QUIC
# - Informations d'erreur de connexion
# Via chrome://net-internals/#h3 voir :
# - État de session HTTP/3
# - Création et fermeture de flux
# - Relations de dépendance de priorité
# - État du push
Débogage du cache Alt-Svc
# Voir le cache Alt-Svc
# chrome://net-internals/#altSvc
# Problème courant : cache Alt-Svc expiré ou incorrect
# Effacer le cache Alt-Svc :
# 1. Ouvrir chrome://net-internals/#altSvc
# 2. Cliquer sur "Clear alt-svc cache"
# 3. Revisiter le site cible
# Forcer HTTP/3 (ignorer la découverte Alt-Svc)
google-chrome \
--origin-to-force-quic-on=example.com:443 \
--net-log=/tmp/chrome-forced-h3.json
Modèle 4 : Débogage curl HTTP/3
Compiler curl avec support HTTP/3
# Compiler curl avec HTTP/3 sur Ubuntu 22.04+
# Méthode : boringssl + nghttp3 + ngtcp2
# Installer les dépendances
sudo apt-get install -y build-essential cmake git
# Compiler boringssl
git clone https://boringssl.googlesource.com/boringssl
cd boringssl
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=on .
make -j$(nproc)
cd ..
# Compiler 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 ..
# Compiler nghttp3
git clone https://github.com/ngtcp2/nghttp3
cd nghttp3
autoreconf -fi
./configure
make -j$(nproc)
sudo make install
cd ..
# Compiler curl avec 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 ..
# Vérifier
curl --version | grep -i http3
# La sortie doit inclure : Features: ... HTTP3 ...
Débogage de requête HTTP/3 de base
# Envoyer une requête HTTP/3 avec sortie détaillée
curl --http3 https://example.com -v
# Exemple de sortie :
# * 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
# Forcer HTTP/3 uniquement (pas de fallback)
curl --http3-only https://example.com -v
# Tester 0-RTT
# Première requête : handshake normal
curl --http3 https://example.com -w "time_connect: %{time_connect}\ntime_appconnect: %{time_appconnect}\ntime_total: %{time_total}\n" -o /dev/null -s
# Seconde requête : 0-RTT (réutiliser la session)
curl --http3 https://example.com -w "time_connect: %{time_connect}\ntime_appconnect: %{time_appconnect}\ntime_total: %{time_total}\n" -o /dev/null -s
Analyse de temporisation curl
# Sortie de temporisation complète
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
# Comparer le temps de connexion 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
# Test par lot (10 exécutions, moyenne)
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
Débogage avec variables d'environnement curl
# Activer la journalisation interne QUIC
export SSLKEYLOGFILE=/tmp/curl-quic-keys.log
# Activer la journalisation détaillée ngtcp2
export NGTCP2_DEBUG_LOG=1
# Activer la journalisation détaillée nghttp3
export NGHTTP3_DEBUG_LOG=1
# Envoyer la requête
curl --http3 https://example.com -v 2>&1 | tee /tmp/curl-h3-debug.log
# Analyser le journal
grep -i "handshake\|0-rtt\|stream\|frame" /tmp/curl-h3-debug.log
# Tester une version QUIC spécifique
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)
Simuler des problèmes réseau avec curl
# Simuler une latence élevée
curl --http3 https://example.com -v \
--limit-rate 100k \
--connect-timeout 5 \
--max-time 30
# Tester la gestion du timeout de connexion
curl --http3-only https://unreachable.example.com -v \
--connect-timeout 3 \
--max-time 10
# Tester la découverte Alt-Svc
# D'abord obtenir l'en-tête Alt-Svc via HTTP/2
curl --http2 https://example.com -v -I 2>&1 | grep -i alt-svc
# Puis se connecter manuellement via HTTP/3
curl --http3 https://example.com -v
# Envoyer un grand fichier pour tester le contrôle de flux
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 @-
Modèle 5 : Monitoring QUIC de production
Collecte de métriques QUIC avec Prometheus
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))
}
Export des métriques QUIC dans Nginx
# nginx.conf - QUIC metrics via 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;
}
}
# Utiliser nginx-prometheus-exporter pour collecter les métriques Nginx
docker run -d --name nginx-exporter \
-p 9113:9113 \
nginx/nginx-prometheus-exporter:1.3 \
--nginx.scrape-uri=http://nginx:80/nginx_status
# Analyseur de journal QUIC personnalisé (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
Configuration du tableau de bord 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"
}
]
}
]
}
}
Règles d'alerte
# Prometheus alerting rules for QUIC
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 packet loss rate exceeds 5%"
description: "QUIC packet loss rate is {{ $value | humanizePercentage }} on {{ $labels.packet_type }}"
- alert: QUICHandshakeTimeout
expr: histogram_quantile(0.99, rate(quic_handshake_duration_seconds_bucket[5m])) > 1
for: 5m
labels:
severity: critical
annotations:
summary: "QUIC handshake p99 latency exceeds 1s"
description: "QUIC {{ $labels.version }} handshake p99 is {{ $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 rejection rate exceeds 30%"
description: "0-RTT rejection rate is {{ $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 connection failure rate exceeds 10%"
description: "QUIC connection failures are {{ $value | humanizePercentage }} of successful connections"
- 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 retransmit rate exceeds 10%"
description: "QUIC retransmit rate is {{ $value | humanizePercentage }}"
Comparaison des 5 modèles de débogage
| Modèle | Cas d'usage | Forces | Limites | Courbe d'apprentissage |
|---|---|---|---|---|
| Capture Wireshark | Problèmes de niveau protocole, analyse de handshake | La plus profonde, la plus complète | Nécessite les clés, pas d'analyse live | Élevée |
| Analyse qlog | Comparaison entre implémentations, optimisation | Standardisée, visualisable | Nécessite le support d'implémentation | Moyenne |
| Chrome NetLog | Problèmes client, migration de connexion | Native navigateur, temps réel | Chrome uniquement, gros volume de données | Moyenne |
| Débogage curl | Validation rapide, intégration CI | CLI, scriptable | Nécessite compilation, fonctions limitées | Faible |
| Monitoring de production | Alertes de production, analyse de tendances | Temps réel, vue globale | Nécessite instrumentation, pas de rappel détaillé | Moyenne-Élevée |
Référence rapide de 10 scénarios de débogage courants
| # | Scénario | Modèle recommandé | Commande/outil clé |
|---|---|---|---|
| 1 | Échec de handshake QUIC | Wireshark | tshark -f "udp port 443" -o "tls.keylog_file:keys.log" -Y "quic.packet_type==initial" |
| 2 | 0-RTT rejeté | Chrome NetLog | chrome://net-internals/#quic trouver les événements ZERO_RTT |
| 3 | Migration de connexion ne fonctionne pas | Chrome NetLog | --origin-to-force-quic-on + événements MIGRATION de NetLog |
| 4 | Récupération de perte lente | qlog | qvis visualiser les événements de détection et récupération de perte |
| 5 | Problèmes de priorité de flux | Wireshark | Déchiffrer et analyser les trames HTTP/3 PRIORITY |
| 6 | Alt-Svc ne fonctionne pas | curl | curl -I https://example.com vérifier l'en-tête Alt-Svc |
| 7 | Comparaison de performance | curl | comparaison de temporisation curl --http2 vs curl --http3 |
| 8 | Perte de paquets anormale en production | Prometheus | alerte rate(quic_packets_lost_total[5m]) |
| 9 | Latence de handshake élevée | Prometheus | histogram_quantile(0.99, quic_handshake_duration_seconds) |
| 10 | Risque de rejeu 0-RTT | Wireshark + qlog | Capturer et vérifier les données 0-RTT pour les requêtes non idempotentes |
Outils recommandés
Lors du débogage HTTP/3, ces outils peuvent vous aider à analyser et vérifier :
- Recherche de code de statut HTTP : Utilisez /fr/network/http-status pour rechercher les significations des codes de statut liés à QUIC/HTTP3
- Encodeur Base64 : Utilisez /fr/encode/base64 pour encoder/décoder des certificats et des jetons
- Calculateur de hachage : Utilisez /fr/encode/hash pour calculer des valeurs de hachage pour l'intégrité de la configuration QUIC
Résumé : Le défi central du débogage HTTP/3 réside dans le chiffrement complet et le transport UDP de QUIC. Les 5 modèles de débogage de production ont des focales différentes : capture Wireshark pour l'analyse approfondie de niveau protocole (nécessite le décryptage SSLKEYLOGFILE), qlog pour l'analyse de journal standardisée entre implémentations, Chrome NetLog pour le débogage temps réel côté client, curl pour la validation rapide et l'intégration CI, et Prometheus+Grafana pour le monitoring continu en production. Commencez par curl pour une validation rapide, passez à Wireshark/qlog pour les problèmes profonds, et construisez toujours un système complet de métriques QUIC de monitoring et d'alertes en production.
Essayez ces outils exécutés localement dans le navigateur — aucune inscription requise →