sql >> Database >  >> RDS >> Database

Logboekbufferspoelingen begrijpen

U hebt waarschijnlijk al vaak gehoord dat SQL Server een garantie biedt voor ACID-transactie-eigenschappen. In dit artikel staat het D-deel centraal, wat natuurlijk staat voor duurzaamheid. Meer specifiek richt dit artikel zich op een aspect van de SQL Server-logboekarchitectuur dat de duurzaamheid van transacties afdwingt:het leegmaken van logboekbuffers. Ik heb het over de functie die de logbuffer dient, de voorwaarden die SQL Server dwingen om de logbuffer naar schijf te spoelen, wat u kunt doen om de transactieprestaties te optimaliseren, evenals recent toegevoegde gerelateerde technologieën zoals vertraagde duurzaamheid en niet-vluchtig opslagklassegeheugen.

Logbuffer spoelt

Het D-deel in de ACID transactie-eigenschappen staat voor duurzaamheid. Op logisch niveau betekent dit dat wanneer een toepassing SQL Server een instructie stuurt om een ​​transactie door te voeren (expliciet of met een automatische transactie), SQL Server normaal gesproken de besturing alleen teruggeeft aan de beller nadat deze kan garanderen dat de transactie duurzaam is. Met andere woorden, zodra de beller de controle terug heeft gekregen na het plegen van een transactie, kan hij erop vertrouwen dat zelfs als de server een moment later een stroomstoring ervaart, de transactiewijzigingen in de database zijn terechtgekomen. Zolang de server succesvol herstart en de databasebestanden niet beschadigd zijn, zult u zien dat alle transactiewijzigingen zijn toegepast.

De manier waarop SQL Server de duurzaamheid van transacties afdwingt, is gedeeltelijk door ervoor te zorgen dat alle wijzigingen van de transactie worden weggeschreven naar het transactielogboek van de database op schijf voordat u de controle aan de beller teruggeeft. In het geval van een stroomstoring nadat de vastlegging van een transactie was bevestigd, weet u dat al die wijzigingen op zijn minst naar het transactielogboek op de schijf zijn geschreven. Dat is zelfs het geval als de gerelateerde gegevenspagina's alleen in de datacache (de bufferpool) zijn gewijzigd, maar nog niet naar de gegevensbestanden op schijf zijn gewist. Wanneer u SQL Server herstart, tijdens de herhalingsfase van het herstelproces, gebruikt SQL Server de informatie die in het logboek is vastgelegd om wijzigingen af ​​te spelen die zijn aangebracht na het laatste controlepunt en die de gegevensbestanden niet hebben gehaald. Er is iets meer aan het verhaal, afhankelijk van het herstelmodel dat u gebruikt en of bulkbewerkingen zijn toegepast na het laatste controlepunt, maar voor de doeleinden van onze discussie volstaat het om ons te concentreren op het deel dat betrekking heeft op het verharden van de wijzigingen in de transactielogboek.

Het lastige van de logboekarchitectuur van SQL Server is dat de logboekregistraties sequentieel zijn. Als SQL Server niet een soort logbuffer had gebruikt om het schrijven van logs naar schijf te verminderen, zouden schrijfintensieve systemen - vooral systemen die veel kleine transacties met zich meebrengen - al snel tegen vreselijke log-write-gerelateerde prestatieknelpunten aanlopen.

Om de negatieve invloed op de prestaties van frequente opeenvolgende logboekschrijfacties naar schijf te verminderen, gebruikt SQL Server een logboekbuffer in het geheugen. Het schrijven van logbestanden wordt eerst naar de logbuffer gedaan en bepaalde omstandigheden zorgen ervoor dat SQL Server de logbuffer naar schijf leegt of hard maakt. De geharde eenheid (ook wel logboekblok genoemd) kan variëren van een minimale sectorgrootte (512 bytes) tot maximaal 60 KB. Hieronder volgen voorwaarden die een logbufferspoeling activeren (negeer de delen die nu tussen vierkante haken staan):

  • SQL Server krijgt een commit-verzoek van een [volledig duurzame] transactie die gegevens wijzigt [in een andere database dan tempdb]
  • De logbuffer raakt vol en bereikt de capaciteit van 60 KB
  • SQL Server moet pagina's met vuile gegevens hard maken, bijvoorbeeld tijdens een controlepuntproces, en de logrecords die de wijzigingen aan die pagina's vertegenwoordigen, waren nog niet gehard (logboekregistratie voor vooruitschrijven , of kortweg WAL)
  • U vraagt ​​handmatig een logbufferspoeling aan door de procedure sys.sp_flush_log uit te voeren
  • SQL Server schrijft een nieuwe reeks-cache-gerelateerde herstelwaarde [in een andere database dan tempdb]

