Database
 sql >> Datenbank >  >> RDS >> Database

Informationen zum Leeren des Protokollpuffers

Sie haben wahrscheinlich schon oft gehört, dass SQL Server eine Garantie für ACID-Transaktionseigenschaften bietet. Dieser Artikel konzentriert sich auf den D-Teil, der natürlich für Haltbarkeit steht. Genauer gesagt konzentriert sich dieser Artikel auf einen Aspekt der SQL Server-Protokollierungsarchitektur, der die Dauerhaftigkeit von Transaktionen erzwingt – das Leeren des Protokollpuffers. Ich spreche über die Funktion, die der Protokollpuffer erfüllt, die Bedingungen, die SQL Server zwingen, den Protokollpuffer auf die Festplatte zu leeren, was Sie tun können, um die Transaktionsleistung zu optimieren, sowie über kürzlich hinzugefügte verwandte Technologien wie verzögerte Haltbarkeit und nichtflüchtigen Speicher der Speicherklasse.

Protokollpuffer wird geleert

Der D-Teil in den ACID-Transaktionseigenschaften steht für Dauerhaftigkeit. Auf logischer Ebene bedeutet dies, dass, wenn eine Anwendung SQL Server eine Anweisung zum Festschreiben einer Transaktion sendet (explizit oder mit einer Autocommit-Transaktion), SQL Server die Steuerung normalerweise erst dann an den Aufrufer zurückgibt, wenn garantiert werden kann, dass die Transaktion dauerhaft ist. Mit anderen Worten, sobald der Aufrufer nach dem Festschreiben einer Transaktion die Kontrolle wiedererlangt hat, kann er darauf vertrauen, dass die Transaktionsänderungen auch dann in die Datenbank gelangen, wenn der Server einen Moment später einen Stromausfall erleidet. Solange der Server erfolgreich neu gestartet wird und die Datenbankdateien nicht beschädigt wurden, werden Sie feststellen, dass alle Transaktionsänderungen übernommen wurden.

Die Art und Weise, wie SQL Server die Dauerhaftigkeit von Transaktionen erzwingt, besteht zum Teil darin, sicherzustellen, dass alle Änderungen der Transaktion in das Transaktionsprotokoll der Datenbank auf der Festplatte geschrieben werden bevor die Kontrolle an den Aufrufer zurückgegeben wird. Im Falle eines Stromausfalls, nachdem das Commit einer Transaktion bestätigt wurde, wissen Sie, dass alle diese Änderungen zumindest in das Transaktionsprotokoll auf der Festplatte geschrieben wurden. Dies gilt auch dann, wenn die zugehörigen Datenseiten nur im Datencache (dem Pufferpool) geändert, aber noch nicht in die Datendateien auf der Festplatte übertragen wurden. Wenn Sie SQL Server während der Redo-Phase des Wiederherstellungsprozesses neu starten, verwendet SQL Server die im Protokoll aufgezeichneten Informationen, um Änderungen wiederzugeben, die nach dem letzten Prüfpunkt angewendet wurden und die es nicht in die Datendateien geschafft haben. Je nachdem, welches Wiederherstellungsmodell Sie verwenden und ob Massenoperationen nach dem letzten Prüfpunkt angewendet wurden, gibt es noch etwas mehr zu erzählen, aber für unsere Diskussion reicht es aus, sich auf den Teil zu konzentrieren, der das Härten der Änderungen an der Transaktionsprotokoll.

Der knifflige Teil der Protokollierungsarchitektur von SQL Server besteht darin, dass Protokollschreibvorgänge sequentiell sind. Hätte SQL Server nicht eine Art Protokollpuffer verwendet, um das Schreiben von Protokollen auf die Festplatte zu erleichtern, würden schreibintensive Systeme – insbesondere solche mit vielen kleinen Transaktionen – schnell in schreckliche Leistungsengpässe beim Schreiben von Protokollen geraten.

Um die negativen Auswirkungen auf die Leistung häufiger sequenzieller Protokollschreibvorgänge auf den Datenträger zu mindern, verwendet SQL Server einen Protokollpuffer im Arbeitsspeicher. Protokollschreibvorgänge werden zuerst in den Protokollpuffer ausgeführt, und bestimmte Bedingungen führen dazu, dass SQL Server den Protokollpuffer auf den Datenträger löscht oder härtet. Die gehärtete Einheit (auch bekannt als Protokollblock) kann von einer minimalen Sektorgröße (512 Byte) bis maximal 60 KB reichen. Im Folgenden sind Bedingungen aufgeführt, die eine Protokollpufferspülung auslösen (ignorieren Sie vorerst die Teile, die in eckigen Klammern erscheinen):

  • SQL Server erhält eine Commit-Anforderung einer [vollständig dauerhaften] Transaktion, die Daten [in einer anderen Datenbank als tempdb] ändert
  • Der Protokollpuffer füllt sich und erreicht seine Kapazität von 60 KB
  • SQL Server muss fehlerhafte Datenseiten härten, z. B. während eines Checkpoint-Prozesses, und die Protokolldatensätze, die die Änderungen an diesen Seiten darstellen, wurden noch nicht gehärtet (Write-Ahead-Protokollierung , oder kurz WAL)
  • Sie fordern manuell eine Protokollpufferbereinigung an, indem Sie die Prozedur sys.sp_flush_log ausführen
  • SQL Server schreibt einen neuen Sequenz-Cache-bezogenen Wiederherstellungswert [in eine andere Datenbank als tempdb]

