In alcuni dei miei precedenti articoli qui sull'ottimizzazione delle prestazioni, ho discusso di più tipi di attesa e di come sono indicativi di vari colli di bottiglia delle risorse. Sto iniziando una nuova serie su scenari in cui un meccanismo di sincronizzazione chiamato latch è un collo di bottiglia delle prestazioni, e in particolare latch non di pagina. In questo post iniziale spiegherò perché sono necessari i latch, cosa sono effettivamente e come possono essere un collo di bottiglia.
Perché sono necessari i fermi?
È un principio di base dell'informatica che ogni volta che una struttura dati esiste in un sistema multi-thread, la struttura dati deve essere protetta in qualche modo. Questa protezione prevede le seguenti condizioni:
- (Garantito) Una struttura dati non può essere modificata da un thread mentre un altro thread la sta leggendo
- (Garantito) Una struttura dati non può essere letta da un thread mentre un altro thread la sta modificando
- (Garantito) Una struttura dati non può essere modificata da due o più thread contemporaneamente
- (Facoltativo) Consenti a due o più thread di leggere la struttura dei dati contemporaneamente
- (Facoltativo) Consenti ai thread di accodarsi in modo ordinato per l'accesso alla struttura dei dati
Questo può essere fatto in diversi modi, tra cui:
- Un meccanismo che consente solo a un singolo thread alla volta di avere accesso alla struttura dei dati. SQL Server implementa questo meccanismo e lo chiama spinlock. Ciò consente n. 1, n. 2 e n. 3 sopra.
- Un meccanismo che consente a più thread di leggere la struttura dei dati contemporaneamente (cioè hanno accesso condiviso), consente a un singolo thread di ottenere l'accesso esclusivo alla struttura dei dati (ad esclusione di tutti gli altri thread) e implementa un modo equo di fare la fila per l'accesso. SQL Server implementa questo meccanismo e lo chiama un latch. Ciò consente tutte e cinque le disposizioni di cui sopra.
Allora perché SQL Server usa sia gli spinlock che i latch? Ad alcune strutture di dati si accede così frequentemente che un latch è semplicemente troppo costoso e quindi viene utilizzato uno spinlock molto leggero. Due esempi di tali strutture di dati sono l'elenco dei buffer liberi nel pool di buffer e l'elenco dei blocchi nel gestore dei blocchi.
Cos'è un fermo?
Un latch è un meccanismo di sincronizzazione che protegge una singola struttura di dati e in SQL Server esistono tre tipi principali di latch:
- Si blocca per proteggere una pagina di file di dati durante la lettura dal disco. Questi vengono visualizzati mentre PAGEIOLATCH_XX attende e ne ho discusso in questo post.
- Latch protegge l'accesso a una pagina di file di dati che è già in memoria (una pagina da 8 KB nel pool di buffer è in realtà solo una struttura di dati). Questi vengono visualizzati mentre PAGELATCH_XX attende e ne ho discusso in questo post.
- Latch protegge le strutture di dati non di pagina. Questi vengono visualizzati come LATCH_SH e LATCH_EX attende.
In questa serie, ci concentreremo sul terzo tipo di chiavistelli.
Un latch è esso stesso una piccola struttura di dati e puoi pensare che abbia tre componenti:
- Una descrizione della risorsa (di ciò che sta proteggendo)
- Un campo di stato che indica in quali modalità è attualmente mantenuto il latch, quanti thread mantengono il latch in quella modalità e se ci sono thread in attesa (oltre ad altre cose di cui non dobbiamo preoccuparci)
- Una coda first-in-first-out di thread in attesa di accesso alla struttura dati e quali modalità di accesso sono in attesa (denominata coda di attesa)
Per i latch non di pagina ci limiteremo a considerare solo le modalità di accesso SH (condivisione) per la lettura della struttura dati ed EX (esclusiva) per la modifica della struttura dati. Ci sono altre modalità più esotiche, ma sono usate raramente e non appariranno come punti di contesa, quindi farò finta che non esistano per il resto di questa discussione.
Alcuni di voi potrebbero sapere che ci sono anche complicazioni più profonde relative ai superlatch/sottolatch e al partizionamento dei latch per la scalabilità, ma non è necessario andare così in profondità ai fini di questa serie.
Acquisizione di un fermo
Quando un thread vuole acquisire un latch, esamina lo stato del latch.
Se il thread vuole acquisire il latch in modalità EX, può farlo solo se non ci sono thread che trattengono il latch in nessuna modalità. In tal caso, il thread acquisisce il latch in modalità EX e imposta lo stato per indicarlo. Se sono presenti uno o più thread che tengono già il latch, il thread imposta lo stato per indicare che c'è un thread in attesa, si inserisce in fondo alla coda di attesa e quindi viene sospeso (nella lista dei waiter dello scheduler è attivo ) in attesa di LATCH_EX.
Se il thread vuole acquisire il latch in modalità SH, può farlo solo se nessun thread mantiene il latch o gli unici thread che tengono il latch sono in modalità SH *e* non ci sono thread in attesa di acquisire il latch. In tal caso, il thread acquisisce il latch in modalità SH, imposta lo stato per indicarlo e incrementa il conteggio dei thread che mantengono il latch. Se il latch è trattenuto in modalità EX o sono presenti uno o più thread in attesa, il thread imposta lo stato per indicare che è presente un thread in attesa, si inserisce in fondo alla coda di attesa e quindi viene sospeso in attesa di LATCH_SH.
Il controllo dei thread in attesa viene eseguito per garantire l'equità a un thread in attesa del latch in modalità EX. Dovrà solo attendere i thread che tengono il latch in modalità SH che ha acquisito il latch prima che inizi ad aspettare. Senza tale controllo, potrebbe verificarsi un termine informatico chiamato "fame", quando un flusso costante di thread che acquisiscono il latch in modalità SH impedisce al thread in modalità EX di essere in grado di acquisire il latch.
Rilascio di un fermo
Se il thread mantiene il latch in modalità EX, annulla lo stato che mostra che il latch è trattenuto in modalità EX e quindi controlla se ci sono thread in attesa.
Se il thread mantiene il latch in modalità SH, diminuisce il conteggio dei thread in modalità SH. Se il conteggio ora è diverso da zero, il thread di rilascio viene eseguito con il latch. Se il conteggio *è* ora zero, annulla lo stato che mostra che il latch è in modalità SH e quindi controlla se ci sono thread in attesa.
Se non ci sono thread in attesa, il thread di rilascio viene eseguito con il latch.
Se il capo della coda di attesa è in attesa della modalità EX, il thread di rilascio esegue le seguenti operazioni:
- Imposta lo stato per mostrare che il fermo è tenuto in modalità EX
- Rimuove il thread in attesa dall'inizio della coda e lo imposta come proprietario del latch
- Segnala al thread in attesa che è il proprietario ed è ora eseguibile (spostando, concettualmente, il thread in attesa dall'elenco dei camerieri sullo scheduler alla coda eseguibile sullo scheduler)
- Ed è fatto con il chiavistello
Se il capo della coda di attesa è in attesa in modalità SH (cosa che può verificarsi solo se il thread di rilascio era in modalità EX), il thread di rilascio esegue le seguenti operazioni:
- Imposta lo stato per mostrare che il fermo è tenuto in modalità SH
- Per tutti i thread nella coda di attesa in attesa della modalità SH
- Rimuove il thread in attesa dall'inizio della coda
- Aumenta il conteggio dei thread che tengono il fermo
- Segnala al thread in attesa che è un proprietario ed è ora eseguibile
- Ed è fatto con il chiavistello
In che modo i chiavistelli possono essere un punto di contesa?
A differenza dei blocchi, i latch vengono generalmente mantenuti solo per la durata dell'operazione di lettura o modifica, quindi sono piuttosto leggeri, ma a causa dell'incompatibilità SH vs. EX, possono essere un punto di contesa altrettanto grande dei blocchi. Ciò può accadere quando molti thread stanno tentando di acquisire un latch in modalità EX (solo uno alla volta può farlo) o quando molti thread stanno tentando di acquisire un latch in modalità SH e un altro thread mantiene il latch in modalità EX.
Riepilogo
Più thread nel sistema si contendono un latch "caldo", maggiore è la contesa e più negativo sarà l'effetto sul throughput del carico di lavoro. Probabilmente hai sentito parlare di noti problemi di contesa sui latch, ad esempio sulle bitmap di allocazione di tempdb, ma la contesa può verificarsi anche per i latch non di pagina.
Ora che ti ho fornito informazioni sufficienti per comprendere i latch e come funzionano, nei prossimi articoli esaminerò alcuni problemi di contesa di latch non di pagina nella vita reale e spiegherò come prevenirli o risolverli.