Depuración de HTTP/3 y QUIC: 5 patrones de producción desde la captura de paquetes hasta el análisis de rendimiento

网络协议

Por qué la depuración de HTTP/3 es 10x más difícil que HTTP/2

HTTP/3 se ejecuta sobre QUIC sobre UDP, lo que significa que 20 años de experiencia en depuración TCP son esencialmente inútiles. Los paquetes TCP son directamente legibles en Wireshark, los flujos HTTP/2 son visibles en Chrome DevTools — pero el tráfico QUIC está cifrado, haciendo los paquetes ilegibles; el multiplexado de flujos de HTTP/3 ocurre en la capa de transporte, invisible para las herramientas de nivel de aplicación.

En 2026, la adopción global de HTTP/3 supera el 45 % (datos de Cloudflare Radar). Sin embargo, cuando los desarrolladores encuentran problemas, a menudo solo ven "connection failed" sin forma de investigar. Este artículo resume 5 patrones de depuración validados en producción, desde la descifrado de capturas hasta el monitoreo en tiempo real, ayudándole a construir un kit de herramientas de depuración HTTP/3 completo.

Dimensión de depuración Herramienta HTTP/2 Herramienta HTTP/3 Cambio de dificultad
Captura de paquetes Wireshark (legible directamente) Wireshark + SSLKEYLOG Requiere descifrado de claves
Registro de protocolo Sin estándar qlog (formato estandarizado) Nuevo concepto
Depuración de cliente Chrome DevTools Chrome NetLog Nivel más bajo
Depuración CLI curl -v curl --http3 + variables de entorno Requiere compilación
Monitoreo de producción Métricas TCP Métricas específicas QUIC Sistema de métricas distinto

Patrón 1: Captura y disección de paquetes QUIC en Wireshark

Problema central: QUIC está totalmente cifrado

QUIC integra TLS 1.3 directamente en el protocolo. Todos los frames, incluidos los encabezados, están cifrados. Las capturas de QUIC en Wireshark solo muestran payloads UDP — los frames internos HTTP/3 no pueden analizarse. Para descifrar el tráfico QUIC, debe obtener las claves de sesión TLS.

Mecanismo SSLKEYLOGFILE

SSLKEYLOGFILE es el mecanismo estándar de depuración TLS. Los clientes que lo admiten (Chrome, Firefox, curl, Go) escriben las claves de sesión TLS en un archivo especificado. Wireshark lee este archivo para descifrar el tráfico.

# Establecer variable de entorno SSLKEYLOGFILE
export SSLKEYLOGFILE=/tmp/sslkeys.log

# Capturar tráfico HTTP/3 con curl (claves escritas automáticamente)
curl --http3 https://example.com -v

# Acceder con Chrome (claves escritas automáticamente)
google-chrome --ssl-key-log-file=/tmp/sslkeys.log

# Acceder con Firefox
export MOZ_LOG="ssl:5"
export SSLKEYLOGFILE=/tmp/sslkeys.log
firefox

Configuración de descifrado QUIC en Wireshark

# 1. Iniciar captura en Wireshark (filtrar tráfico QUIC)
# Filtro de captura:
udp port 443

# 2. Configurar archivo de registro de claves en Wireshark
# Edit -> Preferences -> Protocols -> TLS -> (Pre)-Master-Secret log filename
# Ingresar: /tmp/sslkeys.log

# 3. Verificar éxito del descifrado QUIC
# Antes del descifrado: los paquetes QUIC muestran "Protected Payload, PKN: ..."
# Después del descifrado: frames HTTP/3 visibles (HEADERS, DATA, SETTINGS, etc.)

Análisis de paquetes por línea de comandos con tshark

# Capturar tráfico QUIC con descifrado
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

# Extraer encabezados de solicitud 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

# Analizar información de handshake de conexión 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

# Analizar pérdida de paquetes y retransmisión 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

Exportación de claves en servidor 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
}

Identificación de versión QUIC