De eerste vier voorwaarden zouden vrij duidelijk moeten zijn, als u de informatie tussen vierkante haken voorlopig negeert. De laatste is misschien nog niet duidelijk, maar ik zal het later in het artikel in detail uitleggen.

De tijd dat SQL Server wacht op een I/O-bewerking die een logboekbufferspoeling verwerkt om te voltooien, wordt weergegeven door het wachttype WRITELOG.

Dus, waarom is deze informatie zo interessant, en wat doen we ermee? Als u de voorwaarden begrijpt die het leegmaken van logboekbuffers veroorzaken, kunt u erachter komen waarom bepaalde workloads gerelateerde knelpunten ervaren. Ook zijn er in sommige gevallen acties die u kunt ondernemen om dergelijke knelpunten te verminderen of weg te nemen. Ik zal een aantal voorbeelden behandelen, zoals één grote transactie versus veel kleine transacties, volledig duurzame versus vertraagde duurzame transacties, gebruikersdatabase versus tempdb en sequentieobjectcaching.

Eén grote transactie versus veel kleine transacties

Zoals vermeld, is een van de voorwaarden die een logbufferspoeling activeert, wanneer u een transactie uitvoert om de duurzaamheid van de transactie te garanderen. Dit betekent dat workloads die veel kleine transacties met zich meebrengen, zoals OLTP-workloads, mogelijk te maken krijgen met log-write-gerelateerde bottlenecks.

Hoewel dit vaak niet het geval is, is het een eenvoudige en effectieve manier om het werk te optimaliseren als u in één sessie veel kleine wijzigingen indient, door de wijzigingen in één grote transactie toe te passen in plaats van meerdere kleine.

Bekijk het volgende vereenvoudigde voorbeeld (download PerformanceV3 hier):

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;

Deze code voert 1.000.000 kleine transacties uit die gegevens in een gebruikersdatabase wijzigen. Dit werk zal ten minste 1.000.000 logboekbufferspoelingen activeren. U kunt er nog een paar krijgen omdat de logbuffer vol raakt. U kunt de volgende testsjabloon gebruiken om het aantal logbufferspoelingen te tellen en de tijd te meten die nodig was om het werk te voltooien:

-- 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;

Hoewel de naam van de prestatieteller Log Flushes/sec is, blijft het tot dusver het aantal logbufferspoelingen accumuleren. De code trekt dus de telling vóór het werk af van de telling na het werk om het aantal logspoelingen te berekenen die door het werk zijn gegenereerd. Deze code meet ook de tijd in seconden die nodig was om het werk te voltooien. Hoewel ik dit hier niet doe, zou je, als je dat zou willen, op dezelfde manier het aantal logboekrecords en de grootte kunnen berekenen die door het werk naar het logboek zijn geschreven door de statussen voor en na het werk van de fn_dblog op te vragen functie.

Voor ons voorbeeld hierboven is het volgende het onderdeel dat u in het voorbereidingsgedeelte van de testsjabloon moet plaatsen:

-- 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;

En het volgende is het deel dat u in de sectie met het eigenlijke werk moet plaatsen:

-- 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;

In totaal krijg je de volgende 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;

Deze code duurde 193 seconden om op mijn systeem te voltooien en veroorzaakte 1.000.036 logbufferspoelingen. Dat is erg traag, maar kan worden verklaard door het grote aantal log flushes.