Die ersten vier Bedingungen sollten ziemlich klar sein, wenn Sie die Informationen in eckigen Klammern vorerst ignorieren. Letzteres ist vielleicht noch nicht klar, aber ich werde es später in diesem Artikel ausführlich erläutern.

Die Zeit, die SQL Server auf den Abschluss einer E/A-Operation wartet, die eine Protokollpufferleerung verarbeitet, wird durch den WRITELOG-Wartetyp widergespiegelt.

Warum sind diese Informationen also so interessant und was machen wir damit? Wenn Sie die Bedingungen verstehen, die das Leeren des Protokollpuffers auslösen, können Sie herausfinden, warum bei bestimmten Workloads entsprechende Engpässe auftreten. Außerdem gibt es in einigen Fällen Maßnahmen, die Sie ergreifen können, um solche Engpässe zu verringern oder zu beseitigen. Ich werde eine Reihe von Beispielen behandeln, z. B. eine große Transaktion im Vergleich zu vielen kleinen Transaktionen, vollständig dauerhafte im Vergleich zu verzögerten dauerhaften Transaktionen, Benutzerdatenbank im Vergleich zu tempdb und Zwischenspeichern von Sequenzobjekten.

Eine große Transaktion im Vergleich zu vielen kleinen Transaktionen

Wie bereits erwähnt, ist eine der Bedingungen, die eine Protokollpufferspülung auslöst, wenn Sie eine Transaktion festschreiben, um die Dauerhaftigkeit der Transaktion zu gewährleisten. Das bedeutet, dass bei Workloads, die viele kleine Transaktionen beinhalten, wie OLTP-Workloads, möglicherweise Engpässe beim Schreiben von Protokollen auftreten können.

Auch wenn dies oft nicht der Fall ist, besteht eine einfache und effektive Möglichkeit zur Optimierung der Arbeit darin, die Änderungen in einer einzigen großen Transaktion statt in mehreren kleinen anzuwenden, wenn Sie in einer einzigen Sitzung viele kleine Änderungen übermitteln.

Betrachten Sie das folgende vereinfachte Beispiel (laden Sie PerformanceV3 hier herunter):

SET NOCOUNT ON;
 
USE PerformanceV3;
 
ALTER DATABASE PerformanceV3 SET DELAYED_DURABILITY = Disabled; -- default
 
DROP TABLE IF EXISTS dbo.T1;
 
CREATE TABLE dbo.T1(col1 INT NOT NULL);
 
DECLARE @i AS INT = 1;
 
WHILE @i <= 1000000
BEGIN
 
  BEGIN TRAN
    INSERT INTO dbo.T1(col1) VALUES(@i);
  COMMIT TRAN;
 
  SET @i += 1;
END;

Dieser Code führt 1.000.000 kleine Transaktionen aus, die Daten in einer Benutzerdatenbank ändern. Diese Arbeit löst mindestens 1.000.000 Log-Puffer-Flushes aus. Sie könnten ein paar zusätzliche erhalten, da sich der Protokollpuffer füllt. Sie können die folgende Testvorlage verwenden, um die Anzahl der Leerungen des Protokollpuffers zu zählen und die Zeit zu messen, die für den Abschluss der Arbeit benötigt wurde:

-- Test template
 
-- ... Preparation goes here ...
 
-- Count log flushes and measure time
DECLARE @logflushes AS INT, @starttime AS DATETIME2, @duration AS INT;
 
-- Stats before
SET @logflushes = ( SELECT cntr_value FROM sys.dm_os_performance_counters
                    WHERE counter_name = 'Log Flushes/sec'
                      AND instance_name = @db );
 
SET @starttime = SYSDATETIME();
 
-- ... Actual work goes here ...
 
-- Stats after
SET @duration = DATEDIFF(second, @starttime, SYSDATETIME());
SET @logflushes = ( SELECT cntr_value FROM sys.dm_os_performance_counters
                    WHERE counter_name = 'Log Flushes/sec'
                      AND instance_name = @db ) - @logflushes;
 
SELECT 
  @duration AS durationinseconds,
  @logflushes AS logflushes;

Auch wenn der Name des Leistungsindikators „Log Flushes/sec“ lautet, wird die Anzahl der bisher durchgeführten Log-Buffer-Flushes tatsächlich akkumuliert. Der Code subtrahiert also die Vor-Arbeits-Zählung von der Nach-Arbeits-Zählung, um die Anzahl der durch die Arbeit generierten Log-Flushes zu ermitteln. Dieser Code misst auch die Zeit in Sekunden, die für die Fertigstellung der Arbeit benötigt wurde. Auch wenn ich dies hier nicht mache, könnten Sie, wenn Sie wollten, auf ähnliche Weise die Anzahl der Protokolleinträge und die Größe ermitteln, die von der Arbeit in das Protokoll geschrieben werden, indem Sie die Status vor und nach der Arbeit von fn_dblog abfragen Funktion.

Für unser obiges Beispiel ist das Folgende der Teil, den Sie in den Vorbereitungsabschnitt der Testvorlage einfügen müssen:

-- Preparation
SET NOCOUNT ON;
USE PerformanceV3;
 
ALTER DATABASE PerformanceV3 SET DELAYED_DURABILITY = Disabled;
 
DROP TABLE IF EXISTS dbo.T1;
 