# Números de versión QUIC comunes
# RFC 9000 (QUIC v1):        0x00000001
# RFC 9369 (QUIC v2):        0x6b3343cf
# Google QUIC (gQUIC):       0x51303039 (Q039)
# IETF Draft 29:             0xff00001d

# Filtrar versión específica con tshark
tshark -i eth0 -f "udp port 443" \
  -Y "quic.version == 0x00000001" \
  -T fields \
  -e quic.connection_id \
  -e quic.version

Patrón 2: Análisis y visualización qlog

Qué es qlog

qlog es el formato de registro QUIC/HTTP/3 estandarizado por la IETF (RFC 9657). Define un modelo de eventos unificado que permite la interoperabilidad entre diferentes implementaciones QUIC. A diferencia de los formatos de registro propietarios, qlog le permite analizar registros de quiche, lsquic, quic-go, ngtcp2 y otras implementaciones con las mismas herramientas.

{
  "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"}
            ]
          }
        }]
      ]
    }
  ]
}

Recolección qlog del lado del servidor

# 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;
    }
}

Recolección qlog con 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)
}

Herramientas de visualización qlog

# Visualización qlog: qvis
# Herramienta en línea: https://qvis.quictools.info/
# Subir archivo .qlog para visualización

# Instalar qlog-converter
npm install -g qlog-converter

# Convertir qlog binario a formato JSON
qlog-converter -i binary.qlog -o json.qlog --format JSON

# Convertir JSON qlog a texto legible
qlog-converter -i json.qlog -o readable.txt --format TEXT

# Generar informe de visualización
npm install -g qlog-visualizer
qlog-visualizer -i connection.qlog -o report.html

Análisis de eventos clave qlog

# Analizar temporización de 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\", \"?\")}')
"

# Analizar pérdida de paquetes y retransmisión
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)}')
"

# Analizar temporización a nivel de flujo
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]}')
"

Patrón 3: Chrome NetLog para depuración HTTP/3

Por qué DevTools no es suficiente

El panel Network de Chrome DevTools solo muestra información de nivel de aplicación (encabezados de solicitud, cuerpos de respuesta, temporización). No puede ver detalles de la capa de transporte QUIC (migración de conexión, estado 0-RTT, prioridades de flujo, recuperación de pérdidas). Para depurar problemas de transporte HTTP/3, debe usar Chrome NetLog.

Iniciar Chrome con captura NetLog

# Método 1: flags de línea de comandos
google-chrome \
  --enable-logging=netlog \
  --net-log-capture-mode=Everything \
  --net-log=/tmp/chrome-netlog.json

# Método 2: vista en tiempo real de chrome://net-internals
# 1. Navegar a chrome://net-internals/#export
# 2. Hacer clic en "Start logging to disk"
# 3. Realizar la operación que desea depurar
# 4. Hacer clic en "Stop logging"

# Método 3: Chrome DevTools Protocol
google-chrome --remote-debugging-port=9222

# Disparar NetLog vía CDP
curl -s http://localhost:9222/json/version | python3 -m json.tool

Análisis de eventos NetLog

# La salida de NetLog es formato JSON con todos los eventos de red
# Estructura típica:
# {
#   "constants": { ... },
#   "events": [
#     {"time": ..., "type": "HTTP3_SESSION_INITIALIZED", ...},
#     {"time": ..., "type": "QUIC_SESSION_PACKET_SENT", ...},
#     ...
#   ]
# }

# Extraer eventos relacionados con 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\",\"?\")}')
"

# Analizar temporización de establecimiento de conexión 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}')
"

# Verificar estado 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')
"

# Analizar migración de conexión
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')
"

Depuración en tiempo real con chrome://net-internals

Páginas clave de chrome://net-internals:

#h3              - lista de sesiones HTTP/3
#quic            - lista y configuración de conexiones QUIC
#sockets         - estado de sockets UDP
#dns             - resolución DNS (incluidos registros HTTPS)
#httpCache       - estado de caché
#altSvc          - contenido de caché Alt-Svc
# A través de chrome://net-internals/#quic ver:
# - Conexiones QUIC activas
# - Versión, CID, estado de cada conexión
# - Parámetros de configuración QUIC
# - Información de errores de conexión

# A través de chrome://net-internals/#h3 ver:
# - Estado de sesión HTTP/3
# - Creación y cierre de flujos
# - Relaciones de dependencia de prioridad
# - Estado de push

Depuración de caché Alt-Svc

# Ver caché Alt-Svc
# chrome://net-internals/#altSvc

# Problema común: caché Alt-Svc expirado o incorrecto
# Limpiar caché Alt-Svc:
# 1. Abrir chrome://net-internals/#altSvc
# 2. Hacer clic en "Clear alt-svc cache"
# 3. Visitar el sitio objetivo nuevamente

# Forzar HTTP/3 (omitir descubrimiento Alt-Svc)
google-chrome \
  --origin-to-force-quic-on=example.com:443 \
  --net-log=/tmp/chrome-forced-h3.json

Patrón 4: Depuración curl HTTP/3

Compilar curl con soporte HTTP/3

# Compilar curl con HTTP/3 en Ubuntu 22.04+
# Método: boringssl + nghttp3 + ngtcp2

# Instalar dependencias
sudo apt-get install -y build-essential cmake git

# Compilar boringssl
git clone https://boringssl.googlesource.com/boringssl
cd boringssl
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=on .
make -j$(nproc)
cd ..

# Compilar 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 ..

# Compilar nghttp3
git clone https://github.com/ngtcp2/nghttp3
cd nghttp3
autoreconf -fi
./configure
make -j$(nproc)
sudo make install
cd ..

# Compilar curl con 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 ..

# Verificar
curl --version | grep -i http3
# La salida debe incluir: Features: ... HTTP3 ...

Depuración básica de solicitud HTTP/3

# Enviar solicitud HTTP/3 con salida detallada
curl --http3 https://example.com -v

# Ejemplo de salida:
# *   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

# Forzar solo HTTP/3 (sin fallback)
curl --http3-only https://example.com -v

# Probar 0-RTT
# Primera solicitud: 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

# Segunda solicitud: 0-RTT (reutilizar sesión)
curl --http3 https://example.com -w "time_connect: %{time_connect}\ntime_appconnect: %{time_appconnect}\ntime_total: %{time_total}\n" -o /dev/null -s

Análisis de temporización curl

# Salida de temporización completa
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

# Comparar tiempo de conexión 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

# Prueba por lotes (10 ejecuciones, promedio)
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

Depuración con variables de entorno curl

# Habilitar registro interno QUIC
export SSLKEYLOGFILE=/tmp/curl-quic-keys.log

# Habilitar registro detallado ngtcp2
export NGTCP2_DEBUG_LOG=1

# Habilitar registro detallado nghttp3
export NGHTTP3_DEBUG_LOG=1

# Enviar solicitud
curl --http3 https://example.com -v 2>&1 | tee /tmp/curl-h3-debug.log

# Analizar registro
grep -i "handshake\|0-rtt\|stream\|frame" /tmp/curl-h3-debug.log

# Probar versión QUIC específica
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)

Simular problemas de red con curl

# Simular alta latencia
curl --http3 https://example.com -v \
  --limit-rate 100k \
  --connect-timeout 5 \
  --max-time 30

# Probar manejo de timeout de conexión
curl --http3-only https://unreachable.example.com -v \
  --connect-timeout 3 \
  --max-time 10

# Probar descubrimiento Alt-Svc
# Primero obtener encabezado Alt-Svc vía HTTP/2
curl --http2 https://example.com -v -I 2>&1 | grep -i alt-svc

# Luego conectar manualmente vía HTTP/3
curl --http3 https://example.com -v

# Enviar archivo grande para probar control de flujo
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 @-