In typische OLTP-workloads dienen verschillende sessies tegelijkertijd kleine wijzigingen in verschillende kleine transacties in, dus het is niet zo dat u echt de mogelijkheid heeft om veel kleine wijzigingen in een enkele grote transactie in te kapselen. Als uw situatie echter is dat alle kleine wijzigingen vanuit dezelfde sessie worden ingediend, is een eenvoudige manier om het werk te optimaliseren, het in één enkele transactie in te kapselen. Dit levert u twee belangrijke voordelen op. Een daarvan is dat uw werk minder logrecords zal schrijven. Met 1.000.000 kleine transacties schrijft elke transactie in feite drie logrecords:één voor het starten van de transactie, één voor de wijziging en één voor het vastleggen van de transactie. U kijkt dus naar ongeveer 3.000.000 transactielogboekrecords versus iets meer dan 1.000.000 wanneer uitgevoerd als één grote transactie. Maar wat nog belangrijker is, met één grote transactie worden de meeste log-flushes alleen geactiveerd wanneer de logbuffer vol raakt, plus nog een log-flush helemaal aan het einde van de transactie wanneer deze wordt vastgelegd. Het prestatieverschil kan behoorlijk groot zijn. Om het werk in één grote transactie te testen, gebruikt u de volgende code in het eigenlijke werkgedeelte van de testsjabloon:

-- 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;

Op mijn systeem voltooide dit werk in 7 seconden en veroorzaakte het 1.758 log-flushes. Hier is een vergelijking tussen de twee opties:

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

Maar nogmaals, in typische OLTP-workloads heb je niet echt de mogelijkheid om veel kleine transacties die vanuit verschillende sessies zijn verzonden, te vervangen door één grote transactie die vanuit dezelfde sessie wordt verzonden.

Volledig duurzame versus vertraagde duurzame transacties

Vanaf SQL Server 2014 kunt u een functie genaamd vertraagde duurzaamheid gebruiken waarmee u de prestaties van workloads met veel kleine transacties kunt verbeteren, zelfs als deze door verschillende sessies zijn ingediend, door de normale volledige duurzaamheidsgarantie op te offeren. Bij het uitvoeren van een vertraagde duurzame transactie bevestigt SQL Server de vastlegging zodra het vastleggingslogboekrecord naar de logboekbuffer wordt geschreven, zonder dat een logboekbufferspoeling wordt geactiveerd. De logbuffer wordt leeggemaakt vanwege een van de andere bovengenoemde omstandigheden, zoals wanneer deze vol raakt, maar niet wanneer een vertraagde duurzame transactie wordt uitgevoerd.

Voordat u deze functie gebruikt, moet u goed nadenken of deze geschikt voor u is. In termen van prestaties is de impact alleen significant bij workloads met veel kleine transacties. Als uw werklast om te beginnen voornamelijk grote transacties omvat, zult u waarschijnlijk geen prestatievoordeel zien. Wat nog belangrijker is, u moet het potentieel voor gegevensverlies realiseren. Stel dat de toepassing een vertraagde duurzame transactie uitvoert. Een vastleggingsrecord wordt naar de logbuffer geschreven en onmiddellijk bevestigd (controle teruggegeven aan de beller). Als SQL Server een stroomstoring ondervindt voordat de logbuffer is leeggemaakt, maakt het herstelproces na het opnieuw opstarten alle wijzigingen ongedaan die door de transactie zijn aangebracht, ook al denkt de toepassing dat deze is doorgevoerd.

Dus, wanneer is het OK om deze functie te gebruiken? Een voor de hand liggend geval is wanneer gegevensverlies geen probleem is, zoals dit voorbeeld van Melissa Connors van SentryOne. Een andere is wanneer je na een herstart de middelen hebt om te identificeren welke wijzigingen de database niet hebben gehaald, en je kunt ze reproduceren. Als uw situatie niet in een van deze twee categorieën valt, gebruik deze functie dan ondanks de verleiding niet.

Om met vertraagde duurzame transacties te werken, moet u een databaseoptie instellen met de naam DELAYED_DURABILITY. Deze optie kan worden ingesteld op een van de volgende drie waarden:

  • Uitgeschakeld (standaard):alle transacties in de database zijn volledig duurzaam, en daarom activeert elke commit een logbufferspoeling
  • Gedwongen :alle transacties in de database worden duurzaam vertraagd, en daarom activeren commits geen logbufferspoeling
  • Toegestaan :tenzij anders vermeld, zijn transacties volledig duurzaam en het plegen ervan activeert een logbufferspoeling; als u echter de optie DELAYED_DURABILITY =ON gebruikt in ofwel een COMMIT TRAN-instructie of een atomair blok (van een native gecompileerde proc), wordt die specifieke transactie duurzaam vertraagd en daarom wordt door het committen geen logbuffer-flush geactiveerd

