Diagnostiquer les timeouts de connexion SQL Server
Categories:
58 minutes à lire
Un timeout de connexion recouvre une dizaine de causes, et la moitié n’atteint jamais SQL Server. La méthode consiste à établir, mesures en main, ce que le serveur a vu pendant la fenêtre de l’incident (ring buffer de connectivité, system_health, journal d’erreurs, workers, entrées-sorties), en vérifiant que chaque source couvrait bien cette fenêtre. Si le serveur n’a rien vu, ce résultat désigne la machine cliente et le chemin réseau.
Trois mécanismes du client trompent l’enquête : le repli de TCP sur les canaux nommés, qui fait nommer Named Pipes à une erreur TCP ; la file d’attente du pool plein, qui échoue sans contacter le serveur ; et la période de blocage de SqlClient, qui multiplie une même exception par dizaines sans nouvelle tentative.
Un éditeur de logiciel signale que son application perd par intermittence ses connexions à SQL Server. Ses journaux se remplissent d’erreurs, des écritures se perdent, et sa position est simple : l’application n’a pas changé, c’est donc l’infrastructure. Côté serveur, le DBA regarde le journal d’erreurs et n’y trouve rien.
Cet article reprend la démarche suivie sur un cas réel de ce type, en production, sur une instance SQL Server 2019 hébergée dans une machine virtuelle Azure et infogérée par un tiers : une quinzaine de services applicatifs répartis sur une dizaine de machines, connectés sous un même login SQL. Il est écrit pour être réutilisé par une équipe qui affronte le même symptôme. Les chiffres sont ceux du cas réel, ou ceux d’une instance de test SQL Server 2025 sous Linux quand il fallait reproduire un comportement ; chaque mesure dit d’où elle vient. Une affirmation qui n’est ni mesurée ni documentée par Microsoft est présentée comme une interprétation.
Ce que recouvre un timeout de connexion
Une alerte de connectivité ne dit presque jamais ce qui se passe. Plusieurs familles de causes produisent des symptômes voisins côté client, et elles ne se traitent pas de la même façon. Certaines laissent une trace sur le serveur, d’autres ne l’atteignent jamais.
| Famille | Symptôme côté client | Visible côté serveur | Où regarder |
|---|---|---|---|
| Le serveur n’écoute pas, ou pas où le client croit | connexion refusée, serveur introuvable | non | sys.dm_tcp_listener_states, pare-feu, port dynamique, SQL Server Browser |
| Authentification refusée | Login failed | oui | journal d’erreurs, état de l’erreur 18456 |
| Kerberos, SPN | Cannot generate SSPI context | en général non, l’échec a lieu côté client | SPN du compte de service, page Microsoft sur cette erreur |
| Authentification lente | délai de connexion dépassé | en partie | compteurs Sspi du ring buffer de connectivité, contrôleur de domaine |
| TLS | connexion fermée par l’hôte distant, échec de la négociation | en partie | compteurs Ssl du ring buffer, journal Schannel |
| Workers épuisés | délai dépassé, alors que les requêtes déjà en cours continuent | oui | work_queue_count, attente THREADPOOL, messages 17884 et 17189 |
| Base indisponible | Login failed, base introuvable | oui | sys.databases |
| Pool de connexions plein | « prior to obtaining a connection from the pool » | non | journaux applicatifs, connexions par hôte et programme |
| Ports éphémères épuisés sur le client | délai dépassé, par intermittence | non | netstat sur la machine cliente, pendant l’épisode |
| Résolution de nom défaillante | serveur introuvable, délai dépassé | non | Resolve-DnsName depuis la machine cliente |
| Antivirus ou filtre réseau sur le client | échecs intermittents sans motif | non | pilotes de filtre, fournisseurs Winsock |
| Coupure réseau en amont | connexion fermée brutalement, erreur 10054 | parfois | ring buffer de connectivité, équipements réseau |
La colonne « visible côté serveur » organise toute la suite. Si l’analyse du serveur est menée correctement et ne trouve rien, ce résultat est une information : il désigne les familles invisibles, donc la machine cliente et le chemin réseau.
Avant la première requête
Les questions à poser
Avant toute requête, obtenir de qui signale le problème :
- qui n’arrive pas à se connecter : quelle machine, quel service ;
- depuis quand, et à quelles heures précises ;
- avec quel message exact côté client, pile d’appels comprise ;
- s’il s’agit de l’application ou d’une sonde de supervision.
Sans ces réponses, on cherche dans toutes les directions à la fois. Dans le cas réel, la quatrième question s’est révélée décisive pour une alerte de supervision, et la troisième pour les erreurs de l’éditeur.
Le message d’erreur du client
Le texte de l’exception porte souvent plus d’information que l’équipe qui le transmet ne le pense. Les libellés ci-dessous sont ceux de SqlClient, le pilote .NET ; les autres pilotes ont les leurs, et SqlClient traduit ses messages selon la langue de l’application.
| Message côté client | Ce qu’il désigne |
|---|---|
Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. | Le pool de connexions du client est plein. Aucune requête n’est partie vers le serveur (voir la file d’attente du pool plein) |
Connection Timeout Expired. The timeout period elapsed while attempting to consume the pre-login handshake acknowledgement. | La connexion TCP est établie, mais l’échange de pré-login, négociation TLS comprise, n’a pas abouti à temps : serveur saturé, réseau, TLS. Sous .NET Framework, quand le nom rend plusieurs adresses, la résolution d’adresse transparente (TransparentNetworkIPResolution) découpe aussi le délai entre elles |
Connection Timeout Expired. The timeout period elapsed during the post-login phase. | Le paquet de login est parti, et le serveur n’a pas répondu à temps. Cette phase couvre tout le travail du serveur : authentification, CHECK_POLICY, déclencheurs de connexion, classifieur du Resource Governor, ouverture de la base. Le message cite aussi la création des connexions MARS. Elle ne prouve pas que l’authentification a abouti |
A network-related or instance-specific error occurred ... (provider: Named Pipes Provider, error: 40 - Could not open a connection to SQL Server) | Avec SqlClient sous Windows, TCP a échoué avant, puis le repli sur les canaux nommés aussi (voir le repli sur les canaux nommés) |
A network-related or instance-specific error occurred ... (provider: TCP Provider, error: 0 - ...) | Échec TCP direct : nom, port, pare-feu, réseau |
Login failed for user '...' | Le serveur a reçu la demande et l’a refusée ; l’état de l’erreur 18456 dans le journal du serveur dit pourquoi |
Les messages Connection Timeout Expired de SqlClient sont suivis des durées passées dans chaque phase, du pré-login au post-login : elles disent où le délai a été consommé. Le guide de dépannage de SqlClient en montre un exemple. Dans le code du pilote (Microsoft.Data.SqlClient 7.1.1, SqlConnectionInternal.cs), la phase post-login commence juste après l’envoi du paquet de login, avant la lecture de la réponse du serveur.
Pour chercher un de ces libellés dans un journal, prendre un fragment (prior to obtaining a connection from the pool) plutôt que la phrase entière : le texte d’origine met deux espaces après chaque point, et SqlClient le traduit selon la langue de l’application.
Dans le cas réel, l’éditeur citait Named Pipes Provider, error: 40. Ce seul nom de fournisseur a orienté une partie de l’enquête, et il a fallu le lire avec sa documentation pour ne pas en tirer une conclusion fausse.
Le fuseau horaire des deux côtés
Sur une machine virtuelle Azure, l’horloge du serveur est souvent en UTC. Les journaux applicatifs sont souvent en heure locale, sans le dire. Rapprocher une erreur client d’un événement serveur avec une ou deux heures d’écart fait conclure à l’absence d’événement.
Côté serveur, une ligne lève le doute :
-- Heure locale du serveur, heure UTC et décalage
SELECT SYSDATETIME() AS heure_serveur,
SYSUTCDATETIME() AS heure_utc,
SYSDATETIMEOFFSET() AS avec_decalage;
Côté client, quand le fuseau d’un journal n’est pas connu, l’heure d’envoi du message qui le cite fournit une borne. Dans le cas réel, le mail de l’éditeur est parti à 09:26:54 UTC et citait une erreur horodatée 10:13:06. En UTC, l’erreur serait postérieure à son propre envoi : le journal n’était donc pas en UTC. Ce calcul écarte l’UTC, il ne donne pas le décalage ; à la date de l’incident, Paris était à l’heure d’été, UTC+2, et l’échec avait eu lieu à 08:13:06 UTC. Le décalage se confirme par une seconde source, un autre événement daté des deux côtés par exemple. C’est ce qui a permis le rapprochement avec le ring buffer du serveur.
Ce que le serveur a vu
L’objectif de cette étape est de pouvoir écrire, mesures en main, ce que le serveur a enregistré et ce qu’il n’a pas enregistré pendant la fenêtre de l’incident. Les requêtes complètes sont dans l’annexe A. Aucune n’écrit quoi que ce soit, à part les tables temporaires de la mesure des débits ; les sessions Extended Events de l’annexe B se créent et se suppriment explicitement.
Le cadrage
Quatre questions d’abord : l’instance tourne-t-elle et depuis quand, une base est-elle indisponible, les workers sont-ils épuisés, sur quoi l’instance écoute-t-elle.
Un démarrage récent explique beaucoup de choses à lui seul, et il signale surtout que les ring buffers ont été vidés. Une base hors ligne explique tout. Une valeur work_queue_count non nulle dans sys.dm_os_schedulers signifie que des tâches attendent un worker : quand elle persiste, la saturation de l’instance devient la piste principale. Le journal d’erreurs en garde la trace par le message 17884 (« New queries assigned to process on Node … have not been picked up by a worker thread ») ; le message 17189 signale, lui, l’échec de création d’un thread pour traiter un nouveau login.
Dans le cas réel, sur 8 h 20 de fonctionnement : 22 attentes THREADPOOL pour 47 ms cumulées, 86 workers actifs sur 704 possibles, aucune erreur 17189. L’épuisement des workers était écarté.
Le ring buffer de connectivité
C’est la source la plus directe, et elle est historique, donc exploitable après coup. sys.dm_os_ring_buffers, filtrée sur ring_buffer_type = 'RING_BUFFER_CONNECTIVITY', rend un tampon circulaire en mémoire où SQL Server consigne les incidents de connexion. Le billet de l’équipe SQL Protocols qui l’a présenté avec SQL Server 2008 dit ce qu’il capture : chaque fermeture de connexion décidée par le serveur, un KILL et un login refusé compris. Microsoft le décrit aussi, dans sa page sur les problèmes d’authentification intermittents, comme un journal historique dont les compteurs de login disent où le temps est passé : sur le réseau, dans les API SSL ou SSPI, ou en file d’attente.
Chaque enregistrement porte un type (Error, LoginTimers ou ConnectionClose) et l’adresse et le port distants ; les Error et les LoginTimers portent en plus l’erreur rencontrée, les ConnectionClose en général non, sauf après une coupure (10057, ou 10054 dans leurs tampons). Un login refusé laisse un Error suivi d’un ConnectionClose, en plus de sa ligne 18456 dans le journal d’erreurs. Les enregistrements LoginTimers portent aussi un bloc LoginTimersInMilliseconds, dont le compteur dominant désigne l’étape qui a pris le temps. Microsoft ne donne que la lecture générale (réseau, SSL ou SSPI, file d’attente) ; les lectures plus fines du tableau sont une interprétation :
| Compteur élevé | Lecture | Suite |
|---|---|---|
EnqueueTime | la tâche a attendu un worker | saturation de l’instance, workers, entrées-sorties |
NetReadsTime | le serveur attend que le client envoie | client lent ou réseau |
NetWritesTime | le serveur attend que le client lise | client lent ou réseau |
Ssl/TotalTime | négociation TLS lente ou en échec | journal Schannel, protocoles autorisés |
Sspi/TotalTime | authentification Windows lente | contrôleur de domaine, Kerberos, SPN |
TriggerAndResGovTime/LogonTriggers | un déclencheur de connexion ralentit tout le monde | sys.server_triggers |
TriggerAndResGovTime/ExecClassifier | la fonction de classification du Resource Governor est coûteuse | la fonction de classification |
Un parsing qui renvoie uniquement des NULL est presque toujours un nom d’élément faux. Les noms ont changé depuis la première version du tampon, et ils diffèrent d’un type d’enregistrement à l’autre sur un même build. Mesuré sur l’instance de test, SQL Server 2025 CU7, et identique sur un enregistrement SQL Server 2019 du cas réel : un enregistrement LoginTimers range ses tampons sous TdsBufInfo, avec InputBufError et InputBufBytes, et nomme sa connexion SniConnId, alors qu’un enregistrement Error ou ConnectionClose écrit TdsBuffersInformation, TdsInputBufferError, TdsInputBufferBytes et SniConnectionId. Une requête qui ne lit qu’une des deux formes rend NULL dans cette colonne pour les autres types, sans erreur, et un filtre sur cette colonne les écarte. Avant d’écrire la requête, afficher un enregistrement brut de chaque type :
-- Un enregistrement brut de chaque type du ring buffer de connectivité
SELECT t.type_enregistrement, t.x
FROM (
SELECT x.value('(//RecordType)[1]', 'varchar(30)') AS type_enregistrement, x,
ROW_NUMBER() OVER (PARTITION BY x.value('(//RecordType)[1]', 'varchar(30)')
ORDER BY rb.[timestamp] DESC) AS rang
FROM sys.dm_os_ring_buffers AS rb
CROSS APPLY (SELECT CAST(rb.record AS xml)) AS r(x)
WHERE rb.ring_buffer_type = N'RING_BUFFER_CONNECTIVITY'
) AS t
WHERE t.rang = 1;
Les codes Winsock qu’on y rencontre sont documentés dans la liste des codes d’erreur Windows Sockets :
| Code | Nom | Sens |
|---|---|---|
| 10053 | WSAECONNABORTED | connexion interrompue par la pile locale |
| 10054 | WSAECONNRESET | connexion fermée de force par l’hôte distant |
| 10060 | WSAETIMEDOUT | pas de réponse dans le délai |
| 10061 | WSAECONNREFUSED | connexion refusée : rien n’écoute sur ce port |
Le même 10054 apparaît dans le journal d’erreurs sous sa forme hexadécimale, 0x2746.
La connexion fermée avant le login
Un cas se repère tout de suite : un enregistrement LoginTimers avec Spid à 0, ClientConnectionId composé de zéros et InputBufBytes à 0. Le serveur a accepté une socket TCP, et aucune session n’a été créée. Le login n’a jamais commencé : sonde de supervision, scanner de ports, sonde de répartiteur de charge, ou coupure en amont.
Ces champs ne prouvent pas, à eux seuls, qu’aucun octet n’a circulé. Mesuré sur l’instance de test : un paquet de pré-login reçu, et auquel le serveur a répondu, laisse aussi InputBufBytes à 0 ; sans identifiant client dans ce paquet, ClientConnectionId reste à zéro. Un Ssl/TotalTime non nul signale une négociation TLS. Ces champs ensemble désignent une connexion fermée avant le login, sans identifiant ni TLS : ils ne séparent pas une socket refermée sans rien envoyer d’un pré-login sans identifiant. Dans les deux cas, aucun login n’a été tenté.
Mesuré sur l’instance de test, une socket ouverte puis fermée sans rien envoyer laisse cette signature :
| Champ | Valeur |
|---|---|
RecordType | LoginTimers |
Spid | 0, aucune session créée |
ClientConnectionId | que des zéros |
SniConsumerError | 17830 |
State | 11 |
InputBufError | 10054 |
InputBufBytes | 0 |
TotalTime | la durée pendant laquelle la socket est restée ouverte, presque entièrement en NetReadsTime ; 0 si elle est refermée aussitôt |
C’est la signature relevée en production. La valeur de State n’est pas documentée ; elle sert à regrouper des enregistrements semblables, pas à leur donner un sens. Le conteneur de test passe par le relais de ports de podman, qui ne transmet pas toujours à l’instance la manière dont le client a fermé : une socket coupée aussitôt ouverte y laissait selon les essais cette signature, un ConnectionClose d’erreur 10057, ou rien.
Le message 17830 existe, et sys.messages le déclare journalisé. Sur l’instance de test pourtant, aucune de ces sockets n’a produit de ligne dans le journal d’erreurs : seuls le ring buffer et l’événement error_reported des Extended Events, de gravité 20, les montraient. system_health garde ces deux traces, puisqu’elle capture les erreurs de gravité 20 et plus.
La profondeur du ring buffer
Un ring buffer vide sur la fenêtre de l’incident ne prouve rien tant qu’on ne sait pas s’il couvrait cette fenêtre. Le tampon a une capacité fixe, et les enregistrements les plus anciens sortent quand d’autres arrivent. La page Microsoft sur les problèmes réseau intermittents le dit : sur un serveur chargé, il peut se remplir vite ; sur un serveur peu sollicité, il garde parfois plusieurs jours.
L’identifiant d’enregistrement le dit. Les identifiants se suivent depuis le démarrage de l’instance, en partant de 1 : si le plus petit identifiant présent vaut 1, rien n’est encore sorti. Ce comportement est mesuré sur l’instance de test, après un redémarrage, et n’est pas documenté. L’heure du plus ancien enregistrement, comparée à sqlserver_start_time de sys.dm_os_sys_info, ne prouve rien à elle seule : une instance calme peut n’avoir rien enregistré pendant des heures après son démarrage.
-- Profondeur du ring buffer de connectivité : a-t-il couvert toute la période ?
SELECT si.sqlserver_start_time,
DATEADD(second, (MIN(rb.[timestamp]) - si.ms_ticks) / 1000, SYSDATETIME()) AS plus_ancien,
COUNT(*) AS nb_enregistrements,
MIN(CAST(rb.record AS xml).value('(/Record/@id)[1]', 'bigint')) AS id_min,
MAX(CAST(rb.record AS xml).value('(/Record/@id)[1]', 'bigint')) AS id_max
FROM sys.dm_os_ring_buffers AS rb
CROSS JOIN sys.dm_os_sys_info AS si
WHERE rb.ring_buffer_type = N'RING_BUFFER_CONNECTIVITY'
GROUP BY si.sqlserver_start_time, si.ms_ticks;
La colonne timestamp du ring buffer compte les millisecondes sur la même base que ms_ticks, et l’écart se convertit en secondes à dessein. Jusqu’à SQL Server 2022, DATEADD n’accepte qu’un int, et un écart en millisecondes dépasse sa capacité au bout de 24,8 jours : la requête échouerait justement sur l’instance calme, démarrée depuis des semaines, qu’on veut examiner. SQL Server 2025 ajoute à la vue une colonne create_time, qui donne l’heure de l’enregistrement ; sur l’instance de test, elle retardait de 0,3 seconde sur les autres sources.
Dans le cas réel, l’instance avait démarré à 03:44:27, le plus ancien enregistrement, celui des premières reconnexions, datait de 03:44:34, sept secondes après. Rien n’avait été évincé : le ring buffer couvrait toute la période depuis le démarrage, l’heure de l’alerte comprise. L’absence d’échec à cette heure devenait une mesure, et non une lacune.
Sur l’instance de test, SQL Server 2025 sous Linux, le tampon de connectivité plafonnait à 1 024 enregistrements : après 1 200 échecs de login volontaires, il en restait 1 024, numérotés de 1 401 à 2 424. Ce chiffre vaut pour ce build ; la requête mesure l’éviction au lieu de la supposer.
La sonde de supervision
Dans le cas réel, sur les 128 enregistrements du ring buffer, 102 venaient d’une seule adresse, qui n’apparaissait dans aucune session établie. Tous étaient des LoginTimers d’erreur 17830, état 11, avec Spid à 0 ; l’enregistrement relevé en détail portait zéro octet lu et l’erreur 10054, la signature de la connexion fermée avant le login. 96 affichaient un temps total nul, les six autres entre 1 et 5 ms.
L’espacement a tranché. Du premier enregistrement, à 03:47:35, au dernier, à 12:10:35, les intervalles valaient 300 secondes à une seconde près, sauf un, de 180 secondes, juste après le redémarrage. Toujours à la seconde 35. Un client applicatif ne bat pas à la seconde près pendant plus de huit heures : c’est un ordonnanceur. Et l’enregistrement pris à l’heure de l’alerte de supervision, 12:00:35, était identique à ceux qui le précédaient.
Une sonde qui teste le port en ouvrant puis en refermant une socket sans rien envoyer fabrique le genre d’enregistrements qu’un outil de supervision peut ensuite signaler comme anormaux. La cadence désigne une tâche planifiée ; il reste à faire confirmer par le responsable de la supervision à qui appartient l’adresse et si c’est elle qui alimente la règle d’alerte. La requête C de l’annexe A isole ces connexions fermées avant le login et mesure leur cadence, hôte par hôte. Elle retient l’écart le plus fréquent et sa part plutôt qu’un minimum et un maximum, pour qu’un seul écart aberrant, comme les 180 secondes du redémarrage, ne masque pas la régularité.
Sur un réseau réel, l’hôte distant sépare la sonde des applications tant que le chemin conserve l’adresse source ; un proxy ou une traduction d’adresses peut en regrouper plusieurs sous une seule.
L’historique de system_health
Le ring buffer est en mémoire et se vide au redémarrage. Au-delà, l’historique est dans la session system_health, qui capture les mêmes enregistrements par l’événement connectivity_ring_buffer_recorded et les écrit dans ses fichiers .xel. Ces fichiers survivent au redémarrage : sur l’instance de test, ils remontaient trois jours avant le dernier démarrage. Leur profondeur dépend du volume d’événements ; la page Microsoft donne une rétention de 4 fichiers de 5 Mo, et de 10 fichiers de 100 Mo en éditions Standard et Enterprise.
C’est la seule source qui réponde à la question qui oriente tout : le phénomène est-il nouveau ou chronique. La requête D de l’annexe A compte les événements par heure et par hôte distant. Sur l’instance de test, SQL Server 2025, le champ remote_host de ces événements masque le dernier octet des adresses IPv4 (192.0.2.xx), alors que le ring buffer donne l’adresse entière : la requête regroupe alors par sous-réseau, pas par machine. Un volume stable sur plusieurs jours décrit un phénomène récurrent, antérieur à l’alerte ; une rupture nette date le début observable d’un changement, et cette heure devient le point d’entrée du reste, journaux Windows compris.
Une règle vaut pour toutes les sources : vérifier qu’elle couvre la fenêtre de l’incident avant d’écrire « aucun événement ». La trace par défaut, par exemple, tourne sur cinq fichiers de 20 Mo et ne couvre que quelques heures sur une instance bavarde : zéro ligne n’y signifie pas zéro événement.
Le journal d’erreurs
Chercher les motifs d’échec un par un, et noter ceux qui ne renvoient rien. Une absence consignée motif par motif est un résultat présentable à un tiers. sp_readerrorlog lit le journal courant (0) du moteur (1) et filtre sur une chaîne :
-- Motifs d'échec de connexion dans le journal d'erreurs courant
EXEC sp_readerrorlog 0, 1, N'Login failed';
EXEC sp_readerrorlog 0, 1, N'SSPI handshake';
EXEC sp_readerrorlog 0, 1, N'Network error code';
EXEC sp_readerrorlog 0, 1, N'17187';
EXEC sp_readerrorlog 0, 1, N'17189';
EXEC sp_readerrorlog 0, 1, N'not been picked up by a worker thread';
EXEC sp_readerrorlog 0, 1, N'longer than 15 seconds';
EXEC sp_readerrorlog 0, 1, N'4014';
EXEC sp_readerrorlog 0, 1, N'Severity: 20';
EXEC sp_readerrorlog 0, 1, N'Server is listening on';
Le dernier motif sert de témoin : il doit répondre, puisque l’instance l’écrit à chaque démarrage. Un message de gravité 10, comme 833 (entrées-sorties de plus de 15 secondes) ou 17884, s’écrit sans la ligne Error: ..., Severity: ... : il se cherche par son texte, pas par son numéro. Les textes que voit le client, comme « forcibly closed by the remote host » ou « error during connection initialization », ne se trouvent pas dans le journal du serveur ; côté serveur, la même coupure s’écrit « Network error code 0x2746 ».
Le journal courant commence au dernier démarrage, ou au dernier sp_cycle_errorlog ; pour une fenêtre plus ancienne, le premier paramètre désigne l’archive (1 pour la précédente, et ainsi de suite).
Dans le cas réel, onze motifs ont été interrogés, dont deux textes du client qui ne pouvaient rien rendre, et seul « Server is listening on » répondait. C’est le résultat qui a pesé le plus lourd dans le dossier. L’erreur 4014 (rupture pendant la lecture d’un paquet, gravité 20) ne faisait pourtant pas partie des motifs, alors que le ring buffer en portait une.
Quand des Login failed apparaissent, l’état de l’erreur 18456 donne la cause. La page Microsoft de l’erreur 18456 donne la liste ; les états les plus fréquents :
| État | Cause |
|---|---|
| 2, 5 | identifiant de connexion non valide, login inconnu |
| 6 | login Windows utilisé en authentification SQL |
| 7 | login désactivé, et mot de passe incorrect |
| 8, 9 | mot de passe incorrect ou non valide |
| 11, 12 | login valide, mais l’accès au serveur a échoué |
| 18 | le mot de passe doit être changé |
| 38, 46 | base demandée introuvable |
| 58 | authentification SQL sur une instance en mode Windows seul |
| 102 à 111 | échec de l’authentification Entra ID |
Un nom d’utilisateur vide dans le message (Login failed for user '') signifie, d’après la même page, que SQL Server a voulu transmettre les informations d’identification à LSASS et n’a pas pu : LSASS n’était pas disponible, ou le contrôleur de domaine n’a pas pu être contacté.
Les entrées-sorties
Un disque lent est souvent le premier suspect quand un hébergeur ou un éditeur cherche une cause. Il faut le mesurer, puis vérifier qu’il agit sur les connexions.
Dans le cas réel, le volume de données était réellement lent : sur une fenêtre de 3 h 17, 75 ms de latence d’écriture sur la base principale et 43 ms sur tempdb, contre 1 ms sur le volume du journal de la même machine ; 117 ms sur tempdb en cumul depuis le démarrage. Les deux relevés viennent de sys.dm_io_virtual_file_stats. Mais EnqueueTime valait 0 dans les enregistrements de login, sauf sept fois, avec un maximum de 5 ms, et le journal ne contenait aucune erreur 833 (entrée-sortie de plus de 15 secondes). Le disque ne retardait pas les logins. Le sujet restait entier pour les performances, il sortait du dossier de connectivité.
C’est la règle de la section Les conditions d’une conclusion : une mesure, une comparaison, et un mécanisme qui relie la mesure au symptôme. Ici le mécanisme manquait.
Le chiffrement
Un correctif Windows ou une politique de sécurité qui durcit TLS peut faire échouer des connexions du jour au lendemain. Une requête sur sys.dm_exec_connections dit si les connexions sont chiffrées :
-- Chiffrement, schéma d'authentification et transport des connexions
SELECT encrypt_option, auth_scheme, net_transport, COUNT(*) AS nb
FROM sys.dm_exec_connections
GROUP BY encrypt_option, auth_scheme, net_transport;
Dans le cas réel, au lendemain de deux redémarrages nocturnes attribués à un correctif Windows, encrypt_option valait FALSE sur toutes les connexions applicatives : les sessions n’étaient pas chiffrées. Cela n’écarte pas TLS. SQL Server chiffre toujours le paquet de login, d’après la page Microsoft sur le chiffrement et la validation des certificats, et un durcissement de TLS peut encore faire échouer cette négociation. C’est l’absence d’échec de login dans le ring buffer et dans le journal d’erreurs qui écartait la piste. La même requête montrait auth_scheme = SQL et net_transport = TCP sur toutes les sessions applicatives, ce qui a servi plus loin.
Le pool de connexions
Mesurer le pool avec is_cached
Une application .NET ouvre et ferme des connexions logiques en permanence. Le pool de SqlClient les sert depuis des connexions physiques déjà ouvertes ; seules les connexions physiques neuves passent par le réseau et par l’authentification. Un pool qui se vide ou se purge multiplie donc les occasions d’échec.
Quand un client SqlClient reprend une connexion du pool, il ne refait ni la connexion TCP ni l’authentification : il demande au serveur de réinitialiser la session, et cette demande part avec la première requête. SQL Server déclenche alors l’événement Extended Events sqlserver.login, dont le champ is_cached vaut vrai pour une connexion reprise du pool et faux pour une connexion physique. Sur l’instance de test, 1 000 ouvertures successives par un client .NET avec pool, chacune suivie d’un SELECT 1, ont produit un seul événement login à is_cached = false et 999 à true. Une connexion reprise puis refermée sans aucune requête ne produit rien : sur 500 ouvertures de ce genre, seule la première, physique, a été comptée, et les 499 reprises n’ont rien ajouté. is_cached = true compte donc les reprises suivies d’au moins une requête, et les échecs d’ouverture n’y figurent pas.
Une cible histogram regroupe les événements sur ce champ et ne stocke que deux compteurs ; le script est dans l’annexe B. Trois grandeurs en sortent :
| Grandeur | Calcul | Ce qu’elle dit |
|---|---|---|
| Reprises du pool par seconde | is_cached vrai / durée | à quel point l’application est bavarde, au minimum |
| Connexions physiques par seconde | is_cached faux / durée | ce qui atteint vraiment le réseau et l’authentification |
| Part des reprises | is_cached vrai / total | la santé du pool |
Dans le cas réel, 67 secondes de trace sur le login applicatif ont donné 41 410 reprises et 121 connexions physiques, soit environ 618 et 1,81 par seconde : 99,71 % des ouvertures vues par le serveur sortaient du pool, 997 sur 1 000. Le pool existait et fonctionnait.
| Part des reprises | Connexions physiques | Lecture |
|---|---|---|
| proche de 0 % | presque toutes | pool absent ou désactivé (Pooling=false dans la chaîne de connexion) |
| élevée, avec des connexions physiques non négligeables | quelques-unes par seconde | pool qui grandit pour les pointes, vidé entre deux rafales, ou purgé par une erreur |
| proche de 100 % | presque aucune | pool en régime stable |
Le cas réel tombait dans la deuxième catégorie : 1,81 connexion physique par seconde, environ 6 500 par heure, là où un pool en régime stable n’en crée presque aucune. La page Microsoft sur le pool de connexions de SqlClient donne les mécanismes qui font sortir des connexions du pool :
- le pool grandit avec la demande jusqu’à
Max Pool Size, 100 par défaut, et une pointe crée des connexions neuves ; - le nettoyage périodique retire les connexions inutilisées au-delà de
Min Pool Size, 0 par défaut, après environ quatre à huit minutes d’inactivité : une charge en rafales défait donc un pool qui fonctionne ; Load Balance Timeoutretire une connexion trop âgée quand elle revient au pool ;- une erreur fatale vide tout le pool concerné, et la rafale de reconnexions qui suit sollicite à nouveau le chemin d’authentification.
La file d’attente du pool plein
Quand toutes les connexions d’un pool sont prises, une nouvelle ouverture attend qu’une connexion revienne, au plus Connect Timeout, 15 secondes par défaut. Ensuite elle échoue, avec une InvalidOperationException dont le message est celui-ci :
System.InvalidOperationException: Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool.
This may have occurred because all pooled connections were in use and max pool size was reached.
Cette attente a lieu entièrement chez le client. Sur l’instance de test, dix tâches ont ouvert chacune une connexion avec Max Pool Size=3 et Connect Timeout=2, et l’ont gardée 5 secondes : trois ont obtenu leur connexion, sept ont échoué au bout de 2,2 secondes avec ce message, et le ring buffer de connectivité n’a reçu aucun enregistrement. Aucun outil côté serveur ne voit un pool plein. Depuis le serveur, on peut seulement :
- mesurer la part des reprises, parce qu’un pool qui approche son plafond crée beaucoup de connexions physiques avant de commencer à refuser ;
- compter les connexions vivantes par couple machine, programme et login, et les comparer au plafond (requête F de l’annexe A). Un pool existe par processus, par chaîne de connexion exacte et, en authentification Windows, par identité, donc par service et par machine, pas un par instance ; deux chaînes qui ne diffèrent que par l’ordre des mots-clés font deux pools ;
- donner à l’équipe applicative le fragment à chercher dans ses journaux,
prior to obtaining a connection from the pool, dans la langue de l’application : sur un poste en français, le message devient « Le délai d’attente s’est écoulé avant obtention d’une connexion du pool ». Le type de l’exception, lui, ne change pas.
Dans le cas réel, le service le plus chargé comptait 45 connexions au relevé, loin du plafond ; un instantané ne dit rien des pointes.
La période de blocage
Quand l’ouverture d’une connexion physique échoue, SqlClient met le pool en période de blocage. Pendant cette période, toute ouverture sur le même pool qui doit créer une connexion physique reçoit aussitôt l’exception d’origine, sans nouvelle tentative et sans rien envoyer sur le réseau ; une ouverture qui trouve une connexion libre dans le pool la reçoit normalement. La première période dure 5 secondes, et chaque nouvel échec double la suivante, jusqu’à une minute. La documentation cite les erreurs d’authentification et les délais de login ; le code du pilote réagit à toute erreur d’ouverture, et sur l’instance de test, un essai vers un port fermé a produit le même rejeu.
Mesuré sur l’instance de test avec Microsoft.Data.SqlClient 7.1.1 : un login au mot de passe faux, une tentative d’ouverture toutes les 500 ms pendant 40 secondes.
Pool Blocking Period | Exceptions côté client | Login failed dans le journal d’erreurs | ClientConnectionId distincts |
|---|---|---|---|
Auto (défaut) | 78 | 4 | 4 |
AlwaysBlock | 78 | 4 | 4 |
NeverBlock | 61 | 61 | 61 |
Les vraies tentatives ont eu lieu à 0, 5,6, 15,8 et 35,9 secondes ; les rejeux entre elles duraient de 0 à 3 ms. Chaque exception rejouée porte le ClientConnectionId de la tentative d’origine.
Deux conséquences pour l’enquête :
- le nombre d’erreurs dans un journal applicatif n’est pas le nombre d’échecs réels : une défaillance unique se multiplie par le nombre d’ouvertures qui, pendant la période, ne trouvent pas de connexion libre, et après une purge du pool, ce sont presque toutes ;
- ces rejeux sont invisibles côté serveur par construction, ce qui donne une seconde raison, indépendante, au silence du serveur.
Regrouper les exceptions par ClientConnectionId révèle les rejeux ; ce n’est pas un total fiable des échecs, car un échec survenu avant l’envoi du paquet de pré-login n’a pas d’identifiant (tous ces échecs portent alors le même identifiant composé de zéros), d’après la page Microsoft sur le traçage des données de SqlClient.
Dans le cas réel, l’éditeur signalait des journaux qui grossissaient au point de créer un problème secondaire. La période de blocage l’explique sans qu’il faille supposer des milliers d’échecs. Le mot-clé Pool Blocking Period de la chaîne de connexion la règle. Sa valeur par défaut, Auto, l’active pour une instance SQL Server et la désactive pour les noms de serveur Azure SQL reconnus : une application connectée à Azure SQL Database n’a donc pas de période de blocage par défaut. Microsoft déconseille de passer à NeverBlock sans une logique de nouvelle tentative conçue pour : un problème de mot de passe, de pare-feu ou une panne se transforme alors en rafale d’authentifications.
L’instantané des connexions
Dans le cas réel, la colonne connect_time de sys.dm_exec_connections montrait des connexions jeunes, aucune connexion applicative de plus de 30 minutes, et dix connexions ouvertes en 27 secondes sur une même machine. La première lecture fut que l’application n’utilisait pas de pool ; la mesure par is_cached l’a démentie.
Un instantané de sys.dm_exec_connections est biaisé par la durée : une connexion de 3 secondes n’a presque aucune chance d’y figurer, une connexion de 20 minutes y figure presque sûrement. Les connexions visibles sont la population persistante, pas un échantillon du flux. Par la loi de Little, 220 sessions applicatives pour 1,81 création par seconde donneraient une durée de vie moyenne d’environ deux minutes (121 secondes). Ces deux valeurs viennent de relevés pris à trois heures d’écart, et la loi de Little suppose un régime stable sur une même fenêtre : le calcul ne chiffre pas la durée de vie, il montre seulement qu’un renouvellement régulier des connexions physiques suffit à rendre connect_time jeune, avec un pool qui fonctionne.
CHECK_POLICY sur les logins SQL
D’après CREATE LOGIN, CHECK_POLICY vaut ON par défaut : un login SQL créé sans précaution est soumis à la politique de mots de passe de Windows. SQL Server délègue ces contrôles à l’API Windows NetValidatePasswordPolicy, et l’attente PREEMPTIVE_OS_NETVALIDATEPASSWORDPOLICY de sys.dm_os_wait_stats porte le nom de cet appel.
Mesuré sur l’instance de test, sous Linux et sans domaine : l’attente compte une occurrence par authentification tentée sous un login soumis à la politique, réussie ou non, et aucune pour une connexion reprise du pool ni pour un login sans politique. 50 connexions physiques sous un login CHECK_POLICY = ON l’ont fait avancer de 50, les mêmes sous un login CHECK_POLICY = OFF de 0, 499 reprises du pool de 0. Le rapport n’a pas été mesuré sur un serveur Windows membre d’un domaine.
Dans le cas réel, le login applicatif portait CHECK_POLICY. L’attente cumulait 149 128 ms pour 127 331 occurrences en 8 h 20, soit 1,17 ms en moyenne. Ce temps n’est pas celui de l’authentification entière, et une moyenne basse n’exclut pas des connexions lentes : un max_wait_time_ms qui grimpe appelle une mesure des durées de login elles-mêmes, dans le ring buffer de connectivité.
-- Logins SQL soumis à la politique de mots de passe
SELECT name, is_disabled, is_policy_checked, is_expiration_checked
FROM sys.sql_logins
WHERE is_policy_checked = 1
AND name NOT LIKE N'##%' -- logins internes
ORDER BY name;
-- Validations de politique depuis le démarrage de l'instance
SELECT waiting_tasks_count, wait_time_ms, max_wait_time_ms,
CAST(1.0 * wait_time_ms / NULLIF(waiting_tasks_count, 0) AS decimal(10, 3)) AS ms_par_occurrence
FROM sys.dm_os_wait_stats
WHERE wait_type = N'PREEMPTIVE_OS_NETVALIDATEPASSWORDPOLICY';
L’hypothèse de départ du cas réel était que chaque connexion physique interrogeait le contrôleur de domaine par cet appel, et qu’un domaine lent ralentissait donc les logins SQL. Elle n’est pas établie. L’API s’exécute sur la machine locale quand l’appelant ne lui désigne pas de serveur distant, ce que SQL Server lui passe n’est pas documenté, et sa documentation précise qu’elle ne valide pas les mots de passe des comptes Active Directory : elle travaille sur l’état de compte que lui fournit l’application. La page Microsoft sur les problèmes d’authentification intermittents parle bien du contrôleur de domaine, dans un article centré sur les erreurs SSPI et NTLM de l’authentification Windows, et, à propos d’une application sans pool, d’une surcharge qui peut entraîner « l’inondation d’un contrôleur de domaine mal performant ». Pour un login SQL, la mesure du pool reste la première piste, que CHECK_POLICY soit actif ou non. Mesurer la dépendance demande une trace réseau vers le contrôleur de domaine, sur un serveur membre du domaine, pendant une rafale de connexions physiques.
Les limites de l’attente
Ce compteur dit qu’un login sollicite la politique et le temps qu’il y passe. Il ne mesure pas le nombre de connexions physiques. Dans le cas réel, il donnait 4,24 occurrences par seconde en moyenne depuis le démarrage, et les identifiants du ring buffer des erreurs de sécurité, qui semblaient l’accompagner, avançaient de 3,99 par seconde ; les deux ont été lus comme le nombre de connexions physiques par seconde, avant que is_cached donne 1,81. Les fenêtres ne coïncidaient pas, et la trace is_cached a été prise après la désactivation de CHECK_POLICY, donc l’écart n’a pas été tranché. Les échecs de login comptent aussi dans l’attente, ce qui peut l’écarter du nombre de connexions physiques.
Avant de désactiver CHECK_POLICY
ALTER LOGIN ... WITH CHECK_POLICY = OFF fait disparaître l’attente pour ce login. ALTER LOGIN et la page sur la politique de mots de passe documentent les effets de bord : CHECK_EXPIRATION passe aussi à OFF et ne peut pas revenir à ON tant que CHECK_POLICY est à OFF (erreur 15122), l’historique des mots de passe est effacé et l’heure de verrouillage remise à zéro. Le verrouillage et les contrôles de complexité disparaissent avec la politique, puisque c’est elle qui les porte.
Le retour en arrière n’est pas symétrique. Sur l’instance de test, SQL Server 2025 CU7, ALTER LOGIN ... WITH CHECK_POLICY = ON seul a laissé is_expiration_checked à 0, alors que la page sur la politique de mots de passe annonce que l’expiration revient avec la politique. Qui veut rétablir un login dont l’expiration était active doit l’écrire, en remplaçant login_applicatif par le nom du login concerné :
-- Rétablir la politique et l'expiration sur un login SQL
ALTER LOGIN [login_applicatif] WITH CHECK_POLICY = ON, CHECK_EXPIRATION = ON;
La désactivation se décide au titre de la sécurité, par celui qui porte la politique de sécurité. Un login applicatif dont le mot de passe est long, aléatoire et stocké dans une configuration ne gagne pas grand-chose aux règles de complexité, et son verrouillage, si la politique en fixe un, ressemble davantage à un risque d’interruption de service qu’à une protection. Dans le cas réel, l’hébergeur a désactivé CHECK_POLICY sur le login applicatif. La décision s’écrit, avec le login concerné et la date, pour que le prochain audit sache pourquoi l’option est désactivée.
L’authentification Windows et le contrôleur de domaine
Quand les applications se connectent en authentification Windows, le contrôleur de domaine est sur le chemin de l’authentification, de façon documentée. La page Microsoft sur les problèmes d’authentification intermittents range parmi les causes les plus courantes la réponse lente du contrôleur de domaine, et précise qu’avec NTLM, LSASS limite le nombre de nouvelles connexions traitées à la fois : les autres demandes attendent et peuvent expirer. Les signaux à lire :
Sspi/TotalTimeélevé dans les enregistrementsLoginTimersdu ring buffer de connectivité ;- dans le journal d’erreurs,
SSPI handshake failedavec le code0x80090311(aucune autorité n’a pu être contactée pour l’authentification) ou0x80090304(l’autorité de sécurité locale ne peut pas être contactée), qui désignent le domaine ou LSASS, pas SQL Server ; - le code
0x80090322(SEC_E_WRONG_PRINCIPAL, « The target principal name is incorrect »), qui désigne un SPN mal attribué quand il vient de la négociation SSPI, et un nom de serveur absent du certificat quand il vient de la validation du certificat TLS ; - dans le journal système, sur le serveur comme sur le client, l’événement Netlogon 5719 (impossible d’établir une session sécurisée avec un contrôleur de domaine), et dans le journal de sécurité l’événement 4625, dont le sous-statut détaille l’échec ;
0xC0000133(STATUS_TIME_DIFFERENCE_AT_DCdans la liste des codes NTSTATUS) y signale un décalage d’horloge entre la machine et le contrôleur de domaine.
Le repli sur les canaux nommés
Le message d’erreur que rapporte l’application est souvent celui-ci, cité par Microsoft dans sa page sur l’erreur liée au réseau ou spécifique à l’instance :
A network-related or instance-specific error occurred while establishing a connection to SQL Server. [...]
(provider: Named Pipes Provider, error: 40 - Could not open a connection to SQL Server)
L’équipe qui le lit cherche alors du côté des canaux nommés, que personne n’utilise. Un client qui dispose de plusieurs protocoles les essaie dans l’ordre et, quand le premier échoue, passe au suivant sans le signaler. La note de Microsoft sur les clients qui changent de protocole, écrite pour les anciennes bibliothèques clientes, le dit : le client ne reçoit pas d’erreur indiquant que le premier protocole a échoué.
Pour SqlClient sous Windows, l’ordre est fixe, TCP puis canaux nommés, et la page Ordre des protocoles clients précise qu’il ne se change pas. Les tests du pilote, dans le dépôt dotnet/SqlClient, attendent bien une erreur « Named Pipes Provider » pour un serveur injoignable désigné sans préfixe sous Windows, avec la couche réseau native, et pour un nom qui n’est pas celui d’un serveur Azure SQL ; pour un nom Azure SQL, le pilote n’essaie que TCP. Sous Linux et macOS, et partout où le pilote utilise sa couche réseau managée, il n’essaie que TCP, et l’erreur nomme TCP. Les autres pilotes Windows ont chacun leur ordre : celui du Gestionnaire de configuration pour SQL Server Native Client, une entrée de registre ProtocolOrder propre au pilote pour MSOLEDBSQL 19.1 et suivants. Pour eux, il faut d’abord savoir quelle bibliothèque l’application utilise et comment elle est configurée.
Une erreur qui nomme Named Pipes Provider, sur un client SqlClient sous Windows qui n’a imposé aucun protocole, dit donc que TCP a échoué avant, puis que le repli a échoué à son tour. C’est le cas ordinaire : une nouvelle installation de SQL Server désactive les canaux nommés pour les connexions réseau, et ils passent par le port 445, que le pare-feu Windows ferme par défaut d’après la page sur les alias. Trois conséquences pratiques :
- l’erreur réelle, celle de TCP, est le plus souvent absente du message ; réclamer plus de texte d’exception à l’éditeur ne la fera pas apparaître. Lire tout de même le message jusqu’au bout : la page Microsoft range à côté de l’erreur 40 des messages
TCP Providerqui portent, eux, la cause ; - la cause est à chercher sur le chemin TCP : résolution de nom, routage, pare-feu, port, ports éphémères du client ;
- imposer TCP dans la chaîne de connexion, par le préfixe
tcp:du nom de serveur (SqlConnectionStringBuilder.DataSource), supprime le repli et fait remonter l’erreur TCP elle-même. Avec la couche réseau native, sous Windows, le message porte le libellé système, etSqlException.Numberle code, que SSMS affiche entre parenthèses :10060, aucune réponse dans le délai ;10061, connexion refusée ;11001, nom inconnu. Avec la couche managée, le message ne porte queerror: 35, une exception interne, et la cause est à lire dansSqlException.Numberet dans l’exception interne (mesuré avec Microsoft.Data.SqlClient 7.1.1 sous Linux : 11001 pour un nom inconnu). C’est souvent le premier changement à demander : il ne corrige rien, mais il rend la prochaine erreur lisible.
Dans le cas réel, net_transport = TCP sur toutes les connexions vivantes corroborait la lecture : l’application se connectait en TCP quand elle y parvenait.
La résolution de nom
Un client qui résout le nom du serveur reçoit tous les enregistrements publiés, A et AAAA. Avec SqlClient, l’ordre des essais suit le mot-clé IPAddressPreference, IPv4First par défaut : les adresses IPv4 passent avant les IPv6. Une tentative vers une adresse injoignable n’atteint jamais le serveur, donc SQL Server n’en voit rien. Interprétation : avec SqlClient sous Windows, un échec TCP sur toutes les adresses essayées est aussi ce qui déclenche le repli de la section précédente.
Le cas se présente quand un nom porte une adresse IPv6 lien-local (fe80::), une adresse APIPA (169.254.x.x) d’un adaptateur virtuel, par exemple de cluster, ou une ancienne adresse jamais nettoyée d’une zone DNS sans vieillissement.
Le coût d’une mauvaise tentative dépend de ce qui arrive au paquet. Un réseau inaccessible répond en quelques millisecondes et se voit à peine. Un paquet SYN jeté en silence par un pare-feu laisse le client attendre, et là le client voit un délai dépassé. Dans le code de la couche managée de SqlClient (SniTcpHandle), les adresses sont essayées une à une, et chacune reçoit tout le délai restant : une adresse muette en tête de liste consomme tout le Connect Timeout. Sous .NET Framework, la résolution d’adresse transparente découpe le délai entre les adresses. Un port fermé, qui refuse pourtant aussitôt, peut aussi coûter tout le délai : la boucle d’ouverture de SqlClient (LoginNoFailover dans SqlConnectionInternal.cs), commune aux deux couches réseau, recommence après une erreur réseau à intervalles croissants, jusqu’à une seconde, tant que le délai n’est pas écoulé. Sur la machine de test, sous Linux, un port fermé a pris 14,8 secondes pour un Connect Timeout de 15, et le client voyait un délai dépassé plutôt qu’un refus. Seule la mesure dit ce qui se produit.
Les tests de résolution
Le premier test se lance sur le serveur SQL, les deux autres sur une machine applicative. Les scripts sont dans l’annexe C.
- Sur le serveur : ce que le nom porte (
Resolve-DnsName-Type A_AAAA), quelles interfaces portent quelles adresses, lesquelles s’enregistrent dans le DNS. - Sur le client : ouvrir une socket vers chaque adresse publiée et chronométrer chacune. Une adresse qui répond en quelques millisecondes est la bonne ; une qui échoue tout de suite gêne sans coûter ; une qui consomme son délai est la coupable.
- Sur le client : vingt ouvertures de connexion par nom, vingt par adresse IP, avec
Pooling=falsepour que chaque ouverture soit physique. Si l’adresse est rapide et le nom parfois lent, la résolution est en cause ; si les deux sont lents, c’est le chemin ou l’authentification.
Les pièges du test
Tester la résolution, puis la connexion TCP vers l’adresse attendue, contourne la résolution au lieu de l’éprouver : le test 2 doit viser chaque adresse rendue, pas seulement celle qu’on attend.
Lancé sur le serveur lui-même, le test ne prouve rien pour un client : l’adresse lien-local du serveur lui est joignable, à lui. Pour savoir si un enregistrement vient d’un serveur DNS, deux paramètres de Resolve-DnsName séparent les cas : -NoHostsFile ignore le fichier hosts, -DnsOnly n’interroge que le DNS, sans LLMNR ni NetBIOS. Interprétation, faute de source : une machine résout son propre nom à partir de ses propres interfaces, ce qui rend le test sur le serveur optimiste.
Dans le cas réel, le nom du serveur rendait une adresse IPv6 lien-local en plus de la bonne adresse IPv4. Depuis un client, la tentative vers l’adresse lien-local échouait en 50 ms, à la création de la socket, avant tout envoi. Un échec immédiat et toujours identique ne peut pas produire un défaut intermittent : il serait permanent ou absent. La piste a été écartée par la mesure, et le dossier l’a écrit pour qu’elle ne soit pas rouverte.
Les vérifications côté client
Quand le ring buffer de connectivité et le journal d’erreurs sont vides sur la fenêtre de l’incident, et que la profondeur de ces sources est prouvée, le serveur désigne l’amont. Les vérifications suivantes viennent des pages Microsoft sur les problèmes d’authentification et de réseau intermittents. Le script de collecte est dans l’annexe D.
Les mesures de ports et de journaux n’ont de valeur que pendant un épisode d’erreur, ou juste après. Une mesure à froid ne dit rien, et il faut l’annoncer à l’équipe qui va les lancer : sans quoi elle les lancera le lendemain et conclura que tout est normal.
L’antivirus et les pilotes de filtre réseau
La page sur l’authentification intermittente nomme l’antivirus parmi les causes, difficile à prouver mais courante, à examiner même sans preuve quand les autres pistes n’aboutissent pas. Un antivirus ou un agent de détection qui inspecte le trafic s’insère dans la pile réseau.
# Produit antivirus, protection en temps réel, minifiltres de fichiers et fournisseurs Winsock
Get-MpComputerStatus | Select-Object AntivirusEnabled, RealTimeProtectionEnabled, AMProductVersion
fltmc filters
netsh winsock show catalog | Select-String 'Description'
Get-MpComputerStatus ne répond que pour Microsoft Defender. Un autre produit s’identifie par ses minifiltres de système de fichiers dans fltmc filters, qui ne montre pas les filtres réseau, et parfois par ses fournisseurs dans le catalogue Winsock. Ce catalogue ne suffit pas : depuis Windows 8 et Windows Server 2012, Microsoft fait passer ces produits par la plateforme de filtrage Windows plutôt que par Winsock, et un catalogue sans fournisseur tiers n’écarte pas l’agent.
Le test que propose la page sur le réseau intermittent est une exclusion du processus applicatif, observée 48 heures, à décider avec l’équipe de sécurité. Désactiver l’antivirus ne prouve rien : son pilote de filtre reste chargé. Faute d’exclusion possible, Microsoft propose la désinstallation suivie d’un redémarrage.
Le journal système
Filtrer sur Erreur, Avertissement et Critique autour de l’heure des échecs, avec en tête l’événement Netlogon 5719. La même requête est à passer sur le serveur SQL.
# Événements système des trois derniers jours : authentification, TLS, TCP/IP
Get-WinEvent -FilterHashtable @{LogName='System'; StartTime=(Get-Date).AddDays(-3); Level=1,2,3} |
Where-Object { $_.ProviderName -match 'Netlogon|LsaSrv|Kerberos|Schannel|Tcpip' } |
Select-Object TimeCreated, ProviderName, Id, LevelDisplayName, Message | Format-List
La latence vers le contrôleur de domaine
nltest /dsgetdc:<domaine>
nltest /sc_verify:<domaine>
ping <serveur SQL>
La page Microsoft fait mesurer la latence par ping vers le serveur de base de données, en comparant les domaines ou les centres de données entre eux, et le temps de validation d’un compte par runas /user:<domaine>\<compte> cmd.exe, qui demande un mot de passe au clavier. nltest complète sans interaction : /dsgetdc dit quel contrôleur le localisateur trouve, /sc_verify vérifie le canal sécurisé avec le contrôleur auquel la machine est liée. /sc_verify reconstruit le canal s’il est rompu et demande des droits d’administrateur : relever sa première sortie avant de conclure, car un second passage trouvera un canal réparé. Si le problème persiste après ces tests, la page conclut qu’il ne vient pas de SQL Server, mais des performances de l’infrastructure réseau ou du contrôleur de domaine.
Les ports éphémères
La page sur l’authentification intermittente range l’épuisement des ports éphémères parmi les causes relativement courantes des délais de connexion intermittents, et note que l’absence de pool se signale par un grand nombre de connexions en TIME_WAIT comparé aux connexions établies. La page sur le réseau intermittent le chiffre : avec la plage dynamique par défaut et un délai TIME_WAIT de 240 secondes, un client Windows ne peut ouvrir qu’environ 68 connexions sortantes neuves par seconde.
netstat -ano | find /c "TIME_WAIT"
netstat -ano | find /c "ESTABLISHED"
netsh int ipv4 show dynamicport tcp
# États des connexions TCP vers le port de l'instance
Get-NetTCPConnection -RemotePort 1433 | Group-Object State | Select-Object Name, Count
Un nombre de TIME_WAIT qui approche la taille de la plage dynamique, 16 384 ports par défaut sur un Windows récent (de 49152 à 65535, d’après la page Microsoft sur la plage de ports dynamiques), signe l’épuisement.
La mémoire du serveur SQL
La page sur l’authentification intermittente cite un point côté serveur : une mémoire noyau trop basse sur la machine SQL Server produit des délais de connexion intermittents. Elle recommande de régler max server memory environ 4 à 8 Go sous la mémoire physique, moins encore si d’autres services tournent sur la machine. L’article La consommation de RAM de SQL Server propose une règle par paliers, plus fine pour les grosses machines.
-- max server memory et mémoire physique de la machine
SELECT name, value_in_use
FROM sys.configurations
WHERE name IN (N'max server memory (MB)', N'min server memory (MB)');
SELECT physical_memory_kb / 1024 / 1024 AS memoire_physique_go
FROM sys.dm_os_sys_info;
Les erreurs de raisonnement
Toutes celles-ci ont été commises ou frôlées sur le cas réel.
Prendre un compteur indirect pour une mesure
La première version de l’analyse concluait à un pool de connexions absent, à partir de deux compteurs indépendants, l’attente PREEMPTIVE_OS_NETVALIDATEPASSWORDPOLICY et le débit du ring buffer de sécurité, qui donnaient 4,24 et 3,99 par seconde et qu’on a lus comme le nombre de connexions physiques. La mesure directe par is_cached a donné 1,81 par seconde et 99,71 % de reprises.
Que deux compteurs coïncident suggérait qu’ils comptaient la même chose ; cela ne disait rien de ce que cette chose était. Quand une mesure directe existe, la chercher avant de bâtir sur un indicateur indirect.
Lire un volume d’erreurs client comme un volume d’échecs
La période de blocage de SqlClient multiplie les lignes d’erreur sans multiplier les échecs. Compter les épisodes, ou les ClientConnectionId distincts, pas les lignes ; les échecs survenus avant le pré-login partagent tous l’identifiant composé de zéros.
Conclure sur une source trop courte
Ring buffers vidés au redémarrage, trace par défaut qui tourne en quelques heures sur une instance bavarde. Avant d’écrire « aucun événement », vérifier la date du plus ancien enregistrement de la source.
Oublier le fuseau horaire
Un serveur en UTC, une application en heure locale, un hébergeur dans un troisième fuseau. Toute heure communiquée à un tiers porte son fuseau.
Suspecter la sonde de supervision en dernier
Une sonde qui ouvre et ferme une socket sans rien envoyer produit les enregistrements qu’elle signale ensuite. Identifier les adresses sources récurrentes avant tout.
Accuser un disque lent
Un disque lent est un vrai problème de performance. Règle de méthode, sans source Microsoft : ne le retenir comme cause d’un timeout de connexion que si EnqueueTime monte dans le ring buffer de connectivité, si des messages 833 apparaissent, ou si les délais tombent en phase post-login, qui comprend l’ouverture de la base.
Les conditions d’une conclusion
Un diagnostic tient quand trois éléments sont réunis : une mesure avec sa fenêtre, une comparaison dans le temps ou dans l’espace (75 ms d’écriture sur un volume contre 1 ms sur un autre de la même machine, sur la même fenêtre), et un mécanisme qui relie la mesure au symptôme. Sans le mécanisme, on tient une corrélation. Ce qui reste estimé s’annonce comme tel.
Préparer la prochaine occurrence
Si rien n’a été concluant, c’est souvent que la trace manque au moment de l’épisode. Une session Extended Events permanente et peu coûteuse change la situation pour la fois suivante ; elle est dans l’annexe B.
L’action client_connection_id y joue le rôle principal. Les pilotes Microsoft génèrent cet identifiant et l’envoient au serveur avec le paquet de pré-login ; SqlClient le rend dans ses exceptions, par la propriété SqlException.ClientConnectionId et dans ToString() de l’exception, mais pas dans Message. Dans la session, il est porté par l’action client_connection_id pour les événements login et error_reported, et par le champ connection_peer_id pour les enregistrements du ring buffer. Demander à l’équipe applicative de journaliser ce champ permet de rapprocher une erreur client d’une connexion précise côté serveur, ou de prouver qu’aucune connexion portant cet identifiant n’est arrivée. Un identifiant composé de zéros dans le ring buffer signifie que le serveur n’a reçu aucun identifiant : pas de pré-login, ou un client qui n’en joint pas.
Côté application, la demande à formuler à l’éditeur tient en quelques points :
- la configuration du pool par service :
Max Pool Size,Min Pool Size,Load Balance Timeout,Pooling, et la version du pilote ; - la recherche du libellé « Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool », ou de sa traduction, dans les journaux ;
- la journalisation du
ClientConnectionIddes exceptions, et le regroupement des erreurs par cet identifiant plutôt que leur nombre de lignes ; - le préfixe
tcp:dans la chaîne de connexion, pour supprimer le repli sur les canaux nommés ; - le journal système Windows des machines applicatives autour des heures d’échec ;
- l’antivirus ou l’agent de sécurité installé sur ces machines, et ses exclusions.
Le bilan du cas réel
Après une journée de mesures, sur une fenêtre de 8 h 20 couvrant l’alerte :
- le journal d’erreurs ne contenait aucun échec de connexion, sur onze motifs interrogés ;
- le ring buffer de connectivité, qui couvrait toute la période depuis le démarrage, ne contenait à l’heure de l’échec signalé par l’éditeur, comme entre 09:24 et l’alerte de supervision, que les battements d’une sonde toutes les 300 secondes ; cinq déconnexions réelles plus tôt dans la journée, dont une erreur 4014 de gravité 20 huit minutes après l’échec cité par l’éditeur, restaient à expliquer ;
- les workers, le chiffrement et les entrées-sorties étaient écartés, par une mesure chacun ;
- le pool de connexions fonctionnait, avec 99,71 % de reprises et un renouvellement de 1,81 connexion physique par seconde ;
- le login applicatif portait
CHECK_POLICY, que l’hébergeur a retiré pendant le diagnostic ; sa dépendance au contrôleur de domaine, soupçonnée, n’a pas été établie ; - l’erreur citée par l’éditeur nommait les canaux nommés, ce qui signait un échec TCP en amont, masqué par le repli ;
- la piste d’une adresse IPv6 lien-local dans la résolution du nom a été testée depuis un client et écartée : échec en 50 ms, toujours identique.
Les causes qui vivent sur le serveur SQL ont été écartées par la mesure. L’enquête s’est ensuite déplacée vers l’infrastructure, en particulier les deux redémarrages de la nuit précédant l’alerte, attribués à un correctif Windows et à faire confirmer par l’infogérant, et vers les vérifications côté client sur les machines applicatives.
C’est l’issue la plus fréquente. La méthode ne garantit pas de trouver la cause depuis le serveur ; elle permet d’écrire, mesures en main, ce que le serveur a vu et ce qu’il n’a pas vu, et de remettre aux bonnes équipes une liste de vérifications précise plutôt qu’un « chez nous tout va bien ».
Annexe A. Le triage côté serveur
Blocs autonomes, à dérouler dans l’ordre. Aucun n’écrit, à part deux tables temporaires dans le bloc G. VIEW SERVER STATE suffit pour tous, sauf le bloc H, qui lit sys.master_files et demande en plus VIEW ANY DEFINITION : sans elle, il rend zéro ligne sans erreur. À partir de SQL Server 2022, les permissions sont plus fines : VIEW SERVER PERFORMANCE STATE pour la plupart des vues, VIEW SERVER SECURITY STATE pour sys.dm_tcp_listener_states.
-- A. L'instance répond-elle, et depuis quand
SELECT SERVERPROPERTY('ServerName') AS instance,
SERVERPROPERTY('ProductVersion') AS build,
SYSDATETIME() AS heure_serveur,
SYSUTCDATETIME() AS heure_utc,
osi.sqlserver_start_time,
osi.max_workers_count,
osi.scheduler_count
FROM sys.dm_os_sys_info AS osi;
-- Une base est-elle indisponible
SELECT name, state_desc, user_access_desc, is_read_only
FROM sys.databases
WHERE state_desc <> N'ONLINE' OR user_access_desc <> N'MULTI_USER';
-- Les workers sont-ils épuisés
SELECT (SELECT max_workers_count FROM sys.dm_os_sys_info) AS workers_max,
SUM(active_workers_count) AS workers_actifs,
SUM(runnable_tasks_count) AS taches_pretes,
SUM(work_queue_count) AS taches_sans_worker,
SUM(pending_disk_io_count) AS io_en_attente
FROM sys.dm_os_schedulers
WHERE status = N'VISIBLE ONLINE';
SELECT wait_type, waiting_tasks_count, wait_time_ms, max_wait_time_ms
FROM sys.dm_os_wait_stats
WHERE wait_type IN (N'THREADPOOL', N'RESOURCE_SEMAPHORE',
N'PREEMPTIVE_OS_AUTHENTICATIONOPS', N'PREEMPTIVE_OS_LOOKUPACCOUNTSID',
N'PREEMPTIVE_OS_AUTHORIZATIONOPS', N'PREEMPTIVE_OS_NETVALIDATEPASSWORDPOLICY',
N'ASYNC_NETWORK_IO')
ORDER BY wait_time_ms DESC;
-- Sur quoi l'instance écoute
SELECT ip_address, port, type_desc, state_desc, start_time
FROM sys.dm_tcp_listener_states;
-- B. Ring buffer de connectivité, détail. Les deux formes de noms des tampons TDS
-- sont lues : TdsBufInfo (LoginTimers), TdsBuffersInformation (Error, ConnectionClose).
SELECT x.value('(/Record/@id)[1]', 'bigint') AS id,
DATEADD(millisecond, (rb.[timestamp] - si.ms_ticks) % 1000,
DATEADD(second, (rb.[timestamp] - si.ms_ticks) / 1000, SYSDATETIME())) AS heure_serveur,
x.value('(//RecordType)[1]', 'varchar(30)') AS type_enregistrement,
x.value('(//Spid)[1]', 'int') AS spid,
x.value('(//SniConsumerError)[1]', 'int') AS erreur_sni,
x.value('(//State)[1]', 'int') AS etat,
x.value('(//RemoteHost)[1]', 'varchar(48)') AS hote_distant,
x.value('(//ClientConnectionId)[1]', 'varchar(40)') AS client_connection_id,
COALESCE(x.value('(//TdsBufInfo/InputBufError)[1]', 'int'),
x.value('(//TdsBuffersInformation/TdsInputBufferError)[1]', 'int')) AS erreur_entree,
COALESCE(x.value('(//TdsBufInfo/InputBufBytes)[1]', 'int'),
x.value('(//TdsBuffersInformation/TdsInputBufferBytes)[1]', 'int')) AS octets_lus,
x.value('(//LoginTimersInMilliseconds/TotalTime)[1]', 'int') AS total_ms,
x.value('(//LoginTimersInMilliseconds/EnqueueTime)[1]', 'int') AS en_file_ms,
x.value('(//LoginTimersInMilliseconds/NetReadsTime)[1]', 'int') AS lecture_reseau_ms,
x.value('(//LoginTimersInMilliseconds/NetWritesTime)[1]', 'int') AS ecriture_reseau_ms,
x.value('(//LoginTimersInMilliseconds/Ssl/TotalTime)[1]', 'int') AS ssl_ms,
x.value('(//LoginTimersInMilliseconds/Sspi/TotalTime)[1]', 'int') AS sspi_ms,
x.value('(//LoginTimersInMilliseconds/TriggerAndResGovTime/TotalTime)[1]', 'int') AS declencheurs_rg_ms
FROM sys.dm_os_ring_buffers AS rb
CROSS JOIN sys.dm_os_sys_info AS si
CROSS APPLY (SELECT CAST(rb.record AS xml)) AS t(x)
WHERE rb.ring_buffer_type = N'RING_BUFFER_CONNECTIVITY'
ORDER BY rb.[timestamp] DESC;
L’heure se calcule depuis la colonne timestamp, en secondes puis en millisecondes, pour ne pas dépasser la capacité de DATEADD jusqu’à SQL Server 2022. L’élément RecordTime du XML ne convient pas : il est au format américain, et ni ses secondes ni ses millisecondes ne sont complétées par des zéros (16:57:0.346, 26.62 pour 26,062 secondes), ce que TRY_PARSE lit de travers. Sur SQL Server 2025, la colonne create_time donne aussi l’heure, avec un léger retard (0,3 seconde sur l’instance de test).
-- C. Connexions fermées avant le login, sans pré-login identifié ni TLS : volume et cadence, par hôte
WITH r AS (
SELECT rb.[timestamp] AS ts, CAST(rb.record AS xml) AS x
FROM sys.dm_os_ring_buffers AS rb
WHERE rb.ring_buffer_type = N'RING_BUFFER_CONNECTIVITY'
), vides AS (
SELECT ts,
x.value('(//RemoteHost)[1]', 'varchar(48)') AS hote_distant,
x.value('(//LoginTimersInMilliseconds/TotalTime)[1]', 'int') AS total_ms
FROM r
WHERE x.value('(//RecordType)[1]', 'varchar(30)') = 'LoginTimers'
AND x.value('(//Spid)[1]', 'int') = 0
AND x.value('(//TdsBufInfo/InputBufBytes)[1]', 'int') = 0
AND x.value('(//ClientConnectionId)[1]', 'varchar(40)') = '00000000-0000-0000-0000-000000000000'
AND x.value('(//LoginTimersInMilliseconds/Ssl/TotalTime)[1]', 'int') = 0
), ecarts AS (
SELECT hote_distant, total_ms,
CAST(ROUND((ts - LAG(ts) OVER (PARTITION BY hote_distant ORDER BY ts)) / 1000.0, 0) AS int) AS ecart_s
FROM vides
), par_ecart AS (
SELECT hote_distant, ecart_s, COUNT(*) AS nb,
SUM(COUNT(*)) OVER (PARTITION BY hote_distant) AS nb_ecarts,
ROW_NUMBER() OVER (PARTITION BY hote_distant ORDER BY COUNT(*) DESC) AS rang
FROM ecarts
WHERE ecart_s IS NOT NULL
GROUP BY hote_distant, ecart_s
)
SELECT v.hote_distant,
COUNT(*) AS nb_connexions,
MAX(v.total_ms) AS total_ms_max,
pe.ecart_s AS ecart_le_plus_frequent_s,
CAST(100.0 * pe.nb / pe.nb_ecarts AS decimal(5, 1)) AS part_de_cet_ecart_pct
FROM vides AS v
LEFT JOIN par_ecart AS pe
ON pe.hote_distant = v.hote_distant
AND pe.rang = 1
GROUP BY v.hote_distant, pe.ecart_s, pe.nb, pe.nb_ecarts
ORDER BY nb_connexions DESC;
-- D. Historique horaire des incidents de connexion, par hôte distant, depuis system_health
WITH ev AS (
SELECT DATEADD(hour, DATEDIFF(hour, 0, f.timestamp_utc), 0) AS heure_utc,
t.x.value('(/event/data[@name="remote_host"]/value)[1]', 'varchar(48)') AS hote_distant,
t.x.value('(/event/data[@name="tds_input_buffer_bytes"]/value)[1]', 'int') AS octets_lus
FROM sys.fn_xe_file_target_read_file(N'system_health*.xel', NULL, NULL, NULL) AS f
CROSS APPLY (SELECT CAST(f.event_data AS xml)) AS t(x)
WHERE f.object_name = N'connectivity_ring_buffer_recorded'
)
SELECT heure_utc, hote_distant,
COUNT(*) AS nb_evenements,
SUM(CASE WHEN octets_lus = 0 THEN 1 ELSE 0 END) AS a_zero_octet_lu
FROM ev
GROUP BY heure_utc, hote_distant
ORDER BY heure_utc, nb_evenements DESC;
-- E. Ring buffer des erreurs de sécurité : API appelées et codes
WITH r AS (
SELECT CAST(rb.record AS xml) AS x
FROM sys.dm_os_ring_buffers AS rb
WHERE rb.ring_buffer_type = N'RING_BUFFER_SECURITY_ERROR'
), p AS (
SELECT x.value('(//Error/APIName)[1]', 'varchar(64)') AS api,
x.value('(//Error/CallingAPIName)[1]', 'varchar(64)') AS api_appelante,
x.value('(//Error/ErrorCode)[1]', 'varchar(20)') AS code_erreur,
x.value('(//Error/SQLErrorCode)[1]', 'varchar(64)') AS code_sql
FROM r
)
SELECT api, api_appelante, code_erreur, code_sql, COUNT(*) AS nb
FROM p
GROUP BY api, api_appelante, code_erreur, code_sql
ORDER BY nb DESC;
Le ring buffer des erreurs de sécurité est petit (128 enregistrements sur l’instance de test, qui en avait déjà évincé), et le sens de ses enregistrements n’est pas documenté : il sert à repérer un code d’erreur SSPI ou Schannel (de la forme 0x8009xxxx), pas à compter des connexions.
-- F. Qui est connecté, et comment, par machine, programme et login
SELECT ec.client_net_address, es.host_name, es.program_name, es.login_name,
ec.net_transport, ec.auth_scheme, ec.encrypt_option,
COUNT(*) AS connexions_vivantes,
SUM(CASE WHEN ec.connect_time > DATEADD(minute, -5, GETDATE()) THEN 1 ELSE 0 END) AS ouvertes_depuis_5_min
FROM sys.dm_exec_connections AS ec
JOIN sys.dm_exec_sessions AS es ON es.session_id = ec.session_id
WHERE es.is_user_process = 1
GROUP BY ec.client_net_address, es.host_name, es.program_name, es.login_name,
ec.net_transport, ec.auth_scheme, ec.encrypt_option
ORDER BY connexions_vivantes DESC;
Une ligne compte les connexions d’une machine, d’un programme et d’un login ; plusieurs processus ou plusieurs chaînes de connexion peuvent s’y cumuler, chacun avec son propre pool. Une ligne qui approche 100, le plafond par défaut, mérite la question à l’équipe applicative.
-- G. Débits de connexion sur 10 secondes. Les compteurs « /sec » de
-- sys.dm_os_performance_counters sont cumulatifs : il faut deux relevés.
DROP TABLE IF EXISTS #pc1, #pc2;
SELECT RTRIM(counter_name) AS compteur, cntr_value, SYSDATETIME() AS t
INTO #pc1
FROM sys.dm_os_performance_counters
WHERE object_name LIKE N'%General Statistics%'
AND RTRIM(counter_name) IN (N'Logins/sec', N'Logouts/sec', N'User Connections', N'Connection Reset/sec');
WAITFOR DELAY '00:00:10';
SELECT RTRIM(counter_name) AS compteur, cntr_value, SYSDATETIME() AS t
INTO #pc2
FROM sys.dm_os_performance_counters
WHERE object_name LIKE N'%General Statistics%'
AND RTRIM(counter_name) IN (N'Logins/sec', N'Logouts/sec', N'User Connections', N'Connection Reset/sec');
SELECT a.compteur, a.cntr_value AS releve_1, b.cntr_value AS releve_2,
CAST(1.0 * (b.cntr_value - a.cntr_value)
/ NULLIF(DATEDIFF(millisecond, a.t, b.t) / 1000.0, 0) AS decimal(12, 2)) AS par_seconde
FROM #pc1 AS a
JOIN #pc2 AS b ON b.compteur = a.compteur
ORDER BY a.compteur;
DROP TABLE #pc1, #pc2;
User Connections est une jauge, pas un compteur cumulatif : seules ses deux valeurs relevées ont un sens, pas son débit.
-- H. Latence moyenne des fichiers, cumulée depuis le démarrage
SELECT DB_NAME(vfs.database_id) AS base, mf.type_desc,
LEFT(mf.physical_name, 2) AS volume,
vfs.io_stall_read_ms / NULLIF(vfs.num_of_reads, 0) AS latence_lecture_ms,
vfs.io_stall_write_ms / NULLIF(vfs.num_of_writes, 0) AS latence_ecriture_ms,
vfs.num_of_reads, vfs.num_of_writes
FROM sys.dm_io_virtual_file_stats(NULL, NULL) AS vfs
JOIN sys.master_files AS mf
ON mf.database_id = vfs.database_id
AND mf.file_id = vfs.file_id
ORDER BY latence_ecriture_ms DESC;
LEFT(physical_name, 2) donne la lettre du volume sous Windows. Ces moyennes sont cumulées depuis le démarrage : pour l’heure de l’incident, deux relevés espacés se comparent, comme pour le bloc G.
Annexe B. Les sessions Extended Events
Une session Extended Events est un objet de l’instance. Les créer, les arrêter et les supprimer chacune dans un lot séparé : sur l’instance de test, une session supprimée alors qu’elle tournait, dans le même lot qu’une recréation sous le même nom, est restée active sans plus figurer au catalogue, et n’a disparu qu’après un redémarrage de l’instance. Après suppression, sys.server_event_sessions et sys.dm_xe_sessions ne doivent plus la montrer ni l’une ni l’autre.
La mesure du pool
Une cible histogram regroupe les événements login par valeur de is_cached : deux compteurs en mémoire, aucun fichier.
-- Part des connexions reprises du pool : un compteur par valeur de is_cached
CREATE EVENT SESSION [mesure_pool] ON SERVER
ADD EVENT sqlserver.login (
-- pour un seul login : WHERE sqlserver.server_principal_name = N'<login applicatif>'
)
ADD TARGET package0.histogram (
SET filtering_event_name = N'sqlserver.login',
source = N'is_cached',
source_type = 0)
WITH (MAX_DISPATCH_LATENCY = 5 SECONDS, STARTUP_STATE = OFF);
GO
ALTER EVENT SESSION [mesure_pool] ON SERVER STATE = START;
GO
Après une ou deux minutes de charge normale, notées pour le calcul des débits :
-- Lecture de l'histogramme
SELECT b.value('(value/text())[1]', 'varchar(10)') AS is_cached,
b.value('@count', 'bigint') AS nb
FROM sys.dm_xe_session_targets AS st
JOIN sys.dm_xe_sessions AS s ON s.address = st.event_session_address
CROSS APPLY (SELECT CAST(st.target_data AS xml)) AS t(x)
CROSS APPLY t.x.nodes('/HistogramTarget/Slot') AS n(b)
WHERE s.name = N'mesure_pool';
Puis la session s’arrête et se supprime :
ALTER EVENT SESSION [mesure_pool] ON SERVER STATE = STOP;
GO
DROP EVENT SESSION [mesure_pool] ON SERVER;
GO
La session permanente
La session suivante garde, dans des fichiers qui survivent au redémarrage, les enregistrements du ring buffer de connectivité, les erreurs, et les connexions physiques avec leur client_connection_id. Le filtre des erreurs retient la gravité 16 et plus, et nommément les erreurs de connexion de gravité plus basse, qu’un filtre sur la seule gravité perdrait : 18456 (échec de login), 18452 (domaine non approuvé), 18470 (compte désactivé), 18486 (compte verrouillé), 18487 et 18488 (mot de passe expiré ou à changer), toutes de gravité 14, et 4060 et 4064 (base inaccessible à la connexion), de gravité 11. La liste se complète selon ce que l’on cherche.
-- Session permanente : incidents de connexion, erreurs, connexions physiques
CREATE EVENT SESSION [connectivite] ON SERVER
ADD EVENT sqlserver.connectivity_ring_buffer_recorded,
ADD EVENT sqlserver.error_reported (
ACTION (sqlserver.client_hostname, sqlserver.client_app_name,
sqlserver.username, sqlserver.client_connection_id,
sqlserver.session_id)
WHERE severity >= 16
OR error_number = 18456 OR error_number = 18452 OR error_number = 18470
OR error_number = 18486 OR error_number = 18487 OR error_number = 18488
OR error_number = 4060 OR error_number = 4064),
ADD EVENT sqlserver.login (
SET collect_options_text = 1
ACTION (sqlserver.client_hostname, sqlserver.client_app_name,
sqlserver.username, sqlserver.client_connection_id,
sqlserver.session_id)
WHERE is_cached = 0)
ADD TARGET package0.event_file (
SET filename = N'connectivite.xel',
max_file_size = 64,
max_rollover_files = 8)
WITH (MAX_MEMORY = 8 MB,
EVENT_RETENTION_MODE = ALLOW_SINGLE_EVENT_LOSS,
MAX_DISPATCH_LATENCY = 15 SECONDS,
STARTUP_STATE = ON);
GO
ALTER EVENT SESSION [connectivite] ON SERVER STATE = START;
GO
Sur l’instance de test, un nom de fichier sans chemin a placé les fichiers dans le répertoire du journal d’erreurs, et la lecture les retrouve par le même nom relatif. Les fichiers restent sur le disque après la suppression de la session : jusqu’à neuf fichiers de 64 Mo avec ces réglages, à effacer à la main. Le filtre is_cached = 0 ne garde que les connexions physiques : sur une instance qui encaisse des centaines de reprises du pool par seconde, l’événement login sans filtre remplirait vite les fichiers. Surveiller tout de même leur rotation les premiers jours.
-- Lecture de la session permanente, la plus récente en premier
SELECT f.object_name AS evenement,
f.timestamp_utc AS heure_utc,
t.x.value('(event/action[@name="client_hostname"]/value)[1]', 'nvarchar(128)') AS machine,
t.x.value('(event/action[@name="client_app_name"]/value)[1]', 'nvarchar(256)') AS application,
COALESCE(t.x.value('(event/action[@name="client_connection_id"]/value)[1]', 'varchar(64)'),
t.x.value('(event/data[@name="connection_peer_id"]/value)[1]', 'varchar(64)')) AS client_connection_id,
t.x.value('(event/data[@name="error_number"]/value)[1]', 'int') AS numero_erreur,
t.x.value('(event/data[@name="state"]/value)[1]', 'int') AS etat,
t.x.value('(event/data[@name="message"]/value)[1]', 'nvarchar(max)') AS message,
t.x.value('(event/data[@name="type"]/text)[1]', 'varchar(30)') AS type_enregistrement,
t.x.value('(event/data[@name="remote_host"]/value)[1]', 'varchar(48)') AS hote_distant,
t.x.value('(event/data[@name="sni_consumer_error"]/value)[1]', 'int') AS erreur_sni,
t.x.value('(event/data[@name="total_login_time_ms"]/value)[1]', 'int') AS total_login_ms
FROM sys.fn_xe_file_target_read_file(N'connectivite*.xel', NULL, NULL, NULL) AS f
CROSS APPLY (SELECT CAST(f.event_data AS xml)) AS t(x)
ORDER BY heure_utc DESC;
Les horodatages des Extended Events sont en UTC. Un login refusé produit deux événements error_reported 18456 : l’un porte l’état réel et la raison, l’autre l’état 1, celui que reçoit le client. Pour compter les échecs, écarter l’état 1 ou compter les client_connection_id distincts. Pour retirer la session : ALTER EVENT SESSION [connectivite] ON SERVER STATE = STOP; puis, dans un autre lot, DROP EVENT SESSION [connectivite] ON SERVER;.
Annexe C. La résolution de nom
Test 1, sur le serveur SQL
$nom = 'sqlprod01.domaine.local' # nom tel que les clients l'utilisent
Resolve-DnsName $nom -Type A_AAAA | Where-Object { $_.IPAddress } |
Select-Object Name, Type, IPAddress, Section
Get-NetIPAddress | Select-Object InterfaceAlias, IPAddress, AddressFamily | Sort-Object InterfaceAlias
Get-DnsClient | Select-Object InterfaceAlias, RegisterThisConnectionsAddress
Get-NetAdapter -IncludeHidden | Select-Object Name, InterfaceDescription, Status
# Séparer ce que sert le DNS de ce que fournissent le fichier hosts et la résolution locale
Resolve-DnsName $nom -Type AAAA -DnsOnly
Resolve-DnsName $nom -Type AAAA -NoHostsFile
Un nom sain rend les seules adresses où l’instance écoute, sans adresse fe80:: ni 169.254..
Test 2, sur une machine applicative : chaque adresse publiée
$cible = 'sqlprod01.domaine.local'
$port = 1433
$adresses = Resolve-DnsName $cible -Type A_AAAA |
Where-Object { $_.IPAddress } | Select-Object -ExpandProperty IPAddress
foreach ($ip in $adresses) {
$chrono = [Diagnostics.Stopwatch]::StartNew()
$client = New-Object Net.Sockets.TcpClient
try {
$fini = $client.ConnectAsync($ip, $port).Wait(10000)
$etat = if ($fini) { 'OK' } else { 'TIMEOUT 10 s' }
} catch {
$etat = 'ECHEC : ' + $_.Exception.GetBaseException().Message
} finally {
$chrono.Stop(); $client.Close()
}
'{0,-42} {1,7} ms {2}' -f $ip, $chrono.ElapsedMilliseconds, $etat
}
Test 3, sur la même machine : par nom et par adresse
function Mesure-Ouverture {
param([string]$Serveur, [int]$Repetitions = 20)
# Pooling=false : sans lui, seule la première ouverture serait physique.
$cs = "Server=$Serveur;Database=master;Integrated Security=SSPI;" +
"Connect Timeout=15;Pooling=false;Application Name=diag-connexion"
$durees = @()
for ($i = 1; $i -le $Repetitions; $i++) {
$chrono = [Diagnostics.Stopwatch]::StartNew()
$cx = New-Object Data.SqlClient.SqlConnection $cs
try { $cx.Open(); $etat = 'OK' }
catch { $etat = $_.Exception.Message.Split([char]10)[0] }
finally { $chrono.Stop(); $cx.Dispose() }
$durees += $chrono.ElapsedMilliseconds
'{0,3} {1,7} ms {2}' -f $i, $chrono.ElapsedMilliseconds, $etat
}
$tri = $durees | Sort-Object
'{0} : min {1} ms, médiane {2} ms, max {3} ms' -f $Serveur, $tri[0], $tri[[int][math]::Floor($tri.Count / 2)], $tri[-1]
}
Mesure-Ouverture -Serveur 'sqlprod01.domaine.local'
Mesure-Ouverture -Serveur '192.0.2.10'
Le script utilise System.Data.SqlClient, présent dans Windows PowerShell sans rien installer. Avec un login SQL, remplacer Integrated Security=SSPI par User ID=...;Password=..., en tapant la chaîne dans la console, jamais dans un fichier ni dans un mail. Une connexion par adresse IP en authentification Windows peut passer en NTLM plutôt qu’en Kerberos, selon la configuration du domaine : un écart entre les deux séries peut aussi venir de là, et la colonne auth_scheme de sys.dm_exec_connections le dit.
Annexe D. La collecte sur la machine cliente
À lancer dans une console PowerShell élevée, sur la machine applicative qui a produit l’erreur, de préférence pendant un épisode.
$serveurSql = 'sqlprod01.domaine.local'
$domaine = $env:USERDNSDOMAIN # vide sous un compte local : écrire le nom du domaine
"Machine $env:COMPUTERNAME, heure locale $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss zzz')"
# 1. Antivirus et filtres réseau
try {
Get-MpComputerStatus -ErrorAction Stop |
Select-Object AntivirusEnabled, RealTimeProtectionEnabled, AMProductVersion
} catch { 'Defender absent ou inactif : produit installé à identifier' }
fltmc filters
netsh winsock show catalog | Select-String 'Description'
# 2. Journal système, trois jours
Get-WinEvent -FilterHashtable @{LogName='System'; StartTime=(Get-Date).AddDays(-3); Level=1,2,3} |
Where-Object { $_.ProviderName -match 'Netlogon|LsaSrv|Kerberos|Schannel|Tcpip' } |
Select-Object TimeCreated, ProviderName, Id, LevelDisplayName, Message
# 3. Contrôleur de domaine
nltest /dsgetdc:$domaine
nltest /sc_verify:$domaine
# 4. Ports éphémères, à lancer PENDANT l'épisode
netsh int ipv4 show dynamicport tcp
Get-NetTCPConnection | Group-Object State | Select-Object Name, Count
Get-NetTCPConnection -RemotePort 1433 -ErrorAction SilentlyContinue |
Group-Object State | Select-Object Name, Count
# 5. Connexion TCP brute vers le serveur
Test-NetConnection $serveurSql -Port 1433 | Select-Object ComputerName, RemoteAddress, TcpTestSucceeded
Le point d’entrée de Microsoft pour tout le sujet est la page Résoudre les problèmes de connectivité dans SQL Server, qui renvoie vers la plupart des pages par symptôme citées plus haut et vers les outils de l’équipe de support, dont SQLCheck.