Dans certains de mes articles précédents ici sur le réglage des performances, j'ai discuté de plusieurs types d'attente et de la façon dont ils indiquent divers goulots d'étranglement des ressources. Je commence une nouvelle série sur des scénarios où un mécanisme de synchronisation appelé verrou est un goulot d'étranglement des performances, et plus particulièrement des verrous non liés à la page. Dans ce premier article, je vais expliquer pourquoi les verrous sont nécessaires, ce qu'ils sont réellement et comment ils peuvent constituer un goulot d'étranglement.
Pourquoi les loquets sont-ils nécessaires ?
C'est un principe de base de l'informatique que chaque fois qu'une structure de données existe dans un système multithread, la structure de données doit être protégée d'une manière ou d'une autre. Cette protection donne les conditions suivantes :
- (Garanti) Une structure de données ne peut pas être modifiée par un thread pendant qu'un autre thread la lit
- (Garanti) Une structure de données ne peut pas être lue par un thread pendant qu'un autre thread la modifie
- (Garanti) Une structure de données ne peut pas être modifiée par deux threads ou plus en même temps
- (Facultatif) Autoriser deux threads ou plus à lire la structure de données en même temps
- (Facultatif) Autoriser les threads à se mettre en file d'attente dans l'ordre pour accéder à la structure de données
Cela peut se faire de plusieurs manières, notamment :
- Un mécanisme qui n'autorise jamais qu'un seul thread à la fois à avoir accès à la structure de données. SQL Server implémente ce mécanisme et l'appelle un spinlock. Cela autorise #1, #2 et #3 ci-dessus.
- Un mécanisme qui permet à plusieurs threads de lire la structure de données en même temps (c'est-à-dire qu'ils ont un accès partagé), permet à un seul thread d'obtenir un accès exclusif à la structure de données (à l'exclusion de tous les autres threads) et implémente une façon équitable de faire la queue pour l'accès. SQL Server implémente ce mécanisme et l'appelle un verrou. Cela autorise les cinq conditions ci-dessus.
Alors pourquoi SQL Server utilise-t-il à la fois des spinlocks et des verrous ? Certaines structures de données sont consultées si fréquemment qu'un verrou est tout simplement trop cher et qu'un verrou tournant très léger est donc utilisé à la place. Deux exemples de telles structures de données sont la liste des tampons libres dans le pool de tampons et la liste des verrous dans le gestionnaire de verrous.
Qu'est-ce qu'un loquet ?
Un verrou est un mécanisme de synchronisation qui protège une seule structure de données et il existe trois grands types de verrou dans SQL Server :
- Verrous protégeant une page de fichier de données pendant sa lecture à partir du disque. Celles-ci s'affichent pendant que PAGEIOLATCH_XX attend, et j'en ai parlé dans cet article.
- Verrous protégeant l'accès à une page de fichier de données déjà en mémoire (une page de 8 Ko dans le pool de mémoire tampon n'est en réalité qu'une structure de données). Ceux-ci s'affichent pendant que PAGELATCH_XX attend, et j'en ai discuté dans ce post.
- Verrous protégeant les structures de données autres que les pages. Celles-ci apparaissent sous forme d'attentes LATCH_SH et LATCH_EX.
Dans cette série, nous allons nous concentrer sur le troisième type de verrous.
Un verrou est lui-même une petite structure de données et vous pouvez le considérer comme ayant trois composants :
- Une description de la ressource (de ce qu'elle protège)
- Un champ d'état indiquant dans quels modes le verrou est actuellement maintenu, combien de threads maintiennent le verrou dans ce mode et s'il y a des threads en attente (ainsi que d'autres éléments dont nous n'avons pas à nous préoccuper)
- Une file d'attente premier entré, premier sorti de threads qui attendent d'accéder à la structure de données, et les modes d'accès qu'ils attendent (appelée file d'attente)
Pour les non-page latchs, on se bornera à ne considérer que les modes d'accès SH (partagé) pour la lecture de la structure des données et EX (exclusif) pour la modification de la structure des données. Il existe d'autres modes plus exotiques, mais ils sont rarement utilisés et n'apparaîtront pas comme des points de discorde, donc je vais prétendre qu'ils n'existent pas pour le reste de cette discussion.
Certains d'entre vous savent peut-être qu'il existe également des complications plus profondes autour des super-verrouillages/sous-verrouillages et du partitionnement des verrous pour l'évolutivité, mais nous n'avons pas besoin d'aller à cette profondeur pour les besoins de cette série.
Acquérir un verrou
Lorsqu'un thread souhaite acquérir un verrou, il examine l'état du verrou.
Si le thread veut acquérir le verrou en mode EX, il ne peut le faire que s'il n'y a aucun thread qui détient le verrou dans aucun mode. Si tel est le cas, le thread acquiert le verrou en mode EX et définit le statut pour l'indiquer. S'il y a un ou plusieurs threads tenant déjà le verrou, le thread définit le statut pour indiquer qu'il y a un thread en attente, entre lui-même au bas de la file d'attente, puis est suspendu (sur la liste des serveurs du planificateur, il est sur ) en attente de LATCH_EX.
Si le thread veut acquérir le verrou en mode SH, il ne peut le faire que si aucun thread ne détient le verrou ou si les seuls threads détenant le verrou sont en mode SH *et* qu'il n'y a aucun thread en attente d'acquérir le verrou. Si tel est le cas, le thread acquiert le verrou en mode SH, définit l'état pour l'indiquer et incrémente le nombre de threads détenant le verrou. Si le verrou est maintenu en mode EX ou s'il y a un ou plusieurs threads en attente, alors le thread définit le statut pour indiquer qu'il y a un thread en attente, entre lui-même au bas de la file d'attente, puis est suspendu en attendant LATCH_SH.
La vérification des threads en attente est effectuée pour garantir l'équité d'un thread en attente du verrou en mode EX. Il n'aura qu'à attendre que les threads détenant le verrou en mode SH aient acquis le verrou avant de commencer à attendre. Sans cette vérification, un terme informatique appelé "famine" peut se produire, lorsqu'un flux constant de threads acquérant le verrou en mode SH empêche le thread en mode EX de pouvoir acquérir le verrou.
Libérer un loquet
Si le thread maintient le verrou en mode EX, il annule l'état indiquant que le verrou est maintenu en mode EX, puis vérifie s'il y a des threads en attente.
Si le thread maintient le verrou en mode SH, il décrémente le nombre de threads en mode SH. Si le compte est maintenant différent de zéro, le fil de libération se fait avec le verrou. Si le compte *est* maintenant nul, il annule l'état indiquant que le verrou est maintenu en mode SH, puis vérifie s'il y a des threads en attente.
S'il n'y a pas de threads en attente, la libération du thread se fait avec le verrou.
Si le chef de la file d'attente attend le mode EX, le thread de libération fait ce qui suit :
- Définit l'état pour indiquer que le verrou est maintenu en mode EX
- Supprime le thread en attente de la tête de file d'attente et le définit comme propriétaire du verrou
- Signale au thread en attente qu'il est le propriétaire et qu'il est maintenant exécutable (en déplaçant, conceptuellement, le thread en attente de la liste des serveurs sur son planificateur vers la file d'attente exécutable sur le planificateur)
- Et c'est fait avec le loquet
Si la tête de la file d'attente attend en mode SH (ce qui ne peut être le cas que si le thread de libération était en mode EX), le thread de libération fait ce qui suit :
- Définit l'état pour indiquer que le verrou est maintenu en mode SH
- Pour tous les threads de la file d'attente qui attendent le mode SH
- Supprime le fil en attente de la tête de file d'attente
- Incrémente le nombre de threads tenant le verrou
- Indique au thread en attente qu'il est propriétaire et qu'il est maintenant exécutable
- Et c'est fait avec le loquet
Comment les verrous peuvent-ils être un point de conflit ?
Contrairement aux verrous, les verrous ne sont généralement maintenus que pendant la durée de l'opération de lecture ou de modification, ils sont donc assez légers, mais en raison de l'incompatibilité SH vs EX, ils peuvent être tout aussi importants que les verrous. Cela peut se produire lorsque de nombreux threads tentent tous d'acquérir un verrou en mode EX (un seul à la fois le peut) ou lorsque plusieurs threads tentent d'acquérir un verrou en mode SH et qu'un autre thread maintient le verrou en mode EX.
Résumé
Plus il y a de threads dans le système qui se disputent un verrou « chaud », plus le conflit est élevé et plus l'effet sur le débit de la charge de travail sera négatif. Vous avez probablement entendu parler de problèmes de conflit de verrous bien connus, par exemple autour des bitmaps d'allocation tempdb, mais des conflits peuvent également se produire pour des verrous autres que des pages.
Maintenant que je vous ai donné suffisamment d'informations pour comprendre les verrous et leur fonctionnement, dans les prochains articles, j'examinerai certains problèmes réels de conflit de verrous non liés à la page et expliquerai comment les prévenir ou les contourner.