Continuando la mia serie di articoli sui latch, questa volta parlerò del latch APPEND_ONLY_STORAGE_INSERT_POINT e mostrerò come può essere un collo di bottiglia importante per carichi di lavoro di aggiornamento pesanti in cui viene utilizzata una delle due forme di isolamento degli snapshot.
Ti consiglio vivamente di leggere il post iniziale della serie prima di questo, in modo da avere tutte le conoscenze di base generali sui fermi.
Che cos'è il fermo APPEND_ONLY_STORAGE_INSERT_POINT?
Per spiegare questo latch, ho bisogno di spiegare un po' come funziona l'isolamento degli snapshot.
Quando si abilita una delle due forme di controllo delle versioni, SQL Server utilizza un meccanismo denominato controllo delle versioni per preservare le versioni precedenti alla modifica di un record nel archivio versioni in tempdb. Questo viene fatto come segue:
- Un record viene identificato come in procinto di essere modificato.
- Il record corrente viene copiato nell'archivio versioni.
- Il record è stato modificato.
- Se il record non disponeva già di un tag di controllo delle versioni a 14 byte , uno viene aggiunto alla fine del record. Il tag contiene un timestamp (non un tempo reale) e un puntatore alla versione precedente del record nell'archivio versioni.
- Se il record disponeva già di un tag di controllo delle versioni, viene aggiornato con il nuovo timestamp e il nuovo puntatore dell'archivio versioni.
Il timestamp del controllo delle versioni a livello di istanza viene incrementato ogni volta che inizia una nuova istruzione o batch o viene creata una nuova versione di un record, in qualsiasi database in cui è abilitata una delle due forme di isolamento dello snapshot. Questo timestamp viene utilizzato per assicurarsi che una query elabori la versione corretta di un record.
Ad esempio, immagina un database in cui è stata abilitata la lettura dello snapshot commit, in modo che ogni istruzione possa vedere i record dal momento in cui è iniziata l'istruzione. Il timestamp di controllo delle versioni è impostato per l'inizio dell'istruzione, quindi qualsiasi record che incontra che ha un timestamp più alto è la versione "sbagliata" e quindi la versione "giusta", con un timestamp prima del timestamp dell'istruzione, deve essere recuperata dal negozio di versioni. I meccanismi di questo processo non sono rilevanti ai fini di questo post.
Quindi, come vengono archiviate fisicamente le versioni nell'archivio delle versioni? L'intero record precedente alla modifica, comprese le colonne fuori riga, viene copiato nell'archivio delle versioni, suddiviso in blocchi da 8.000 byte, che possono estendersi su due pagine se necessario (ad esempio, 2.000 byte alla fine di una pagina e 6.000 byte a l'inizio del successivo). Questa memoria per scopi speciali è composta da unità di allocazione di sola aggiunta e viene utilizzato solo per le operazioni di archivio delle versioni. Si chiama così perché i nuovi dati possono essere aggiunti solo immediatamente dopo la fine dell'ultima versione inserita. Ogni tanto viene creata una nuova unità di allocazione e ciò consente alla pulizia regolare dell'archivio delle versioni di essere molto efficiente, poiché un'unità di allocazione non necessaria può essere semplicemente eliminata. Ancora una volta, i meccanismi di ciò esulano dallo scopo di questo post.
E ora arriviamo alla definizione del latch:qualsiasi thread che deve copiare un record pre-modifica nell'archivio versioni deve sapere dove si trova il punto di inserimento nell'unità di allocazione di sola aggiunta corrente. Queste informazioni sono protette dal blocco APPEND_ONLY_STORAGE_INSERT_POINT.
In che modo il fermo diventa un collo di bottiglia?
Ecco il problema:esiste solo una modalità accettabile in cui è possibile acquisire il latch APPEND_ONLY_STORAGE_INSERT_POINT:modalità EX (esclusiva). E come saprai leggendo il post introduttivo alla serie, solo un thread alla volta può tenere il fermo in modalità EX.
Riunendo tutte queste informazioni:quando uno o più database hanno l'isolamento degli snapshot abilitato e c'è un carico di lavoro simultaneo sufficientemente elevato di aggiornamenti per quei database, ci saranno molte versioni generate dalle varie connessioni e questo latch diventerà un un po' di collo di bottiglia, con la dimensione del collo di bottiglia che aumenta all'aumentare del carico di lavoro di aggiornamento dove è coinvolto il controllo delle versioni.
Mostra il collo di bottiglia
Puoi facilmente riprodurre il collo di bottiglia per te stesso. L'ho fatto come segue:
- Creata una tabella con un gruppo di colonne intere denominate cXXX dove XXX è un numero e un indice cluster su una colonna di identità int denominata DocID
- Inseriti 100.000 record, con valori casuali per tutte le colonne
- Creato uno script con un ciclo infinito per selezionare un DocID casuale nell'intervallo da 1 a 10.000, selezionare un nome di colonna casuale e incrementare il valore della colonna di 1 (quindi creando una versione)
- Creato nove script identici, ma ciascuno selezionando da un diverso intervallo di chiavi del cluster di 10.000 valori
- Imposta DELAYED_DURABILITY su FORCED per ridurre le attese di WRITELOG (è vero che lo faresti raramente, ma aiuta ad esacerbare il collo di bottiglia a scopo dimostrativo)
Ho quindi eseguito tutti e dieci gli script contemporaneamente e misurato il contatore dei metodi di accesso:ricerche indice/sec per tenere traccia del numero di aggiornamenti in corso. Non potevo usare Database:Richieste batch/sec poiché ogni script aveva un solo batch (il ciclo infinito) e non volevo usare Transazioni/sec poiché poteva contare le transazioni interne oltre a quella che avvolgeva ogni aggiornamento.
Quando l'isolamento degli snapshot non era abilitato, sul mio laptop Windows 10 con SQL Server 2019 ricevevo circa 80.000 aggiornamenti al secondo attraverso le dieci connessioni. Quindi, quando ho attivato l'impostazione READ_COMMMITED_SNAPSHOT per il database e ho eseguito nuovamente il test, il throughput del carico di lavoro è sceso a circa 60.000 aggiornamenti al secondo (un calo del 25% del throughput). Dall'analisi delle statistiche sulle attese, l'85% di tutte le attese era LATCH_EX e dalle statistiche sui latch, il 100% riguardava APPEND_ONLY_STORAGE_INSERT_POINT.
Tieni presente che ho impostato lo scenario per mostrare il collo di bottiglia nel peggiore dei casi. In un ambiente reale con un carico di lavoro misto, la guida generalmente accettata per un calo del throughput quando si utilizza l'isolamento degli snapshot è del 10-15%.
Riepilogo
Un'altra potenziale area che potrebbe essere influenzata da questo collo di bottiglia sono i secondari leggibili dal gruppo di disponibilità. Se una replica del database è impostata per essere leggibile, tutte le query su di essa utilizzano automaticamente l'isolamento dello snapshot e tutta la riproduzione dei record di registro dal database primario genererà versioni. Con un carico di lavoro di aggiornamento sufficientemente elevato proveniente dal database primario e molti database impostati per essere leggibili, e con la ripetizione parallela che è la norma per i gruppi di disponibilità secondari, il latch APPEND_ONLY_STORAGE_INSERT_POINT potrebbe diventare un collo di bottiglia anche su un gruppo di disponibilità leggibile secondario, il che potrebbe portare al secondario in ritardo rispetto al primario. Non l'ho testato, ma è esattamente lo stesso meccanismo che ho descritto sopra, quindi sembra probabile. In tal caso, è possibile disabilitare la ripetizione parallela utilizzando il flag di traccia 3459, ma ciò potrebbe comportare un throughput complessivo peggiore sul secondario.
Mettendo da parte lo scenario del gruppo di disponibilità, sfortunatamente, non utilizzare l'isolamento degli snapshot è l'unico modo per evitare completamente questo collo di bottiglia, che non è un'opzione praticabile se il tuo carico di lavoro si basa sulla semantica fornita dall'isolamento degli snapshot o se ne hai bisogno per alleviare i problemi di blocco (poiché l'isolamento dello snapshot significa che le query di lettura non acquisiscono blocchi di condivisione che bloccano le query di modifica).
Modifica:dai commenti seguenti, puoi * rimuovere il collo di bottiglia del latch utilizzando ADR in SQL Server 2019, ma le prestazioni sono molto peggiori a causa dell'overhead di ADR. Lo scenario in cui il latch diventa un collo di bottiglia a causa dell'elevato carico di lavoro di aggiornamento non è assolutamente un caso d'uso valido per ADR.