CREATE TABLE dbo.T1(col1 INT NOT NULL);
 
DECLARE @db AS sysname = N'PerformanceV3';
 
DECLARE @logflushes AS INT, @starttime AS DATETIME2, @duration AS INT;

Und das Folgende ist der Teil, den Sie in den eigentlichen Arbeitsabschnitt einfügen müssen:

-- Actual work
DECLARE @i AS INT = 1;
 
WHILE @i <= 1000000
BEGIN
 
  BEGIN TRAN
    INSERT INTO dbo.T1(col1) VALUES(@i);
  COMMIT TRAN;
 
  SET @i += 1;
END;

Insgesamt erhalten Sie folgenden Code:

-- Example test with many small fully durable transactions in user database
-- ... Preparation goes here ...
 
-- Preparation
SET NOCOUNT ON;
USE PerformanceV3;
 
ALTER DATABASE PerformanceV3 SET DELAYED_DURABILITY = Disabled;
 
DROP TABLE IF EXISTS dbo.T1;
 
CREATE TABLE dbo.T1(col1 INT NOT NULL);
 
DECLARE @db AS sysname = N'PerformanceV3';
 
DECLARE @logflushes AS INT, @starttime AS DATETIME2, @duration AS INT;
 
-- Stats before
SET @logflushes = ( SELECT cntr_value FROM sys.dm_os_performance_counters
 
                    WHERE counter_name = 'Log Flushes/sec'
 
                      AND instance_name = @db );
 
SET @starttime = SYSDATETIME();
 
-- ... Actual work goes here ...
 
-- Actual work
DECLARE @i AS INT = 1;
 
WHILE @i <= 1000000
BEGIN
 
  BEGIN TRAN
    INSERT INTO dbo.T1(col1) VALUES(@i);
  COMMIT TRAN;
 
  SET @i += 1;
END;
 
-- Stats after
SET @duration = DATEDIFF(second, @starttime, SYSDATETIME());
 
SET @logflushes = ( SELECT cntr_value FROM sys.dm_os_performance_counters
                    WHERE counter_name = 'Log Flushes/sec'
                      AND instance_name = @db ) - @logflushes;
 
SELECT 
  @duration AS durationinseconds,
  @logflushes AS logflushes;

Dieser Code dauerte auf meinem System 193 Sekunden und löste 1.000.036 Log-Buffer-Flushes aus. Das ist sehr langsam, lässt sich aber durch die große Anzahl von Log-Flushes erklären.

Bei typischen OLTP-Workloads übermitteln verschiedene Sitzungen gleichzeitig kleine Änderungen in verschiedenen kleinen Transaktionen, sodass Sie nicht wirklich die Möglichkeit haben, viele kleine Änderungen in einer einzigen großen Transaktion zu kapseln. Wenn Ihre Situation jedoch so ist, dass alle kleinen Änderungen aus derselben Sitzung übermittelt werden, besteht eine einfache Möglichkeit zur Optimierung der Arbeit darin, sie in einer einzigen Transaktion zu kapseln. Dadurch erhalten Sie zwei Hauptvorteile. Einer davon ist, dass Ihre Arbeit weniger Protokolldatensätze schreiben wird. Bei 1.000.000 kleinen Transaktionen schreibt jede Transaktion tatsächlich drei Protokolldatensätze:einen für den Beginn der Transaktion, einen für die Änderung und einen für das Festschreiben der Transaktion. Sie betrachten also etwa 3.000.000 Transaktionsprotokolldatensätze im Vergleich zu etwas mehr als 1.000.000, wenn sie als eine große Transaktion ausgeführt werden. Aber noch wichtiger ist, dass bei einer großen Transaktion die meisten Log-Flushes nur ausgelöst werden, wenn der Log-Puffer voll ist, plus ein weiterer Log-Flush ganz am Ende der Transaktion, wenn sie festgeschrieben wird. Der Leistungsunterschied kann ziemlich groß sein. Um die Arbeit in einer großen Transaktion zu testen, verwenden Sie den folgenden Code im eigentlichen Arbeitsteil der Testvorlage:

-- Actual work
BEGIN TRAN;
 
DECLARE @i AS INT = 1;
 
WHILE @i <= 1000000
BEGIN
 
  INSERT INTO dbo.T1(col1) VALUES(@i);
  SET @i += 1;
 
END;
 
COMMIT TRAN;

Auf meinem System war diese Arbeit in 7 Sekunden abgeschlossen und löste 1.758 Log-Flushes aus. Hier ist ein Vergleich zwischen den beiden Optionen:

#transactions  log flushes  duration in seconds
-------------- ------------ --------------------
1000000        1000036      193
1              1758         7

Aber auch hier haben Sie bei typischen OLTP-Arbeitslasten nicht wirklich die Möglichkeit, viele kleine Transaktionen, die von verschiedenen Sitzungen gesendet werden, durch eine große Transaktion zu ersetzen, die von derselben Sitzung gesendet wird.

Voll dauerhafte versus verzögerte dauerhafte Transaktionen

