Diagnostic de la latence des requêtes HTTP : De l'équilibreur de charge au noyau Linux
Les temps de réponse croissants ou les erreurs 5xx soudaines en production exigent une localisation instantanée et précise du problème. Au lieu de vérifier chaotiquement les services, il est plus efficace d'adopter une approche systématique qui divise l'ensemble du pipeline de traitement des requêtes en segments réseau et application. Nous décomposons une méthode pour identifier rapidement les goulots d'étranglement à l'aide des journaux standard de Nginx, des en-têtes Server-Timing et des métriques TCP_INFO de bas niveau.
Point de départ : Analyse des journaux de l'équilibreur de charge
Une architecture d'application web typique comprend plusieurs couches : utilisateur, CDN, équilibreur de charge (SLB), application, pool de connexions et base de données. Chacune peut introduire de la latence, mais l'équilibreur de charge doit toujours être votre point de départ pour les diagnostics — il est au centre du pipeline et permet de déterminer instantanément la direction de recherche.
Pour Nginx, les variables clés sont $request_time et $upstream_response_time. La première enregistre le temps de traitement complet de la requête, de la réception du premier octet du client à l'envoi du dernier octet de la réponse. La seconde mesure uniquement le temps d'attente d'une réponse du serveur upstream. Si ces métriques ne sont pas encore collectées, étendez votre format de journalisation :
log_format timing '$remote_addr - $request_uri '
'status=$status '
'rt=$request_time '
'uct=$upstream_connect_time '
'urt=$upstream_response_time';
access_log /var/log/nginx/access.log timing;
L'interprétation des données repose sur une simple comparaison. Si $upstream_response_time est proche de $request_time, le goulot d'étranglement se trouve à droite de l'équilibreur de charge : dans le code de l'application, le pool de connexions ou la base de données. Si $upstream_response_time est normal mais que $request_time est anormalement élevé, le problème est à gauche : dans l'infrastructure réseau, le routage CDN ou du côté client. Notez que l'injection de $request_time dans les en-têtes de réponse via add_header n'est pas recommandée pour des mesures précises, car Nginx forme les en-têtes avant d'avoir terminé la transmission du corps de la réponse. Les données fiables ne sont disponibles que dans le journal d'accès après fermeture de la connexion.
Identification précise des problèmes côté application
Lorsque l'équilibreur de charge indique des retards upstream, il faut creuser dans les opérations internes de l'application. La norme W3C Server-Timing permet d'envoyer des mesures précises pour les étapes individuelles de traitement de la requête vers le client ou le système de monitoring. L'en-tête ressemble à : Server-Timing: app;dur=120, db;dur=95, pool-wait;dur=18. Cela offre de la transparence sans déployer d'agents APM lourds.
L'intégration de l'en-tête dans votre pile d'applications nécessite des modifications minimales. Exemples d'implémentation pour les langages populaires :
Go (net/http) :
start := time.Now()
rows, err := db.QueryContext(ctx, query)
dbDur := time.Since(start)
w.Header().Set("Server-Timing",
fmt.Sprintf("db;dur=%.2f", float64(dbDur.Microseconds())/1000))
Python (middleware Django) :
class ServerTimingMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
start = time.monotonic()
response = self.get_response(request)
dur = (time.monotonic() - start) * 1000
response["Server-Timing"] = f"app;dur={dur:.2f}"
return response
Node.js (Express) :
app.use((req, res, next) => {
const start = process.hrtime.bigint();
res.on('finish', () => {
const ms = Number(process.hrtime.bigint() - start) / 1e6;
// already sent, but for logging:
console.log(`Server-Timing: app;dur=${ms.toFixed(2)}`);
});
// for header — time up to response:
const origEnd = res.end;
res.end = function(...args) {
const ms = Number(process.hrtime.bigint() - start) / 1e6;
res.setHeader('Server-Timing', `app;dur=${ms.toFixed(2)}`);
origEnd.apply(this, args);
};
next();
});
Pour transmettre les métriques propres à Nginx via Server-Timing, utilisez la directive map afin d'éviter d'envoyer des valeurs vides pour les fichiers statiques ou les erreurs :
Nginx (timing upstream en tant que Server-Timing) :
# Dans le bloc http {} de nginx.conf :
map $upstream_header_time $server_timing_upstream {
"-" "";
default "ngx-upstream;dur=$upstream_header_time;desc=\"NgxUpstream\"";
}
# Dans le bloc location {} de votre serveur :
add_header Server-Timing-Ngx "ngx-total;dur=$request_time;desc=\"NgxTotal\"" always;
add_header Server-Timing-Ngx $server_timing_upstream always;
Notez l'utilisation du préfixe Server-Timing-Ngx et de la variable $upstream_header_time. Les navigateurs attendent des valeurs en millisecondes, tandis que Nginx renvoie des secondes. De plus, $upstream_header_time capture le moment de réception des en-têtes de l'upstream, reflétant précisément le temps de génération de la réponse — contrairement à $upstream_response_time, qui peut être mis à zéro dans certaines conditions de mise en tampon.
L'analyse des données permet de classer rapidement l'incident :
- Un
db;durdominant pointe vers des requêtes SQL sous-optimales, des index manquants ou des verrous de table. - Un
pool-wait;durélevé signale l'épuisement de la limite de connexions dans PgBouncer ou un pooler similaire. - Un
app;durimportant avec undb;durfaible indique des opérations liées au CPU, des E/S bloquantes ou des fuites mémoire à l'exécution. - Des erreurs 502/504 sur l'équilibreur de charge avec des timings normaux signifient généralement des plantages de processus d'application ou des timeouts d'orchestrateur.
Segment réseau : Phases HTTP et métriques du noyau
Si l'upstream répond rapidement mais que la latence globale augmente, le problème est dans le chemin réseau. Toute requête HTTP passe par six phases séquentielles : DNS, Connect (handshake TCP), handshake TLS, Send, Wait (TTFB) et Receive. Des anomalies dans des phases spécifiques resserrent immédiatement la recherche. Un DNS long pointe vers des problèmes de résolveur ou des chaînes CNAME complexes. Des valeurs élevées pour Connect et TLS indiquent une latence réseau vers le nœud edge du CDN, l'absence de reprise de session TLS ou des protocoles obsolètes. Si Connect et TLS sont minimes mais que Wait est anormalement élevé, le délai se trouve au niveau du WAF, des workers CDN ou de la récupération à l'origine.
Pour des diagnostics réseau approfondis, les décompositions HTTP standard ne suffisent pas. Vous avez besoin des métriques du noyau Linux via getsockopt(TCP_INFO). Le noyau suit chaque connexion TCP et fournit des données précises : RTT, nombre de retransmissions, taille de la fenêtre de congestion (cwnd) et RTO. Ces métriques sont critiques car les timings HTTP ne peuvent pas séparer la latence réseau du temps de traitement serveur lorsque la réponse tient dans un segment TCP unique. Soustraire le RTT du noyau de la phase Wait donne le vrai temps de traitement de la requête. Les retransmissions et une cwnd réduite pointent directement vers des pertes de paquets et des algorithmes de contrôle de congestion ajoutant des centaines de millisecondes par requête. Combiner les en-têtes d'application avec des métriques réseau de bas niveau crée une observabilité complète sans dépendre de fournisseurs tiers.
Points clés
- Comparer
$request_timeet$upstream_response_timedans Nginx sépare instantanément les responsabilités réseau et application. - Les en-têtes Server-Timing offrent une visibilité granulaire sur les délais internes (BD, pools, exécution) sans systèmes APM lourds.
- Analyser les phases de requête HTTP identifie précisément les problèmes DNS, handshake TLS et routage CDN.
- Les métriques TCP_INFO du noyau donnent une vue objective des pertes et délais réseau indétectables au niveau application.
- Une approche diagnostique systématique réduit le MTTR et élimine les redémarrages chaotiques de services pendant les incidents.
— Editorial Team
Aucun commentaire pour le moment.