Du har säkert hört många gånger förut att SQL Server ger en garanti för ACID-transaktionsegenskaper. Den här artikeln fokuserar på D-delen, som såklart står för hållbarhet. Mer specifikt fokuserar den här artikeln på en aspekt av SQL Server-loggningsarkitekturen som upprätthåller transaktionshållbarhet – loggbuffertspolningar. Jag pratar om funktionen som loggbufferten tjänar, villkoren som tvingar SQL Server att spola loggbufferten till disken, vad du kan göra för att optimera transaktionsprestanda, såväl som nyligen tillagda relaterade teknologier som fördröjd hållbarhet och icke-flyktigt lagringsklassminne.
Spolar loggbuffert
D-delen i ACID-transaktionsegenskaperna står för hållbarhet. På den logiska nivån betyder det att när en applikation skickar SQL Server en instruktion om att utföra en transaktion (explicit eller med en auto-commit transaktion), returnerar SQL Server normalt kontrollen till den som ringer först efter att den kan garantera att transaktionen är varaktig. Med andra ord, när den som ringer fått tillbaka kontrollen efter att ha utfört en transaktion kan den lita på att även om servern en stund senare råkar ut för ett strömavbrott, gjorde transaktionsändringarna den till databasen. Så länge servern startar om framgångsrikt och databasfilerna inte var skadade kommer du att upptäcka att alla transaktionsändringar har tillämpats.
Sättet SQL Server upprätthåller transaktionens hållbarhet, delvis, är genom att se till att alla transaktionens ändringar skrivs till databasens transaktionslogg på disken innan du återför kontrollen till den som ringer. I ett fall av ett strömavbrott efter att en transaktions bekräftelse har bekräftats, vet du att alla dessa ändringar åtminstone skrevs till transaktionsloggen på disken. Det är fallet även om de relaterade datasidorna endast modifierades i datacachen (buffertpoolen) men ännu inte spolas till datafilerna på disken. När du startar om SQL Server, under omställningsfasen av återställningsprocessen, använder SQL Server informationen som registreras i loggen för att spela upp ändringar som tillämpades efter den senaste kontrollpunkten och som inte har hamnat i datafilerna. Det finns lite mer i historien beroende på vilken återställningsmodell du använder och på om bulkoperationer tillämpades efter den senaste kontrollpunkten, men för vår diskussions syften räcker det med att fokusera på den del som involverar hårdare ändringar av transaktionslogg.
Den knepiga delen i SQL Servers loggningsarkitektur är att loggskrivningar är sekventiella. Hade SQL Server inte använt någon sorts loggbuffert för att lindra loggskrivningar till disk, skulle skrivintensiva system – särskilt de som involverar många små transaktioner – snabbt stöta på fruktansvärda loggskrivningsrelaterade prestandaflaskhalsar.
För att lindra den negativa prestandaeffekten av frekventa sekventiella loggskrivningar till disken använder SQL Server en loggbuffert i minnet. Loggskrivningar görs först till loggbufferten, och vissa förhållanden gör att SQL Server rensar eller hårdnar loggbufferten till disken. Den härdade enheten (alias loggblock) kan variera från minst en sektorstorlek (512 byte) till maximalt 60 KB. Följande är villkor som utlöser en loggbuffertspolning (ignorera de delar som visas inom hakparenteser för närvarande):
- SQL Server får en commit-begäran av en [helt hållbar] transaktion som ändrar data [i en annan databas än tempdb]
- Loggbufferten fylls och når sin kapacitet på 60 kB
- SQL Server behöver härda smutsiga datasidor, t.ex. under en kontrollpunktsprocess, och loggposterna som representerar ändringarna på dessa sidor har ännu inte härdats (skriv i förväg med loggning , eller kortfattat WAL)
- Du begär manuellt en loggbuffertspolning genom att utföra proceduren sys.sp_flush_log
- SQL Server skriver ett nytt sekvenscache-relaterat återställningsvärde [i en annan databas än tempdb]
De fyra första villkoren bör vara ganska tydliga, om du för närvarande bortser från informationen inom hakparenteser. Den sista är kanske inte klar än, men jag ska förklara den i detalj senare i artikeln.
Tiden som SQL Server väntar på att en I/O-operation som hanterar en loggbuffertspolning ska slutföras återspeglas av väntetypen WRITELOG.
Så varför är denna information så intressant, och vad gör vi med den? Att förstå villkoren som utlöser loggbuffertspolningar kan hjälpa dig att ta reda på varför vissa arbetsbelastningar upplever relaterade flaskhalsar. I vissa fall finns det också åtgärder du kan vidta för att minska eller eliminera sådana flaskhalsar. Jag kommer att täcka ett antal exempel som en stor transaktion kontra många små transaktioner, helt hållbara kontra försenade varaktiga transaktioner, användardatabas kontra tempdb och sekvensobjektcache.
En stor transaktion kontra många små transaktioner
Som nämnts är ett av villkoren som utlöser en loggbuffertspolning när du genomför en transaktion för att garantera transaktionens hållbarhet. Detta innebär att arbetsbelastningar som involverar många små transaktioner, som OLTP-arbetsbelastningar, potentiellt kan uppleva logg-skriv-relaterade flaskhalsar.
Även om detta ofta inte är fallet, om du har en enda session som skickar in många små ändringar, är ett enkelt och effektivt sätt att optimera arbetet att tillämpa ändringarna i en enda stor transaktion istället för flera små.
Tänk på följande förenklade exempel (ladda ner PerformanceV3 här):
STÄLL IN NOCOUNT PÅ; ANVÄND PerformanceV3; ALTER DATABASE PerformanceV3 SET DELAYED_DURABILITY =Inaktiverad; -- standard DROP TABLE OM FINNS dbo.T1; CREATE TABLE dbo.T1(col1 INT NOT NULL); DEKLARERA @i SOM INT =1; WHILE @i <=1000000BEGIN BÖRJA ÖVERFÖRA INSERT INTO dbo.T1(col1) VALUES(@i); ÅTGÄRDER TRAN; SET @i +=1;END;
Denna kod exekverar 1 000 000 små transaktioner som ändrar data i en användardatabas. Detta arbete kommer att utlösa minst 1 000 000 loggbuffertspolningar. Du kan få några ytterligare på grund av att stockbufferten fylls upp. Du kan använda följande testmall för att räkna antalet loggbuffertspolningar och mäta tiden det tog arbetet att slutföra:
-- Testmall -- ... Förberedelserna går här ... -- Räkna loggspolningar och mät tidDECLARE @logflushes AS INT, @starttime AS DATETIME2, @duration AS INT; -- Stats beforeSET @logflushes =(VÄLJ cntr_value FROM sys.dm_os_performance_counters WHERE counter_name ='Logg Flushes/sek' OCH instansnamn =@db ); SET @starttid =SYSDATETIME(); -- ... Faktiskt arbete går här ... -- Stats afterSET @duration =DATEDIFF(second, @starttime, SYSDATETIME());SET @logflushes =( SELECT cntr_value FROM sys.dm_os_performance_counters WHERE counter_name ='Logg Flushes/sek. ' OCH instansnamn =@db ) - @logflushes; VÄLJ @duration AS durationinseconds, @logflushes AS logflushes;
Även om prestandaräknarens namn är Log Flushes/sek, fortsätter den faktiskt att ackumulera antalet loggbuffertspolningar hittills. Så, koden subtraherar räkningen före arbete från räkningen efter arbete för att räkna ut antalet loggspolningar som genereras av arbetet. Den här koden mäter också tiden i sekunder det tog arbetet att slutföra. Även om jag inte gör det här, kan du, om du vill, på liknande sätt ta reda på antalet loggposter och storleken som skrivits till loggen av arbetet genom att fråga om tillstånden före och efter arbetet i fn_dblog. funktion.
För vårt exempel ovan är följande delen du behöver placera i förberedelsedelen av testmallen:
-- PreparationSET NOCOUNT ON;USE PerformanceV3; ALTER DATABASE PerformanceV3 SET DELAYED_DURABILITY =Inaktiverad; SLIPP TABELL OM FINNS 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;
Och följande är den del som du behöver placera i själva arbetsdelen:
-- Faktiskt arbeteDECLARE @i AS INT =1; WHILE @i <=1000000BEGIN BÖRJA ÖVERFÖRA INSERT INTO dbo.T1(col1) VALUES(@i); ÅTGÄRDER TRAN; SET @i +=1;END;
Sammantaget får du följande kod:
-- Exempeltest med många små helt hållbara transaktioner i användardatabasen-- ... Förberedelser går här ... -- PreparationSET NOCOUNT ON;USE PerformanceV3; ALTER DATABASE PerformanceV3 SET DELAYED_DURABILITY =Inaktiverad; SLIPP TABELL OM FINNS 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 beforeSET @logflushes =(VÄLJ cntr_value FROM sys.dm_os_performance_counters WHERE counter_name ='Logg Flushes/sek' OCH instansnamn =@db ); SET @starttid =SYSDATETIME(); -- ... Faktiskt arbete går här ... -- Faktiskt arbeteDECLARE @i AS INT =1; WHILE @i <=1000000BEGIN BÖRJA ÖVERFÖRA INSERT INTO dbo.T1(col1) VALUES(@i); ÅTGÄRDER TRAN; SET @i +=1;SLUT; -- Stats afterSET @duration =DATEDIFF(second, @starttime, SYSDATETIME()); SET @logflushes =(VÄLJ cntr_value FRÅN sys.dm_os_performance_counters WHERE counter_name ='Loggspolningar/sek' OCH instansnamn =@db ) - @logflushes; VÄLJ @duration AS durationinseconds, @logflushes AS logflushes;
Den här koden tog 193 sekunder att slutföra på mitt system och utlöste 1 000 036 loggbuffertspolningar. Det är väldigt långsamt, men kan förklaras på grund av det stora antalet stockspolningar.
I typiska OLTP-arbetsbelastningar skickar olika sessioner små ändringar i olika små transaktioner samtidigt, så det är inte så att du verkligen har möjlighet att kapsla in massor av små ändringar i en enda stor transaktion. Men om din situation är att alla små ändringar skickas från samma session, är ett enkelt sätt att optimera arbetet att kapsla in det i en enda transaktion. Detta kommer att ge dig två huvudsakliga fördelar. En är att ditt arbete kommer att skriva färre loggposter. Med 1 000 000 små transaktioner skriver varje transaktion faktiskt tre loggposter:en för att påbörja transaktionen, en för ändringen och en för att genomföra transaktionen. Så du tittar på cirka 3 000 000 transaktionsloggposter mot lite över 1 000 000 när de utförs som en stor transaktion. Men ännu viktigare, med en stor transaktion utlöses de flesta loggspolningarna först när loggbufferten fylls, plus ytterligare en loggspolning i slutet av transaktionen när den genomförs. Skillnaden i prestanda kan vara ganska stor. För att testa arbetet i en stor transaktion, använd följande kod i själva arbetsdelen av testmallen:
-- Faktiskt arbeteBEGIN TRAN; DEKLARERA @i SOM INT =1; WHILE @i <=1000000BEGIN INSERT INTO dbo.T1(col1) VALUES(@i); SET @i +=1; SLUTET; BETA ÖVERFÖRING;
På mitt system slutfördes detta arbete på 7 sekunder och utlöste 1 758 loggspolningar. Här är en jämförelse mellan de två alternativen:
#transactions log rensar varaktigheten i sekunder------- ------------ -------------- ------1000000 1000036 1931 1758 7
Men återigen, i typiska OLTP-arbetsbelastningar har du inte riktigt möjlighet att ersätta många små transaktioner som skickats in från olika sessioner med en stor transaktion som skickats in från samma session.
Helt hållbara kontra försenade varaktiga transaktioner
Från och med SQL Server 2014 kan du använda en funktion som kallas fördröjd hållbarhet som gör att du kan förbättra prestandan för arbetsbelastningar med många små transaktioner, även om de skickas in av olika sessioner, genom att offra den normala fulla hållbarhetsgarantin. När en försenad varaktig transaktion utförs, bekräftar SQL Server commit så snart commit-loggposten skrivs till loggbufferten, utan att utlösa en loggbuffertspolning. Loggbufferten töms på grund av något av de andra ovannämnda förhållandena, som när den fylls, men inte när en försenad varaktig transaktion genomförs.
Innan du använder den här funktionen måste du fundera mycket noga på om den är lämplig för dig. När det gäller prestanda är dess inverkan betydande endast i arbetsbelastningar med många små transaktioner. Om din arbetsbelastning till att börja med huvudsakligen involverar stora transaktioner, kommer du förmodligen inte att se någon prestationsfördel. Ännu viktigare är att du måste inse potentialen för dataförlust. Säg att applikationen begår en försenad varaktig transaktion. En commit-post skrivs till loggbufferten och bekräftas omedelbart (kontroll ges tillbaka till den som ringer). Om SQL Server upplever ett strömavbrott innan loggbufferten töms, efter omstart, ångrar återställningsprocessen alla ändringar som gjordes av transaktionen, även om applikationen tror att den har begåtts.
Så när är det OK att använda den här funktionen? Ett uppenbart fall är när dataförlust inte är ett problem, som det här exemplet från SentryOnes Melissa Connors. En annan är när du efter en omstart har möjlighet att identifiera vilka ändringar som inte gjorde det i databasen, och du kan återskapa dem. Om din situation inte faller inom någon av dessa två kategorier, använd inte den här funktionen trots frestelsen.
För att arbeta med försenade varaktiga transaktioner måste du ställa in ett databasalternativ som heter DELAYED_DURABILITY. Det här alternativet kan ställas in på ett av tre värden:
- Inaktiverad (standard):alla transaktioner i databasen är helt hållbara, och därför utlöser varje commit en loggbuffertspolning
- Tvingad :alla transaktioner i databasen är fördröjda varaktiga, och därför utlöser commits inte en loggbuffertspolning
- Tillåtet :om inget annat nämns är transaktioner helt hållbara och att utföra dem utlöser en loggbuffertspolning; Men om du använder alternativet DELAYED_DURABILITY =ON i antingen en COMMIT TRAN-sats eller ett atomblock (av en inbyggd kompilerad proc), fördröjs den specifika transaktionen varaktig och därför utlöser inte en loggbuffertspolning om du utför den.
Som ett test, använd följande kod i förberedelsedelen av vår testmall (notera att databasalternativet är inställt på Forcerad):
-- PreparationSET NOCOUNT ON;USE PerformanceV3; -- http://tsql.solidq.com/SampleDatabases/PerformanceV3.zip ALTER DATABASE PerformanceV3 SET DELAYED_DURABILITY =Forcerad; SLIPP TABELL OM FINNS dbo.T1; CREATE TABLE dbo.T1(col1 INT NOT NULL); DECLARE @db AS sysname =N'PerformanceV3';
Och använd följande kod i den faktiska arbetssektionen (meddelande, 1 000 000 små transaktioner):
-- Faktiskt arbeteDECLARE @i AS INT =1; WHILE @i <=1000000BEGIN BÖRJA ÖVERFÖRA INSERT INTO dbo.T1(col1) VALUES(@i); ÅTGÄRDER TRAN; SET @i +=1;END;
Alternativt kan du använda Tillåtet läge på databasnivå och sedan i kommandot COMMIT TRAN lägga till MED (DELAYED_DURABILITY =ON).
På mitt system tog arbetet 22 sekunder att slutföra och utlöste 95 407 loggspolningar. Det är längre tid än att köra arbetet som en stor transaktion (7 sekunder) eftersom fler loggposter genereras (kom ihåg, per transaktion, en för att påbörja transaktionen, en för ändringen och en för att genomföra transaktionen); det är dock mycket snabbare än de 193 sekunder som det tog arbetet att slutföra med 1 000 000 helt hållbara transaktioner eftersom antalet stockspolningar minskade från över 1 000 000 till färre än 100 000. Dessutom, med fördröjd hållbarhet, skulle du få prestandavinsten även om transaktionerna skickas från olika sessioner där det inte är möjligt att använda en stor transaktion.
För att visa att det inte finns någon fördel att använda fördröjd hållbarhet när du gör arbetet som stora transaktioner, behåll samma kod i förberedelsedelen av det senaste testet och använd följande kod i själva arbetsdelen:
-- Faktiskt arbeteBEGIN TRAN; DEKLARERA @i SOM INT =1; WHILE @i <=1000000BEGIN INSERT INTO dbo.T1(col1) VALUES(@i); SET @i +=1;SLUT; BETA ÖVERFÖRING;
Jag fick 8 sekunders körtid (jämfört med 7 för en stor och helt hållbar transaktion) och 1 759 loggspolningar (jämfört med 1 758). Siffrorna är i stort sett desamma, men med den försenade varaktiga transaktionen har du risken för dataförlust.
Här är en sammanfattning av prestationssiffrorna för alla fyra testerna:
varaktighet #transactions log rensar varaktigheten i sekunder-------------------------- ------ --------------------full 1000000 1000036 193full 1 1758 7försenad 1000000 95407 22försenad 1 1759 8
Lagringsklassminne
Funktionen för fördröjd hållbarhet kan avsevärt förbättra prestandan för OLTP-liknande arbetsbelastningar som involverar ett stort antal små uppdateringstransaktioner som kräver hög frekvens och låg latens. Problemet är att du riskerar dataförlust. Tänk om du inte kan tillåta någon dataförlust, men du fortfarande vill ha prestandavinster som liknar fördröjd hållbarhet, där loggbufferten inte töms för varje commit, snarare när den fylls? Vi gillar alla att äta kakan och ha den också, eller hur?
Du kan uppnå detta i SQL Server 2016 SP1 eller senare genom att använda lagringsklassminne, aka NVDIMM-N icke-flyktig lagring. Den här hårdvaran är i huvudsak en minnesmodul som ger dig minnesprestanda, men informationen där finns kvar och går därför inte förlorad när strömmen är borta. Tillägget i SQL Server 2016 SP1 låter dig konfigurera loggbufferten som en kvarstående på sådan hårdvara. För att göra detta ställer du in SCM som en volym i Windows och formaterar den som en DAX-volym (Direct Access Mode). Du lägger sedan till en loggfil till databasen med det vanliga kommandot ALTER DATABASE
För mer information om den här funktionen, inklusive prestandasiffror, se Transaction Commit latensacceleration med användning av Storage Class Memory i Windows Server 2016/SQL Server 2016 SP1 av Kevin Farlee.
Märkligt nog förbättrar SQL Server 2019 stödet för lagringsklassminne utöver bara scenariot med kvarstående loggcache. Det stöder placering av datafiler, loggfiler och In-Memory OLTP-kontrollpunktfiler på sådan hårdvara. Allt du behöver göra är att exponera den som en volym på OS-nivå och formatera som en DAX-enhet. SQL Server 2019 känner automatiskt igen denna teknik och fungerar i en upplyst läge, direkt åtkomst till enheten, kringgå operativsystemets lagringsstack. Välkommen till framtiden!
Användardatabas kontra tempdb
tempdb-databasen skapas givetvis från grunden som en ny kopia av modelldatabasen varje gång du startar om SQL Server. Som sådan finns det aldrig ett behov av att återställa någon data som du skriver till tempdb, oavsett om du skriver den till temporära tabeller, tabellvariabler eller användartabeller. Allt är borta efter omstart. Genom att veta detta kan SQL Server lätta på mycket av de loggningsrelaterade kraven. Till exempel, oavsett om du aktiverar alternativet för fördröjd hållbarhet eller inte, utlöser inte commit-händelser en loggbuffertspolning. Dessutom minskar mängden information som behöver loggas eftersom SQL Server bara behöver tillräckligt med information för att stödja återställning av transaktioner, eller ångra arbete, om det behövs, men inte rulla transaktioner framåt eller göra om arbete. Som ett resultat tenderar transaktionsloggposter som representerar ändringar av ett objekt i tempdb att vara mindre jämfört med när samma ändring tillämpas på ett objekt i en användardatabas.
För att demonstrera detta kommer du att köra samma test som du körde tidigare i PerformanceV3, bara denna gång i tempdb. Vi börjar med testet av många små transaktioner när databasalternativet DELAYED_DURABILITY är inställt på Disabled (standard). Använd följande kod i förberedelsedelen av testmallen:
-- PreparationSET NOCOUNT ON;ANVÄND tempdb; ALTER DATABASE tempdb SET DELAYED_DURABILITY =Inaktiverad; SLIPP TABELL OM FINNS dbo.T1; CREATE TABLE dbo.T1(col1 INT NOT NULL); DECLARE @db AS sysname =N'tempdb';
Använd följande kod i själva arbetssektionen:
-- Faktiskt arbeteDECLARE @i AS INT =1; WHILE @i <=1000000BEGIN BÖRJA ÖVERFÖRA INSERT INTO dbo.T1(col1) VALUES(@i); ÅTGÄRDER TRAN; SET @i +=1;END;
Detta arbete genererade 5 095 stockspolningar och det tog 19 sekunder att slutföra. Det kan jämföras med över en miljon loggspolningar och 193 sekunder i en användardatabas med full hållbarhet. Det är ännu bättre än med fördröjd hållbarhet i en användardatabas (95 407 loggspolningar och 22 sekunder) på grund av den minskade storleken på loggposterna.
För att testa en stor transaktion, lämna förberedelsedelen oförändrad och använd följande kod i själva arbetsdelen:
-- Faktiskt arbeteBEGIN TRAN; DEKLARERA @i SOM INT =1; WHILE @i <=1000000BEGIN INSERT INTO dbo.T1(col1) VALUES(@i); SET @i +=1;SLUT; BETA ÖVERFÖRING;
Jag fick 1 228 loggspolningar och 9 sekunders körtid. Det jämförs med 1 758 loggspolningar och 7 sekunders körtid i användardatabasen. Körtiden är liknande, till och med lite snabbare i användardatabasen, men det kan vara små variationer mellan testerna. Storleken på loggposterna i tempdb reduceras, och därför får du färre loggspolningar jämfört med användardatabasen.
Du kan också prova att köra testerna med alternativet DELAYED_DURABILITY inställt på Forcerad, men detta kommer inte att ha någon inverkan i tempdb eftersom, som nämnts, commit-händelser ändå inte utlöser en loggspolning i tempdb.
Här är prestandamåtten för alla tester, både i användardatabasen och i tempdb:
databasens hållbarhet #transactions log rensar varaktigheten i sekunder-------------------------------- ----- ---------- ------------ --------------------PerformanceV3 full 1000000 1000036 193PerformanceV3 full 1 1758 7PerformanceV3 fördröjd 1000000 95407 22PerformanceV3 fördröjd 1 1759 8tempdb full 1000000 5095 19tempdb full 1 1228 9tempdb försenad 1000000 5091 18tempdb fördröjdCaching av sekvensobjekt
Kanske är ett överraskande fall som utlöser loggbuffertspolningar relaterat till alternativet sekvensobjektcache. Betrakta som ett exempel följande sekvensdefinition:
SKAPA SEKVENS dbo.Seq1 SOM STORT MINVÄRDE 1 CACHE 50; -- standardcachestorleken är 50;Varje gång du behöver ett nytt sekvensvärde använder du funktionen NEXT VALUE FOR, som så:
VÄLJ NÄSTA VÄRDE FÖR dbo.Seq1;CACHE-egenskapen är en prestandafunktion. Utan det, varje gång ett nytt sekvensvärde begärdes, skulle SQL Server ha behövt skriva det aktuella värdet till disken för återställningsändamål. Det är faktiskt det beteende du får när du använder NO CACHE-läget. Istället, när alternativet är inställt på ett värde som är större än noll, skriver SQL Server ett återställningsvärde till disken endast en gång för varje antal förfrågningar i cachestorlek. SQL Server bibehåller två medlemmar i minnet, dimensionerade som sekvenstypen, en som håller det aktuella värdet och en håller antalet värden kvar innan nästa diskskrivning av återställningsvärdet behövs. I händelse av ett strömavbrott, vid omstart, ställer SQL Server in det aktuella sekvensvärdet till återställningsvärdet.
Detta är förmodligen mycket lättare att förklara med ett exempel. Tänk på ovanstående sekvensdefinition med CACHE-alternativet satt till 50 (standard). Du begär ett nytt sekvensvärde för första gången genom att köra ovanstående SELECT-sats. SQL Server ställer in de tidigare nämnda medlemmarna till följande värden:
Återställningsvärde på disk:50, aktuellt värde i minnet:1, värden kvar i minnet:49, Du får:149 fler förfrågningar kommer inte att röra disken, utan bara uppdatera minnesmedlemmarna. Efter totalt 50 förfrågningar ställs medlemmarna in på följande värden:
Återställningsvärde på disk:50, aktuellt värde i minnet:50, värden kvar i minnet:0, Du får:50Gör ytterligare en begäran om ett nytt sekvensvärde, och detta utlöser en diskskrivning av återställningsvärdet 100. Medlemmarna ställs sedan in på följande värden:
Återställningsvärde på disk:100, nuvarande värde i minnet:51, värden kvar i minnet:49, du får:51Om systemet vid denna tidpunkt råkar ut för ett strömavbrott, efter omstart, sätts det aktuella sekvensvärdet till 100 (värdet som återställs från disken). Nästa begäran om ett sekvensvärde ger 101 (skriver återställningsvärdet 150 till disken). Du förlorade alla värden i intervallet 52 till 100. Det mesta du kan förlora på grund av en oren avslutning av SQL Server-processen, som vid ett strömavbrott, är lika många värden som cachestorleken. Avvägningen är tydlig; Ju större cachestorlek, desto färre skriver disken av återställningsvärdet, och därmed desto bättre prestanda. Samtidigt, desto större gap som kan genereras mellan två sekvensvärden vid strömavbrott.
Allt detta är ganska enkelt, och du kanske är mycket väl bekant med hur det fungerar. Vad som kan vara förvånande är att varje gång SQL Server skriver ett nytt återställningsvärde till disken (var 50:e begäran i vårt exempel), hårdnar den också loggbufferten. Det är inte fallet med identitetskolumnegenskapen, även om SQL Server internt använder samma cachningsfunktion för identitet som den gör för sekvensobjektet, låter den dig bara inte kontrollera dess storlek. Den är på som standard med storlek 10000 för BIGINT och NUMERIC, 1000 för INT, 100 för SMALLINT och 10 för TINYINT. Om du vill kan du stänga av den med spårningsflagga 272 eller konfigurationsalternativet IDENTITY_CACHE (2017+). Anledningen till att SQL Server inte behöver tömma loggbufferten när man skriver det identitetscache-relaterade återställningsvärdet till disken är att ett nytt identitetsvärde bara kan skapas när en rad infogas i en tabell. I händelse av ett strömavbrott kommer en rad som infogas i en tabell av en transaktion som inte genomfördes att dras ut från tabellen som en del av databasåterställningsprocessen när systemet startar om. Så även om SQL Server efter omstarten genererar samma identitetsvärde som det som skapades i transaktionen som inte genomfördes, finns det ingen chans för dubbletter eftersom raden drogs ut ur tabellen. Om transaktionen hade genomförts skulle detta ha utlöst en loggrensning, vilket också skulle fortsätta att skriva ett cache-relaterat återställningsvärde. Microsoft kände sig därför inte tvungen att tömma loggbufferten varje gång en identitetscache-relaterad diskskrivning av återställningsvärdet äger rum.
Med sekvensobjektet är situationen annorlunda. En applikation kan begära ett nytt sekvensvärde och inte lagra det i databasen. I händelse av ett strömavbrott efter skapandet av ett nytt sekvensvärde i en transaktion som inte genomfördes, efter omstart, finns det inget sätt för SQL Server att tala om för applikationen att inte förlita sig på det värdet. Därför, för att undvika att skapa ett nytt sekvensvärde efter omstart som är lika med ett tidigare genererat sekvensvärde, tvingar SQL Server en loggrensning varje gång ett nytt sekvenscache-relaterat återställningsvärde skrivs till disken. Ett undantag från denna regel är när sekvensobjektet skapas i tempdb, det finns naturligtvis inget behov av sådana loggspolningar eftersom tempdb i alla fall efter en omstart av systemet skapas på nytt.
En negativ prestandapåverkan av de frekventa loggspolningarna är särskilt märkbar när man använder en mycket liten sekvenscachestorlek och i en transaktion genererar massor av sekvensvärden, t.ex. när man infogar massor av rader i en tabell. Utan sekvensen skulle en sådan transaktion mestadels hårdna loggbufferten när den fylls, plus en gång till när transaktionen genomförs. Men med sekvensen får du en loggspolning varje gång en diskskrivning av ett återställningsvärde äger rum. Det är därför du vill undvika att använda en liten cachestorlek – för att inte tala om läget NO CACHE.
För att demonstrera detta, använd följande kod i förberedelsedelen av vår testmall:
-- PreparationSET NOCOUNT ON;USE PerformanceV3; -- prova PerformanceV3, tempdb ALTER DATABASE PerformanceV3 -- prova PerformanceV3, tempdb SET DELAYED_DURABILITY =Inaktiverad; -- försök Disabled, Forced DROP TABLE OM FINNS dbo.T1; DROPSEKVENS OM FINNS dbo.Seq1; SKAPA SEKVENS dbo.Seq1 SOM STORT MINVÄRDE 1 CACHE 50; -- prova NO CACHE, CACHE 50, CACHE 10000 DECLARE @db AS sysname =N'PerformanceV3'; -- prova PerformanceV3, tempdbOch följande kod i själva arbetssektionen:
-- Faktiskt arbeteSELECT -- n -- för att testa utan seq NÄSTA VÄRDE FÖR dbo.Seq1 AS n -- för att testa sequenceINTO dbo.T1FROM PerformanceV3.dbo.GetNums(1, 1000000) AS N;Den här koden använder en transaktion för att skriva 1 000 000 rader i en tabell med hjälp av SELECT INTO-satsen, vilket genererar lika många sekvensvärden som antalet infogade rader.
Enligt instruktionerna i kommentarerna, kör testet med NO CACHE, CACHE 50 och CACHE 10000, både i PerformanceV3 och i tempdb, och prova både helt hållbara transaktioner och fördröjda varaktiga.
Här är prestandasiffrorna som jag fick på mitt system:
databasens hållbarhet cachelogg rensar varaktigheten i sekunder-------------------------------- ------ --- ------------ --------------------PerformanceV3 full NO CACHE 1000047 171PerformanceV3 full 50 20008 4PerformanceV3 full 10000 339 <1tempdb Full No Cache 96 4TempDB Full 50 74 1TempDB Full 10000 8 <1PerformanceV3 Försenad ingen cache 1000045 166PerformanceV3 Försenad 50 20011 4PerformanceV3 Försenad 10000 334 <1tempdb Försenad No Cache 91 4TempDB -fördröjning 50 74 1TempDB -fördröjningDet finns en hel del intressanta saker att lägga märke till.
Med INGEN CACHE får du en loggspolning för varje enskilt sekvensvärde som genereras. Därför rekommenderas det starkt att undvika det.
Med en liten sekvenscachestorlek får du fortfarande massor av loggspolningar. Situationen kanske inte är lika illa som med NO CACHE, men observera att arbetsbelastningen tog 4 sekunder att slutföra med standardcachestorleken 50 jämfört med mindre än en sekund med storleken 10 000. Jag använder personligen 10 000 som mitt föredragna värde.
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.
Slutsats
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.