Ab SQL Server 2014 können Sie ein Feature namens verzögerte Dauerhaftigkeit verwenden, mit dem Sie die Leistung von Workloads mit vielen kleinen Transaktionen verbessern können, selbst wenn sie von verschiedenen Sitzungen übermittelt werden, indem Sie die normale volle Dauerhaftigkeitsgarantie opfern. Beim Committen einer verzögerten dauerhaften Transaktion bestätigt SQL Server das Commit, sobald der Commit-Protokolldatensatz in den Protokollpuffer geschrieben wird, ohne dass eine Protokollpufferleerung ausgelöst wird. Der Protokollpuffer wird aufgrund einer der anderen oben genannten Bedingungen geleert, z. B. wenn er voll ist, aber nicht, wenn eine verzögerte dauerhafte Transaktion festgeschrieben wird.

Bevor Sie diese Funktion verwenden, müssen Sie sehr sorgfältig überlegen, ob sie für Sie geeignet ist. In Bezug auf die Leistung ist seine Auswirkung nur bei Workloads mit vielen kleinen Transaktionen signifikant. Wenn Ihre Arbeitslast zunächst hauptsächlich große Transaktionen umfasst, werden Sie wahrscheinlich keinen Leistungsvorteil feststellen. Noch wichtiger ist, dass Sie das Potenzial für Datenverlust erkennen müssen. Angenommen, die Anwendung schreibt eine verzögerte dauerhafte Transaktion fest. Ein Commit-Datensatz wird in den Protokollpuffer geschrieben und sofort bestätigt (die Kontrolle wird an den Aufrufer zurückgegeben). Wenn bei SQL Server ein Stromausfall auftritt, bevor der Protokollpuffer geleert wurde, macht der Wiederherstellungsprozess nach dem Neustart alle Änderungen rückgängig, die von der Transaktion vorgenommen wurden, obwohl die Anwendung davon ausgeht, dass sie festgeschrieben wurde.

Wann ist es in Ordnung, diese Funktion zu verwenden? Ein offensichtlicher Fall ist, wenn Datenverlust kein Problem darstellt, wie in diesem Beispiel von Melissa Connors von SentryOne. Ein anderer ist, wenn Sie nach einem Neustart die Mittel haben, um zu erkennen, welche Änderungen es nicht in die Datenbank geschafft haben, und Sie können sie reproduzieren. Wenn Ihre Situation nicht in eine dieser beiden Kategorien fällt, verwenden Sie diese Funktion trotz der Versuchung nicht.

Um mit verzögerten dauerhaften Transaktionen zu arbeiten, müssen Sie eine Datenbankoption namens DELAYED_DURABILITY festlegen. Diese Option kann auf einen von drei Werten gesetzt werden:

  • Deaktiviert (Standard):Alle Transaktionen in der Datenbank sind vollständig dauerhaft, und daher löst jeder Commit einen Log-Puffer-Flush aus
  • Erzwungen :Alle Transaktionen in der Datenbank werden dauerhaft verzögert, und daher lösen Commits keine Protokollpufferspülung aus
  • Erlaubt :Sofern nicht anders angegeben, sind Transaktionen vollständig dauerhaft und ihre Festschreibung löst eine Protokollpufferspülung aus; Wenn Sie jedoch die Option DELAYED_DURABILITY =ON entweder in einer COMMIT TRAN-Anweisung oder in einem atomaren Block (einer nativ kompilierten Prozedur) verwenden, wird diese bestimmte Transaktion dauerhaft verzögert, und daher löst ihr Commit keinen Log-Puffer-Flush aus

Verwenden Sie als Test den folgenden Code im Vorbereitungsabschnitt unserer Testvorlage (beachten Sie, dass die Datenbankoption auf Forced gesetzt ist):

-- Preparation
SET NOCOUNT ON;
USE PerformanceV3; -- http://tsql.solidq.com/SampleDatabases/PerformanceV3.zip
 
ALTER DATABASE PerformanceV3 SET DELAYED_DURABILITY = Forced;
 
DROP TABLE IF EXISTS dbo.T1;
 
CREATE TABLE dbo.T1(col1 INT NOT NULL);
 
DECLARE @db AS sysname = N'PerformanceV3';

Und verwenden Sie den folgenden Code im eigentlichen Arbeitsabschnitt (Hinweis, 1.000.000 kleine Transaktionen):

-- Actual work
DECLARE @i AS INT = 1;
 
WHILE @i <= 1000000
BEGIN
 
  BEGIN TRAN
    INSERT INTO dbo.T1(col1) VALUES(@i);
  COMMIT TRAN;
 
  SET @i += 1;
END;

Alternativ können Sie den zulässigen Modus auf Datenbankebene verwenden und dann im COMMIT TRAN-Befehl WITH (DELAYED_DURABILITY =ON) hinzufügen.

Auf meinem System dauerte die Arbeit 22 Sekunden und löste 95.407 Log-Flushes aus. Das ist länger, als die Arbeit als eine große Transaktion (7 Sekunden) auszuführen, da mehr Protokolldatensätze generiert werden (denken Sie daran, dass pro Transaktion einer für den Beginn der Transaktion, einer für die Änderung und einer für das Festschreiben der Transaktion vorhanden ist); Es ist jedoch viel schneller als die 193 Sekunden, die die Arbeit mit 1.000.000 vollständig dauerhaften Transaktionen dauerte, da die Anzahl der Protokolllöschungen von über 1.000.000 auf weniger als 100.000 gesunken ist. Außerdem würden Sie mit verzögerter Haltbarkeit den Leistungsgewinn auch dann erzielen, wenn die Transaktionen aus verschiedenen Sitzungen gesendet werden, in denen es keine Option ist, eine große Transaktion zu verwenden.

