Cloud

Azure SQL : diagnostiquer l'épuisement du pool de connexions avant de scaler

Un runbook de production pour séparer fuite de connexions, saturation SQL, latence réseau et renouvellement d'identité avant d'augmenter le service tier.

17 août 2026 azureazure-sqlconnection-poolmanaged-identityobservabilitykqlperformanceautomationrunbookrollbackproduction

Une API commence à répondre lentement, puis remonte des erreurs du type Timeout expired prior to obtaining a connection from the pool. Le CPU Azure SQL reste pourtant acceptable et quelques requêtes manuelles aboutissent. Sous pression, deux réponses paraissent immédiates : augmenter la taille de la base ou relever la limite du pool.

Ces actions peuvent masquer une fuite de connexions, multiplier les sessions inutiles et déplacer l’incident vers la limite suivante. Ce runbook traite le cas d’une application distribuée qui utilise Azure SQL avec un pool par processus. L’objectif est de déterminer si la contrainte se trouve dans le processus applicatif, le moteur SQL, le réseau ou l’identité, puis de décider entre correction, scaling temporaire ou rollback.

Figer un échantillon représentatif

Commencez par relier une erreur à une révision, une instance et une base précises. Un pool est généralement local à un processus : dix replicas configurés avec un maximum de 100 connexions ne forment pas un pool de 100, mais une capacité théorique de 1 000 connexions.

yaml azure-sql-pool-incident.yml
incident: inc-20260817-014
window_utc:
start: 2026-08-17T08:10:00Z
end: 2026-08-17T08:40:00Z

application:
service: orders-api
revision: release-2026.08.17.2
instances: 12
runtime: dotnet
pool_max_per_process: 100

database:
logical_server: sql-prod-weu
database: orders
service_tier: <current-tier>

evidence:
- exact exception and stack
- role instance and operation ID
- SQL session and request snapshot
- connection success, failure and latency
- recent deployment, scale or identity change

temporary_constraints:
- do not raise pool size before counting all processes
- do not restart every replica at once
- do not increase database tier without a server-side limit
- preserve one failing instance for evidence

Vérifiez si l’erreur touche toutes les instances ou seulement la nouvelle révision. Une seule instance qui dérive oriente vers une fuite locale. Toutes les instances avec une hausse simultanée des temps SQL orientent plutôt vers la base ou une dépendance commune. Une vague d’échecs après un scale-out peut simplement révéler la multiplication des pools.

Définir le contrat de connexion

Avant de lire les métriques, inventoriez ce que l’application est censée faire. Notez la taille maximale du pool par processus, le nombre maximal de processus, le délai d’acquisition, le délai de commande, la politique de retry et le mode d’authentification.

Une connexion doit être rendue au pool après chaque unité de travail. Une transaction longue, un lecteur non fermé, un chemin d’exception qui oublie Dispose ou une requête aval lente peut retenir la connexion sans activité SQL visible. Le retry amplifie alors la demande : chaque requête bloquée en crée une autre avant que la première ait libéré sa ressource.

Calculez une enveloppe simple :

text pool-capacity-envelope.txt
Theoretical connection ceiling
application replicas x worker processes x pool max

Compare with
observed concurrent requests
observed SQL sessions
active SQL requests
database session and worker limits

Example interpretation
12 replicas x 1 process x pool max 100 = 1,200 possible sessions
70 observed SQL sessions + local pool timeout on one replica
=> investigate local retention before scaling Azure SQL

Cette enveloppe n’est pas un objectif de consommation. Elle sert à rendre visible le risque créé par un pool trop large et un autoscaling applicatif non borné.

Prouver l’état côté Azure SQL

Prenez plusieurs snapshots espacés de quelques secondes. Un seul instant peut manquer une rafale. Les vues dynamiques exigent les permissions de lecture adaptées ; utilisez un compte d’observation dédié et ne distribuez pas un rôle d’administration pour le diagnostic.

sql 01-azure-sql-session-snapshot.sql
SELECT
  s.status,
  s.login_name,
  s.program_name,
  s.host_name,
  s.client_interface_name,
  COUNT(*) AS session_count,
  SUM(CASE WHEN r.session_id IS NOT NULL THEN 1 ELSE 0 END) AS active_requests,
  MIN(s.login_time) AS oldest_login_time,
  MAX(s.last_request_end_time) AS latest_request_end_time
FROM sys.dm_exec_sessions AS s
LEFT JOIN sys.dm_exec_requests AS r
  ON r.session_id = s.session_id
WHERE s.is_user_process = 1
GROUP BY
  s.status,
  s.login_name,
  s.program_name,
  s.host_name,
  s.client_interface_name
ORDER BY session_count DESC;

SELECT
  r.session_id,
  r.status,
  r.command,
  r.wait_type,
  r.wait_time,
  r.blocking_session_id,
  r.cpu_time,
  r.total_elapsed_time
FROM sys.dm_exec_requests AS r
WHERE r.session_id <> @@SPID
ORDER BY r.total_elapsed_time DESC;