Gebruik als test de volgende code in de voorbereidingssectie van onze testsjabloon (merk op dat de database-optie is ingesteld op Geforceerd):

-- 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';

En gebruik de volgende code in het gedeelte over het eigenlijke werk (let op, 1.000.000 kleine transacties):

-- 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;

Als alternatief kunt u de modus Toegestaan ​​op databaseniveau gebruiken en vervolgens in het COMMIT TRAN-commando WITH (DELAYED_DURABILITY =ON) toevoegen.

Op mijn systeem duurde het werk 22 seconden om te voltooien en veroorzaakte het 95.407 log flushes. Dat is langer dan het werk als één grote transactie uitvoeren (7 seconden), aangezien er meer logrecords worden gegenereerd (onthoud, per transactie, één voor het starten van de transactie, één voor de wijziging en één voor het vastleggen van de transactie); het is echter veel sneller dan de 193 seconden die nodig waren om het werk te voltooien met behulp van 1.000.000 volledig duurzame transacties, aangezien het aantal logspoelingen daalde van meer dan 1.000.000 tot minder dan 100.000. Bovendien zou je met vertraagde duurzaamheid de prestatiewinst behalen, zelfs als de transacties worden ingediend vanuit verschillende sessies waarbij het geen optie is om één grote transactie te gebruiken.

Om aan te tonen dat het geen voordeel heeft om vertraagde duurzaamheid te gebruiken bij het uitvoeren van het werk als grote transacties, houdt u dezelfde code in het voorbereidingsgedeelte van de laatste test en gebruikt u de volgende code in het eigenlijke werkgedeelte:

-- 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;

Ik kreeg 8 seconden runtime (vergeleken met 7 voor één grote, volledig duurzame transactie) en 1.759 log flushes (vergeleken met 1.758). De cijfers zijn in wezen hetzelfde, maar met de vertraagde duurzame transactie heb je wel het risico van gegevensverlies.

Hier is een samenvatting van de prestatiecijfers voor alle vier de tests:

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

Opslagklasse geheugen

De functie voor vertraagde duurzaamheid kan de prestaties aanzienlijk verbeteren van OLTP-achtige workloads die een groot aantal kleine updatetransacties met zich meebrengen die een hoge frequentie en lage latentie vereisen. Het probleem is dat u gegevensverlies riskeert. Wat als u geen gegevensverlies kunt toestaan, maar toch prestatiewinst wilt die vergelijkbaar is met vertraagde duurzaamheid, waarbij de logbuffer niet voor elke commit wordt leeggemaakt, maar wanneer deze vol raakt? We houden er allemaal van om de taart op te eten en te hebben, toch?

U kunt dit bereiken in SQL Server 2016 SP1 of hoger door gebruik te maken van opslagklassegeheugen, ook bekend als NVDIMM-N niet-vluchtige opslag. Deze hardware is in wezen een geheugenmodule die u prestaties op geheugenniveau geeft, maar de informatie daar blijft behouden en gaat daarom niet verloren als de stroom uitvalt. Met de toevoeging in SQL Server 2016 SP1 kunt u de logbuffer configureren als een permanente buffer op dergelijke hardware. Hiervoor stelt u de SCM in als een volume in Windows en formatteert u deze als een Direct Access Mode (DAX)-volume. Vervolgens voegt u een logbestand toe aan de database met de normale opdracht ALTER DATABASE ADD LOG FILE, waarbij het bestandspad zich op het DAX-volume bevindt, en stelt u de grootte in op 20 MB. SQL Server herkent op zijn beurt dat het een DAX-volume is en behandelt vanaf dat moment de logbuffer als een persistente op dat volume. Transactie-commit-gebeurtenissen activeren geen logbuffer-flushes meer, maar zodra de commit is vastgelegd in de logbuffer, weet SQL Server dat deze daadwerkelijk is blijven bestaan, en geeft daarom de controle terug aan de beller. Wanneer de logbuffer vol raakt, spoelt SQL Server deze door naar de transactielogbestanden op de traditionele opslag.