Um zu demonstrieren, dass die verzögerte Dauerhaftigkeit keinen Vorteil bringt, wenn die Arbeit als große Transaktionen ausgeführt wird, behalten Sie denselben Code im Vorbereitungsteil des letzten Tests bei und verwenden Sie den folgenden Code im eigentlichen Arbeitsteil:

-- Actual work
BEGIN TRAN;
 
DECLARE @i AS INT = 1;
 
WHILE @i <= 1000000
BEGIN
  INSERT INTO dbo.T1(col1) VALUES(@i);
 
  SET @i += 1;
END;
 
COMMIT TRAN;

Ich habe 8 Sekunden Laufzeit (im Vergleich zu 7 für eine große, vollständig dauerhafte Transaktion) und 1.759 Log-Flushes (im Vergleich zu 1.758). Die Zahlen sind im Wesentlichen gleich, aber bei der verzögerten dauerhaften Transaktion besteht das Risiko eines Datenverlusts.

Hier ist eine Zusammenfassung der Leistungszahlen für alle vier Tests:

durability          #transactions  log flushes  duration in seconds
------------------- -------------- ------------ --------------------
full                1000000        1000036      193
full                1              1758         7
delayed             1000000        95407        22
delayed             1              1759         8

Speicherklassenspeicher

Die verzögerte Haltbarkeitsfunktion kann die Leistung von Workloads im OLTP-Stil erheblich verbessern, die eine große Anzahl kleiner Aktualisierungstransaktionen umfassen, die eine hohe Frequenz und geringe Latenz erfordern. Das Problem ist, dass Sie Datenverlust riskieren. Was ist, wenn Sie keinen Datenverlust zulassen können, aber dennoch Leistungssteigerungen mit verzögerter Haltbarkeit wünschen, bei denen der Protokollpuffer nicht bei jedem Commit geleert wird, sondern wenn er voll ist? Wir alle essen den Kuchen gerne und haben ihn auch, oder?

Sie können dies in SQL Server 2016 SP1 oder höher erreichen, indem Sie Speicherklassenspeicher verwenden, auch bekannt als NVDIMM-N-nichtflüchtiger Speicher. Diese Hardware ist im Wesentlichen ein Speichermodul, das Ihnen Leistung auf Speicherniveau bietet, aber die dortigen Informationen bleiben erhalten und gehen daher nicht verloren, wenn die Stromversorgung unterbrochen wird. Mit dem Zusatz in SQL Server 2016 SP1 können Sie den Protokollpuffer auf dieser Hardware als dauerhaft konfigurieren. Dazu richten Sie den SCM als Volume in Windows ein und formatieren es als DAX-Volume (Direct Access Mode). Anschließend fügen Sie der Datenbank mit dem normalen Befehl ALTER DATABASE ADD LOG FILE eine Protokolldatei hinzu, wobei sich der Dateipfad auf dem DAX-Volume befindet, und legen Sie die Größe auf 20 MB fest. SQL Server wiederum erkennt, dass es sich um ein DAX-Volume handelt, und behandelt den Protokollpuffer von diesem Moment an als dauerhaft auf diesem Volume. Transaktions-Commit-Ereignisse lösen keine Protokollpuffer-Flushes mehr aus, sondern sobald der Commit im Protokollpuffer aufgezeichnet wurde, weiß SQL Server, dass er tatsächlich beibehalten wird, und gibt daher die Kontrolle an den Aufrufer zurück. Wenn der Protokollpuffer voll ist, schreibt SQL Server ihn in die Transaktionsprotokolldateien auf dem herkömmlichen Speicher.

Weitere Einzelheiten zu dieser Funktion, einschließlich Leistungszahlen, finden Sie unter Latenzbeschleunigung bei Transaktionscommits mit Speicherklassenspeicher in Windows Server 2016/SQL Server 2016 SP1 von Kevin Farlee.

Seltsamerweise verbessert SQL Server 2019 die Unterstützung für Speicherklassenspeicher über das Szenario des persistenten Protokollcaches hinaus. Es unterstützt das Platzieren von Datendateien, Protokolldateien und In-Memory-OLTP-Prüfpunktdateien auf solcher Hardware. Alles, was Sie tun müssen, ist es als Volume auf Betriebssystemebene verfügbar zu machen und als DAX-Laufwerk zu formatieren. SQL Server 2019 erkennt diese Technologie automatisch und arbeitet erleuchtet Modus, der direkt auf das Gerät zugreift und den Speicherstapel des Betriebssystems umgeht. Willkommen in der Zukunft!

Benutzerdatenbank im Vergleich zu tempdb

Die tempdb-Datenbank wird natürlich bei jedem Neustart von SQL Server als neue Kopie der model-Datenbank von Grund auf neu erstellt. Daher besteht keine Notwendigkeit, Daten, die Sie in tempdb schreiben, wiederherzustellen, unabhängig davon, ob Sie sie in temporäre Tabellen, Tabellenvariablen oder Benutzertabellen schreiben. Nach Neustart ist alles weg. Mit diesem Wissen kann SQL Server viele der protokollierungsbezogenen Anforderungen lockern. Beispielsweise lösen Commit-Ereignisse unabhängig davon, ob Sie die Option für verzögerte Dauerhaftigkeit aktivieren oder nicht, keine Protokollpufferlöschung aus. Darüber hinaus wird die Menge der zu protokollierenden Informationen reduziert, da SQL Server nur genügend Informationen benötigt, um bei Bedarf das Zurücksetzen von Transaktionen oder das Rückgängigmachen von Arbeiten zu unterstützen, nicht jedoch das Fortsetzen von Transaktionen oder das Wiederholen von Arbeiten. Infolgedessen sind Transaktionsprotokolldatensätze, die Änderungen an einem Objekt in tempdb darstellen, tendenziell kleiner als wenn dieselbe Änderung auf ein Objekt in einer Benutzerdatenbank angewendet wird.