Interprétez les deux plans ensemble :

  • beaucoup de sessions sleeping pour une seule révision, avec peu de requêtes actives : rétention ou pool surdimensionné côté application ;
  • requêtes actives nombreuses, attentes longues ou blocages : diagnostiquer la charge SQL avant de toucher au pool ;
  • peu de sessions visibles alors que l’application expire en acquisition : problème local au processus, chaîne de connexion fragmentée ou erreur avant l’ouverture ;
  • connexions échouées pour toutes les instances : vérifier réseau, firewall, DNS, limite de service ou authentification.

Ne tuez pas en masse les sessions sleeping. Elles peuvent être saines et réutilisables. Identifiez d’abord leur origine et confirmez qu’elles dépassent le comportement attendu du pool.

Corréler dépendances et instances avec KQL

Les traces Application Insights permettent de comparer révisions et instances. Adaptez les noms de dimensions à votre instrumentation ; le principe est de conserver le même intervalle UTC et de séparer acquisition locale, connexion SQL et durée de commande.

kusto 02-sql-dependency-by-instance.kql
let Window = 2h;
dependencies
| where timestamp > ago(Window)
| where type =~ "SQL"
| summarize
  Calls=count(),
  Failures=countif(success == false),
  P95Duration=percentile(duration, 95),
  Results=make_set(resultCode, 8)
by cloud_RoleName, cloud_RoleInstance, target, bin(timestamp, 5m)
| order by timestamp desc, Failures desc

Ajoutez une trace structurée lors de l’acquisition : durée d’attente, état succès/échec, instance, révision et identifiant d’opération, sans journaliser la chaîne de connexion ni le jeton. Si les logs ne distinguent pas attente du pool et exécution SQL, l’incident révèle d’abord une lacune d’observabilité.

Comparez ensuite les métriques Azure SQL disponibles dans votre workspace : pourcentage de sessions, workers, CPU, connexions réussies ou échouées et deadlocks. Le nom exact des métriques dépend de la configuration d’export ; listez les métriques présentes avant d’écrire une alerte permanente.

Écarter réseau et identité

Un pool vide parce que les nouvelles connexions échouent ressemble parfois à un pool plein. Testez la résolution DNS et l’ouverture TCP depuis le même subnet et la même identité que l’application. Un Private Endpoint peut être présent dans l’architecture, mais son rôle ici se limite au chemin d’accès : il ne prouve ni la disponibilité du moteur ni la bonne gestion du pool.

Avec une identité managée, contrôlez séparément l’obtention du jeton et la connexion SQL. Une implémentation qui redemande un jeton pour chaque opération ou fragmente les pools en variant la chaîne de connexion peut créer de la latence et trop de pools distincts. Conservez un cache conforme au SDK utilisé, une chaîne stable et une expiration de connexion compatible avec la durée de vie du jeton.

Ne remplacez pas immédiatement l’identité managée par un secret client. Si le jeton échoue, capturez le code, l’heure, l’identité réelle et le scope demandé, puis corrigez ce plan sans modifier la capacité SQL.

Corriger avec un canari borné

La correction dépend de la preuve :

  • fuite ou Dispose manquant : corriger le chemin fautif et déployer une seule instance canari ;
  • transaction ou commande longue : réduire sa portée, corriger la requête ou lever le blocage ;
  • retry amplificateur : borner tentatives, délai total et jitter ;
  • pool fragmenté : stabiliser la chaîne de connexion et les options qui définissent l’identité du pool ;
  • trop de pools après scale-out : borner le scaling applicatif et recalculer l’enveloppe globale ;
  • limite SQL réellement atteinte avec connexions utiles : augmenter temporairement la capacité, avec seuil de retour documenté.

Le canari doit recevoir une fraction mesurable du trafic. Surveillez acquisition P95, erreurs, sessions SQL, requêtes actives et latence métier. Arrêtez l’expérience si les timeouts augmentent, si les sessions progressent sans redescendre ou si la base approche une limite plus vite que le trafic utile.

Valider, décider ou rollback

La validation ne se limite pas à la disparition de l’exception pendant cinq minutes. Elle doit traverser un pic représentatif et un cycle de scale applicatif.

text azure-sql-pool-decision.txt
Validate
Pool acquisition P95 returns to the target range
Error rate stays stable through a representative peak
SQL sessions follow traffic and return after the peak
Active requests and waits remain explainable
Managed identity token acquisition remains healthy
Scale-out does not multiply sessions beyond the envelope

Rollback application change
Route traffic back to the previous revision
Drain the canary without killing healthy SQL sessions
Confirm errors, sessions and business latency recover

Rollback temporary scaling
Keep the larger tier until the application fix is proven
Return to the previous tier during a controlled window
Recheck session, worker, CPU and latency headroom

Do not close the incident when
Restarting replicas is the only action that works
Pool wait and SQL execution are still indistinguishable
Session growth has no owner or stop condition

Conclusion

Un timeout de pool Azure SQL n’est pas, à lui seul, une preuve de sous-dimensionnement de la base. Le pool appartient au processus applicatif, tandis que les limites de sessions, workers et ressources appartiennent au service SQL. Le diagnostic doit relier ces deux plans avec le réseau et l’identité.

La bonne sortie est une décision vérifiable : corriger la rétention locale, lever une contention SQL, réparer le chemin de connexion ou scaler temporairement parce qu’une limite réelle est prouvée. Dans tous les cas, le canari, les critères d’arrêt et le rollback empêchent qu’une hausse de capacité ne transforme une fuite discrète en incident plus large.