Zie voor meer informatie over deze functie, inclusief prestatiecijfers, Transactie Commit-latentieversnelling met Storage Class Memory in Windows Server 2016/SQL Server 2016 SP1 door Kevin Farlee.

Vreemd genoeg verbetert SQL Server 2019 de ondersteuning voor geheugen van opslagklassen die verder gaat dan alleen het persistente logcachescenario. Het ondersteunt het plaatsen van gegevensbestanden, logbestanden en In-Memory OLTP-controlepuntbestanden op dergelijke hardware. Het enige dat u hoeft te doen, is het als een volume op OS-niveau bloot te stellen en als een DAX-station te formatteren. SQL Server 2019 herkent deze technologie automatisch en werkt in een verlichte modus, directe toegang tot het apparaat, het omzeilen van de opslagstack van het besturingssysteem. Welkom in de toekomst!

Gebruikersdatabase versus tempdb

De tempdb-database wordt natuurlijk helemaal opnieuw gemaakt als een nieuwe kopie van de modeldatabase elke keer dat u SQL Server opnieuw start. Als zodanig is het nooit nodig om gegevens te herstellen die u naar tempdb schrijft, of u deze nu naar tijdelijke tabellen, tabelvariabelen of gebruikerstabellen schrijft. Het is allemaal weg na opnieuw opstarten. Dit wetende, kan SQL Server veel van de logboekgerelateerde vereisten versoepelen. Ongeacht of u bijvoorbeeld de optie voor vertraagde duurzaamheid inschakelt of niet, commit-gebeurtenissen activeren geen logbuffer-flush. Bovendien is de hoeveelheid informatie die moet worden vastgelegd, verminderd, aangezien SQL Server alleen voldoende informatie nodig heeft om transacties terug te draaien of werk ongedaan te maken, indien nodig, maar niet om transacties vooruit te rollen of werk opnieuw uit te voeren. Als gevolg hiervan zijn transactielogboekrecords die wijzigingen aan een object in tempdb vertegenwoordigen, meestal kleiner in vergelijking met wanneer dezelfde wijziging wordt toegepast op een object in een gebruikersdatabase.

Om dit te demonstreren, voer je dezelfde tests uit die je eerder in PerformanceV3 hebt uitgevoerd, alleen deze keer in tempdb. We beginnen met het testen van veel kleine transacties wanneer de databaseoptie DELAYED_DURABILITY is ingesteld op Uitgeschakeld (standaard). Gebruik de volgende code in het voorbereidingsgedeelte van de testsjabloon:

-- 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';

Gebruik de volgende code in het gedeelte over het eigenlijke werk:

-- 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;

Dit werk genereerde 5.095 log-flushes en het duurde 19 seconden om te voltooien. Dat is vergeleken met meer dan een miljoen log flushes en 193 seconden in een gebruikersdatabase met volledige duurzaamheid. Dat is zelfs beter dan met vertraagde duurzaamheid in een gebruikersdatabase (95.407 log-flushes en 22 seconden) vanwege de kleinere omvang van de logrecords.

Om één grote transactie te testen, laat u de voorbereidingssectie ongewijzigd en gebruikt u de volgende code in de eigenlijke werksectie:

-- 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;

Ik heb 1.228 log flushes en 9 seconden runtime. Dat is vergeleken met 1.758 log-flushes en 7 seconden runtime in de gebruikersdatabase. De runtime is vergelijkbaar, zelfs een beetje sneller in de gebruikersdatabase, maar het kunnen kleine variaties tussen de tests zijn. De grootte van de logrecords in tempdb is kleiner, en daarom krijgt u minder log-flushes in vergelijking met de gebruikersdatabase.

Je kunt ook proberen de tests uit te voeren met de optie DELAYED_DURABILITY ingesteld op Forced, maar dit heeft geen invloed op tempdb, aangezien, zoals gezegd, commit-gebeurtenissen sowieso geen log-flush in tempdb activeren.

Hier zijn de prestatiemetingen voor alle tests, zowel in de gebruikersdatabase als 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