Um dies zu demonstrieren, führen Sie die gleichen Tests aus, die Sie zuvor in PerformanceV3 ausgeführt haben, nur diesmal in tempdb. Wir beginnen mit dem Test vieler kleiner Transaktionen, wenn die Datenbankoption DELAYED_DURABILITY auf Disabled (Standard) gesetzt ist. Verwenden Sie den folgenden Code im Vorbereitungsabschnitt der Testvorlage:

-- Preparation
SET NOCOUNT ON;
USE tempdb;
 
ALTER DATABASE tempdb SET DELAYED_DURABILITY = Disabled;
 
DROP TABLE IF EXISTS dbo.T1;
 
CREATE TABLE dbo.T1(col1 INT NOT NULL);
 
DECLARE @db AS sysname = N'tempdb';

Verwenden Sie den folgenden Code im eigentlichen Arbeitsabschnitt:

-- Actual work
DECLARE @i AS INT = 1;
 
WHILE @i <= 1000000
BEGIN
 
  BEGIN TRAN
    INSERT INTO dbo.T1(col1) VALUES(@i);
  COMMIT TRAN;
 
  SET @i += 1;
END;

Diese Arbeit generierte 5.095 Log-Flushes und dauerte 19 Sekunden. Dem stehen über eine Million Log-Flushes und 193 Sekunden in einer Benutzerdatenbank mit voller Haltbarkeit gegenüber. Das ist aufgrund der geringeren Größe der Protokolldatensätze sogar besser als bei verzögerter Dauerhaftigkeit in einer Benutzerdatenbank (95.407 Log-Flushes und 22 Sekunden).

Um eine große Transaktion zu testen, lassen Sie den Abschnitt „preparation“ unverändert und verwenden Sie den folgenden Code im Abschnitt „actual work“:

-- Actual work
BEGIN TRAN;
 
DECLARE @i AS INT = 1;
 
WHILE @i <= 1000000
BEGIN
  INSERT INTO dbo.T1(col1) VALUES(@i);
 
  SET @i += 1;
END;
 
COMMIT TRAN;

Ich habe 1.228 Log-Flushes und 9 Sekunden Laufzeit. Dem stehen 1.758 Log-Flushes und 7 Sekunden Laufzeit in der Benutzerdatenbank gegenüber. Die Laufzeit ist ähnlich, sogar etwas schneller in der Benutzerdatenbank, aber es könnte kleine Abweichungen zwischen den Tests geben. Die Größe der Protokolldatensätze in tempdb wird reduziert, und daher erhalten Sie im Vergleich zur Benutzerdatenbank weniger Protokolllöschungen.

Sie können auch versuchen, die Tests mit der auf Forced gesetzten Option DELAYED_DURABILITY auszuführen, aber dies hat keine Auswirkungen auf tempdb, da, wie bereits erwähnt, Commit-Ereignisse sowieso keinen Log-Flush in tempdb auslösen.

Hier sind die Leistungsmessungen für alle Tests, sowohl in der Benutzerdatenbank als auch in tempdb:

database       durability          #transactions  log flushes  duration in seconds
-------------- ------------------- -------------- ------------ --------------------
PerformanceV3  full                1000000        1000036      193
PerformanceV3  full                1              1758         7
PerformanceV3  delayed             1000000        95407        22
PerformanceV3  delayed             1              1759         8
tempdb         full                1000000        5095         19
tempdb         full                1              1228         9
tempdb         delayed             1000000        5091         18
tempdb         delayed             1              1226         9

Sequenzobjekt-Caching

Ein vielleicht überraschender Fall, der das Leeren des Protokollpuffers auslöst, hängt mit der Sequenzobjekt-Cache-Option zusammen. Betrachten Sie als Beispiel die folgende Sequenzdefinition:

CREATE SEQUENCE dbo.Seq1 AS BIGINT MINVALUE 1 CACHE 50; -- the default cache size is 50;

Jedes Mal, wenn Sie einen neuen Sequenzwert benötigen, verwenden Sie die Funktion NÄCHSTER WERT FÜR, etwa so:

SELECT NEXT VALUE FOR dbo.Seq1;

Die CACHE-Eigenschaft ist ein Leistungsmerkmal. Ohne sie hätte SQL Server jedes Mal, wenn ein neuer Sequenzwert angefordert wurde, den aktuellen Wert zu Wiederherstellungszwecken auf die Festplatte schreiben müssen. In der Tat ist dies das Verhalten, das Sie erhalten, wenn Sie den NO CACHE-Modus verwenden. Wenn die Option auf einen Wert größer als null festgelegt ist, schreibt SQL Server stattdessen nur einmal pro Cachegröße von Anforderungen einen Wiederherstellungswert auf den Datenträger. SQL Server verwaltet zwei Elemente im Arbeitsspeicher, deren Größe dem Sequenztyp entspricht, eines mit dem aktuellen Wert und eines mit der Anzahl der verbleibenden Werte, bevor der nächste Datenträgerschreibvorgang des Wiederherstellungswerts erforderlich ist. Im Falle eines Stromausfalls setzt SQL Server beim Neustart den aktuellen Sequenzwert auf den Wiederherstellungswert.