Patrón 5: Monitoreo QUIC de producción

Recolección de métricas QUIC con 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))
}

Exportación de métricas QUIC en 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;
    }
}
# Usar nginx-prometheus-exporter para recolectar métricas de Nginx
docker run -d --name nginx-exporter \
  -p 9113:9113 \
  nginx/nginx-prometheus-exporter:1.3 \
  --nginx.scrape-uri=http://nginx:80/nginx_status

# Analizador de registro QUIC personalizado (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

Configuración de panel 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"
          }
        ]
      }
    ]
  }
}

Reglas de alerta

# 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 }}"

Comparación de los 5 patrones de depuración

Patrón Caso de uso Fortalezas Limitaciones Curva de aprendizaje
Captura Wireshark Problemas a nivel de protocolo, análisis de handshake La más profunda, completa Requiere claves, sin análisis en vivo Alta
Análisis qlog Comparación entre implementaciones, optimización Estandarizado, visualizable Requiere soporte de implementación Media
Chrome NetLog Problemas de cliente, migración de conexión Nativo del navegador, en tiempo real Solo Chrome, gran volumen de datos Media
Depuración curl Validación rápida, integración CI CLI, scriptable Requiere compilación, funciones limitadas Baja
Monitoreo de producción Alertas de producción, análisis de tendencias En tiempo real, vista global Requiere instrumentación, sin detalle recordado Media-Alta

Referencia rápida de 10 escenarios comunes de depuración

# Escenario Patrón recomendado Comando/herramienta clave
1 Fallo de handshake QUIC Wireshark tshark -f "udp port 443" -o "tls.keylog_file:keys.log" -Y "quic.packet_type==initial"
2 0-RTT rechazado Chrome NetLog chrome://net-internals/#quic buscar eventos ZERO_RTT
3 Migración de conexión no funciona Chrome NetLog --origin-to-force-quic-on + eventos MIGRATION de NetLog
4 Recuperación de pérdida lenta qlog qvis visualizar eventos de detección y recuperación de pérdida
5 Problemas de prioridad de flujo Wireshark Descifrar y analizar frames HTTP/3 PRIORITY
6 Alt-Svc no funciona curl curl -I https://example.com verificar encabezado Alt-Svc
7 Comparación de rendimiento curl comparación de temporización curl --http2 vs curl --http3
8 Pérdida de paquetes anormal en producción Prometheus alerta rate(quic_packets_lost_total[5m])
9 Latencia de handshake elevada Prometheus histogram_quantile(0.99, quic_handshake_duration_seconds)
10 Riesgo de repetición 0-RTT Wireshark + qlog Capturar y verificar datos 0-RTT para solicitudes no idempotentes

Herramientas recomendadas

Durante la depuración HTTP/3, estas herramientas pueden ayudarle a analizar y verificar:

  • Búsqueda de código de estado HTTP: Use /es-419/network/http-status para buscar los significados de códigos de estado relacionados con QUIC/HTTP3
  • Codificador Base64: Use /es-419/encode/base64 para codificar/decodificar certificados y tokens
  • Calculadora de hash: Use /es-419/encode/hash para calcular valores hash para la integridad de la configuración QUIC

Resumen: El desafío central de la depuración HTTP/3 radica en el cifrado completo y el transporte UDP de QUIC. Los 5 patrones de depuración de producción tienen enfoques diferentes: captura Wireshark para análisis profundo a nivel de protocolo (requiere descifrado SSLKEYLOGFILE), qlog para análisis de registro estandarizado entre implementaciones, Chrome NetLog para depuración en tiempo real del lado del cliente, curl para validación rápida e integración CI, y Prometheus+Grafana para monitoreo continuo de producción. Comience con curl para validación rápida, escale a Wireshark/qlog para problemas profundos, y construya siempre un sistema completo de monitoreo y alertas de métricas QUIC en producción.

Prueba estas herramientas que se ejecutan en tu navegador — no requieren registro →

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