Object caching op volgorde zetten

Misschien is een verrassend geval dat logbufferspoelingen veroorzaakt, gerelateerd aan de cache-optie voor sequentieobjecten. Beschouw als voorbeeld de volgende reeksdefinitie:

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

Elke keer dat je een nieuwe reekswaarde nodig hebt, gebruik je de VOLGENDE WAARDE VOOR-functie, zoals:

SELECT NEXT VALUE FOR dbo.Seq1;

De eigenschap CACHE is een prestatiefunctie. Zonder dit zou SQL Server elke keer dat een nieuwe reekswaarde werd aangevraagd, de huidige waarde naar schijf moeten schrijven voor hersteldoeleinden. Dat is inderdaad het gedrag dat je krijgt als je de NO CACHE-modus gebruikt. In plaats daarvan, wanneer de optie is ingesteld op een waarde groter dan nul, schrijft SQL Server slechts eenmaal per cache-grootte aantal aanvragen een herstelwaarde naar schijf. SQL Server houdt twee leden in het geheugen bij, met de grootte van het reekstype, één met de huidige waarde en één met het aantal resterende waarden voordat de volgende schijfschrijfbewerking van de herstelwaarde nodig is. In het geval van een stroomstoring, stelt SQL Server bij herstart de huidige sequentiewaarde in op de herstelwaarde.

Dit is waarschijnlijk veel gemakkelijker uit te leggen met een voorbeeld. Overweeg de bovenstaande sequentiedefinitie met de CACHE-optie ingesteld op 50 (standaard). U vraagt ​​voor de eerste keer een nieuwe reekswaarde aan door de bovenstaande SELECT-instructie uit te voeren. SQL Server stelt de bovengenoemde leden in op de volgende waarden:

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

Nog 49 verzoeken zullen de schijf niet raken, maar werken alleen de geheugenleden bij. Na in totaal 50 verzoeken zijn de leden ingesteld op de volgende waarden:

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

Doe nog een verzoek om een ​​nieuwe reekswaarde en dit activeert een schijfschrijfactie van de herstelwaarde 100. De leden worden vervolgens ingesteld op de volgende waarden:

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

Als het systeem op dit punt een stroomstoring ondervindt, wordt de huidige sequentiewaarde na het opnieuw opstarten ingesteld op 100 (de waarde die van de schijf is hersteld). Het volgende verzoek om een ​​sequentiewaarde levert 101 op (schrijf de herstelwaarde 150 naar schijf). U bent alle waarden in het bereik van 52 tot 100 kwijt. Het meeste dat u kunt verliezen door een onreine beëindiging van het SQL Server-proces, zoals bij een stroomstoring, zijn zoveel waarden als de cachegrootte. De afweging is duidelijk; hoe groter de cachegrootte, hoe minder de schijf naar de herstelwaarde schrijft en dus hoe beter de prestaties. Tegelijkertijd geldt:hoe groter de kloof die kan worden gegenereerd tussen twee sequentiewaarden in het geval van een stroomstoring.

Dit is allemaal vrij eenvoudig en misschien weet u heel goed hoe het werkt. Wat misschien verrassend is, is dat elke keer dat SQL Server een nieuwe herstelwaarde naar schijf schrijft (elke 50 verzoeken in ons voorbeeld), het ook de logbuffer verhardt. Dat is niet het geval met de eigenschap identiteitskolom, ook al gebruikt SQL Server intern dezelfde cachefunctie voor identiteit als voor het sequentieobject, maar u kunt de grootte ervan niet bepalen. Het is standaard ingeschakeld met maat 10000 voor BIGINT en NUMERIC, 1000 voor INT, 100 voor SMALLINT en 10 VOOR TINYINT. Als je wilt, kun je het uitschakelen met traceringsvlag 272 of de IDENTITY_CACHE scoped-configuratieoptie (2017+). De reden dat SQL Server de logbuffer niet hoeft te legen bij het schrijven van de herstelwaarde met betrekking tot identiteitscache naar schijf, is dat er alleen een nieuwe identiteitswaarde kan worden gemaakt wanneer een rij in een tabel wordt ingevoegd. In het geval van een stroomstoring, wordt een rij die in een tabel is ingevoegd door een transactie die niet is vastgelegd, uit de tabel gehaald als onderdeel van het databaseherstelproces wanneer het systeem opnieuw wordt opgestart. Dus zelfs als SQL Server na het opnieuw opstarten dezelfde identiteitswaarde genereert als de waarde die is gemaakt in de transactie die niet is vastgelegd, is er geen kans op duplicaten omdat de rij uit de tabel is gehaald. Als de transactie was gepleegd, zou dit een log-flush hebben geactiveerd, die ook het schrijven van een cache-gerelateerde herstelwaarde zou voortzetten. Daarom voelde Microsoft zich niet genoodzaakt om de logbuffer leeg te spoelen telkens wanneer een identiteitscache-gerelateerde schijf naar de herstelwaarde wordt geschreven.