An einem Beispiel lässt sich das wahrscheinlich viel einfacher erklären. Betrachten Sie die obige Sequenzdefinition mit der CACHE-Option auf 50 (Standard). Sie fordern zum ersten Mal einen neuen Sequenzwert an, indem Sie die obige SELECT-Anweisung ausführen. SQL Server legt die oben genannten Mitglieder auf die folgenden Werte fest:

On disk recovery value: 50, In-memory current value: 1, In-memory values left: 49, You get: 1

49 weitere Anfragen werden die Festplatte nicht berühren, sondern nur die Speicherelemente aktualisieren. Nach insgesamt 50 Anfragen werden die Mitglieder auf die folgenden Werte gesetzt:

On disk recovery value: 50, In-memory current value: 50, In-memory values left: 0, You get: 50

Fordern Sie erneut einen neuen Sequenzwert an, und dies löst einen Datenträgerschreibvorgang mit dem Wiederherstellungswert 100 aus. Die Mitglieder werden dann auf die folgenden Werte gesetzt:

On disk recovery value: 100, In-memory current value: 51, In-memory values left: 49, You get: 51

Wenn das System zu diesem Zeitpunkt einen Stromausfall erleidet, wird der aktuelle Sequenzwert nach dem Neustart auf 100 gesetzt (der von der Festplatte wiederhergestellte Wert). Die nächste Anforderung für einen Sequenzwert erzeugt 101 (Schreiben des Wiederherstellungswerts 150 auf die Platte). Sie haben alle Werte im Bereich von 52 bis 100 verloren. Durch ein unsauberes Beenden des SQL-Server-Prozesses, etwa bei einem Stromausfall, können höchstens so viele Werte wie die Cache-Größe verloren gehen. Der Kompromiss ist klar; Je größer die Cache-Größe, desto weniger Festplattenschreibvorgänge des Wiederherstellungswerts und desto besser die Leistung. Gleichzeitig ist die Lücke, die bei einem Stromausfall zwischen zwei Sequenzwerten erzeugt werden kann, umso größer.

All dies ist ziemlich einfach, und vielleicht sind Sie sehr gut damit vertraut, wie es funktioniert. Was überraschen könnte, ist, dass jedes Mal, wenn SQL Server einen neuen Wiederherstellungswert auf die Festplatte schreibt (in unserem Beispiel alle 50 Anforderungen), auch der Protokollpuffer gehärtet wird. Das ist bei der Identitätsspalteneigenschaft nicht der Fall, obwohl SQL Server intern die gleiche Caching-Funktion für die Identität verwendet wie für das Sequenzobjekt, aber es lässt Sie einfach nicht seine Größe steuern. Es ist standardmäßig mit der Größe 10000 für BIGINT und NUMERIC, 1000 für INT, 100 für SMALLINT und 10 für TINYINT aktiviert. Wenn Sie möchten, können Sie es mit dem Trace-Flag 272 oder der bereichsbezogenen Konfigurationsoption IDENTITY_CACHE (2017+) deaktivieren. Der Grund, warum SQL Server den Protokollpuffer beim Schreiben des identitätscachebezogenen Wiederherstellungswerts auf die Festplatte nicht leeren muss, liegt darin, dass ein neuer Identitätswert nur erstellt werden kann, wenn eine Zeile in eine Tabelle eingefügt wird. Im Falle eines Stromausfalls wird eine Zeile, die durch eine Transaktion, die nicht festgeschrieben wurde, in eine Tabelle eingefügt wurde, als Teil des Datenbankwiederherstellungsprozesses aus der Tabelle gezogen, wenn das System neu gestartet wird. Selbst wenn SQL Server nach dem Neustart denselben Identitätswert generiert wie den, der in der Transaktion erstellt wurde, die nicht festgeschrieben wurde, gibt es keine Chance für Duplikate, da die Zeile aus der Tabelle gezogen wurde. Hätte die Transaktion festgeschrieben, hätte dies eine Protokolllöschung ausgelöst, die auch das Schreiben eines Cache-bezogenen Wiederherstellungswerts beibehalten würde. Daher sah sich Microsoft nicht gezwungen, den Protokollpuffer jedes Mal zu leeren, wenn ein identitätscachebezogener Datenträgerschreibvorgang des Wiederherstellungswerts stattfindet.

Beim Sequenzobjekt ist die Situation anders. Eine Anwendung kann einen neuen Sequenzwert anfordern und ihn nicht in der Datenbank speichern. Im Falle eines Stromausfalls nach der Erstellung eines neuen Sequenzwerts in einer Transaktion, die nicht festgeschrieben wurde, gibt es nach dem Neustart keine Möglichkeit für SQL Server, der Anwendung mitzuteilen, dass sie sich nicht auf diesen Wert verlassen soll. Um zu vermeiden, dass nach dem Neustart ein neuer Sequenzwert erstellt wird, der einem zuvor generierten Sequenzwert entspricht, erzwingt SQL Server daher jedes Mal eine Protokollleerung, wenn ein neuer sequenzcachebezogener Wiederherstellungswert auf den Datenträger geschrieben wird. Eine Ausnahme von dieser Regel ist, wenn das Sequenzobjekt in tempdb erstellt wird, sind solche Log-Flushes natürlich nicht erforderlich, da tempdb sowieso nach einem Systemneustart neu erstellt wird.