Met het sequentieobject is de situatie anders. Een applicatie kan een nieuwe sequentiewaarde opvragen en niet opslaan in de database. In het geval van een stroomstoring na het maken van een nieuwe reekswaarde in een transactie die niet is vastgelegd, is er na het opnieuw opstarten geen manier voor SQL Server om de toepassing te laten weten niet op die waarde te vertrouwen. Om te voorkomen dat er na het opnieuw opstarten een nieuwe reekswaarde wordt gemaakt die gelijk is aan een eerder gegenereerde reekswaarde, dwingt SQL Server daarom een ​​logboekspoeling af telkens wanneer een nieuwe reekscachegerelateerde herstelwaarde naar schijf wordt geschreven. Een uitzondering op deze regel is dat wanneer het sequence-object in tempdb wordt gemaakt, dergelijke log-flushes natuurlijk niet nodig zijn, aangezien tempdb sowieso opnieuw wordt gemaakt nadat het systeem opnieuw is opgestart.

Een negatief effect op de prestaties van de frequente log-flush is vooral merkbaar bij het gebruik van een zeer kleine reeks cache-grootte, en in één transactie die veel reekswaarde genereert, bijvoorbeeld bij het invoegen van veel rijen in een tabel. Zonder de volgorde zou een dergelijke transactie de logbuffer meestal hard maken wanneer deze vol raakt, plus nog een keer wanneer de transactie wordt vastgelegd. Maar met de reeks krijgt u een log-flush elke keer dat een schijf naar een herstelwaarde wordt geschreven. Daarom wil je het gebruik van een kleine cachegrootte vermijden — om nog maar te zwijgen van de NO CACHE-modus.

Om dit te demonstreren, gebruikt u de volgende code in de voorbereidingssectie van onze testsjabloon:

-- 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

En de volgende code in het eigenlijke werkgedeelte:

-- 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;

Deze code gebruikt één transactie om 1.000.000 rijen in een tabel te schrijven met behulp van de SELECT INTO-instructie, waarbij evenveel reekswaarden worden gegenereerd als het aantal ingevoegde rijen.

Voer de test uit zoals aangegeven in de opmerkingen met GEEN CACHE, CACHE 50 en CACHE 10000, zowel in PerformanceV3 als in tempdb, en probeer zowel volledig duurzame transacties als vertraagde duurzame transacties.

Dit zijn de prestatiecijfers die ik op mijn systeem heb:

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

Er zijn nogal wat interessante dingen om op te merken.

Met GEEN CACHE krijgt u een log-flush voor elke gegenereerde sequentiewaarde. Daarom wordt het ten zeerste aanbevolen om het te vermijden.

Met een kleine reeks cache-grootte krijg je nog steeds veel log-flushes. Misschien is de situatie niet zo slecht als met GEEN CACHE, maar merk op dat de werklast 4 seconden duurde om te voltooien met de standaard cachegrootte van 50 vergeleken met minder dan een seconde met de grootte van 10.000. Persoonlijk gebruik ik 10.000 als mijn voorkeurswaarde.

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.

Conclusie

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.


  1. Praag PostgreSQL Developer Day 2016

  2. RDBMS versus NoSQL

  3. PostgreSQL uitvoeren met Amazon RDS

  4. Hoe verander je 2 zoekopdrachten met gemeenschappelijke kolommen (A, B) en (A, C) in slechts één (A, B, C)?