Eine negative Leistungsauswirkung der häufigen Log-Flushes macht sich besonders bemerkbar, wenn eine sehr kleine Sequenz-Cache-Größe verwendet wird und in einer Transaktion viele Sequenzwerte generiert werden, z. B. wenn viele Zeilen in eine Tabelle eingefügt werden. Ohne die Sequenz würde eine solche Transaktion den Protokollpuffer meistens härten, wenn er voll ist, plus ein weiteres Mal, wenn die Transaktion festgeschrieben wird. Aber mit der Sequenz erhalten Sie jedes Mal eine Protokolllöschung, wenn ein Wiederherstellungswert auf die Festplatte geschrieben wird. Aus diesem Grund sollten Sie vermeiden, eine kleine Cache-Größe zu verwenden – ganz zu schweigen vom NO CACHE-Modus.

Um dies zu demonstrieren, verwenden Sie den folgenden Code im Vorbereitungsabschnitt unserer Testvorlage:

-- Preparation
SET NOCOUNT ON;
USE PerformanceV3; -- try PerformanceV3, tempdb
 
ALTER DATABASE PerformanceV3         -- try PerformanceV3, tempdb
  SET DELAYED_DURABILITY = Disabled; -- try Disabled, Forced
 
DROP TABLE IF EXISTS dbo.T1;
 
DROP SEQUENCE IF EXISTS dbo.Seq1;
 
CREATE SEQUENCE dbo.Seq1 AS BIGINT MINVALUE 1 CACHE 50; -- try NO CACHE, CACHE 50, CACHE 10000
 
DECLARE @db AS sysname = N'PerformanceV3'; -- try PerformanceV3, tempdb

Und der folgende Code im eigentlichen Arbeitsabschnitt:

-- Actual work
SELECT
  -- n -- to test without seq
  NEXT VALUE FOR dbo.Seq1 AS n -- to test sequence
INTO dbo.T1
FROM PerformanceV3.dbo.GetNums(1, 1000000) AS N;

Dieser Code verwendet eine Transaktion, um 1.000.000 Zeilen mit der Anweisung SELECT INTO in eine Tabelle zu schreiben, wobei so viele Sequenzwerte wie die Anzahl der eingefügten Zeilen generiert werden.

Wie in den Kommentaren angewiesen, führen Sie den Test mit NO CACHE, CACHE 50 und CACHE 10000 aus, sowohl in PerformanceV3 als auch in tempdb, und probieren Sie sowohl vollständig dauerhafte als auch verzögert dauerhafte Transaktionen aus.

Hier sind die Leistungszahlen, die ich auf meinem System erhalten habe:

database       durability          cache     log flushes  duration in seconds
-------------- ------------------- --------- ------------ --------------------
PerformanceV3  full                NO CACHE  1000047      171
PerformanceV3  full                50        20008        4
PerformanceV3  full                10000     339          < 1
tempdb         full                NO CACHE  96           4
tempdb         full                50        74           1
tempdb         full                10000     8            < 1
PerformanceV3  delayed             NO CACHE  1000045      166
PerformanceV3  delayed             50        20011        4
PerformanceV3  delayed             10000     334          < 1
tempdb         delayed             NO CACHE  91           4
tempdb         delayed             50        74           1
tempdb         delayed             10000     8            < 1

Es gibt einige interessante Dinge zu beachten.

Mit NO CACHE erhalten Sie einen Log-Flush für jeden einzelnen generierten Sequenzwert. Daher wird dringend empfohlen, dies zu vermeiden.

Bei einer kleinen Sequenz-Cache-Größe erhalten Sie immer noch viele Log-Flushes. Vielleicht ist die Situation nicht so schlimm wie bei NO CACHE, aber beachten Sie, dass die Workload mit der Standard-Cache-Größe von 50 4 Sekunden dauerte, verglichen mit weniger als einer Sekunde mit der Größe 10.000. Ich persönlich verwende 10.000 als meinen bevorzugten Wert.

In tempdb you don’t get log flushes when a sequence cache-related recovery value is written to disk, but the recovery value is still written to disk every cache-sized number of requests. That’s perhaps surprising since such a value would never need to be recovered. Therefore, even when using a sequence object in tempdb, I’d still recommend using a large cache size.

Also notice that delayed durability doesn’t prevent the need for log flushes every time the sequence cache-related recovery value is written to disk.

Schlussfolgerung

This article focused on log buffer flushes. Understanding this aspect of SQL Server’s logging architecture is important especially in order to be able to optimize OLTP-style workloads that require high frequency and low latency. Workloads using In-Memory OLTP included, of course. You have more options with newer features like delayed durability and persisted log buffer with storage class memory. Make sure you’re very careful with the former, though, since it does incur potential for data loss unlike the latter.

Be careful not to use the sequence object with a small cache size, not to speak of the NO CACHE mode. I find the default size 50 too small and prefer to use 10,000. I’ve heard people expressing concerns that with a cache size 10000, after multiple power failures they might lose all the values in the type. However, even with a four-byte INT type, using only the positive range, 10,000 fits 214,748 times. If your system experience that many power failures, you have a completely different problem to worry about. Therefore, I feel very comfortable with a cache size of 10,000.