Du har sikkert hørt mange gange før, at SQL Server giver en garanti for ACID-transaktionsegenskaber. Denne artikel fokuserer på D-delen, som selvfølgelig står for holdbarhed. Mere specifikt fokuserer denne artikel på et aspekt af SQL Server-logningsarkitekturen, der gennemtvinger transaktionsholdbarhed - log buffer flushes. Jeg taler om den funktion, som logbufferen tjener, de forhold, der tvinger SQL Server til at skylle logbufferen til disken, hvad du kan gøre for at optimere transaktionsydelsen, samt nyligt tilføjede relaterede teknologier som forsinket holdbarhed og ikke-flygtig lagerklassehukommelse.
Skylninger af logbuffer
D-delen i ACID-transaktionsegenskaberne står for holdbarhed. På det logiske niveau betyder det, at når en applikation sender SQL Server en instruktion om at udføre en transaktion (eksplicit eller med en auto-commit transaktion), returnerer SQL Server normalt først kontrol til den, der ringer, efter at den kan garantere, at transaktionen er holdbar. Med andre ord, når den, der ringer, har fået kontrollen tilbage efter at have begået en transaktion, kan den stole på, at selvom serveren et øjeblik senere oplever et strømsvigt, har transaktionsændringerne gjort det til databasen. Så længe serveren genstarter med succes, og databasefilerne ikke er beskadigede, vil du opdage, at alle transaktionsændringer er blevet anvendt.
Måden SQL Server til dels håndhæver transaktionernes holdbarhed på er ved at sikre, at alle transaktionens ændringer skrives til databasens transaktionslog på disk før kontrollen tilbage til den, der ringer. I tilfælde af strømsvigt, efter at en transaktions forpligtelse er blevet bekræftet, ved du, at alle disse ændringer i det mindste blev skrevet til transaktionsloggen på disken. Det er tilfældet, selvom de relaterede datasider kun blev ændret i datacachen (bufferpuljen), men endnu ikke tømt til datafilerne på disken. Når du genstarter SQL Server, under gendannelsesfasen, bruger SQL Server de oplysninger, der er registreret i loggen, til at afspille ændringer, der blev anvendt efter det sidste kontrolpunkt, og som ikke er nået til datafilerne. Der er lidt mere i historien afhængigt af den gendannelsesmodel, du bruger, og om bulkoperationer blev anvendt efter det sidste kontrolpunkt, men i forbindelse med vores diskussion er det tilstrækkeligt at fokusere på den del, der involverer at hærde ændringerne i transaktionslog.
Den vanskelige del i SQL Servers logningsarkitektur er, at logskrivninger er sekventielle. Havde SQL Server ikke brugt en form for log-buffer til at lindre logskrivninger til disk, ville skrivetunge systemer – især dem, der involverer mange små transaktioner – hurtigt støde ind i frygtelige log-skrivningsrelaterede ydeevneflaskehalse.
For at afhjælpe den negative ydeevnepåvirkning af hyppige sekventielle logskrivninger til disk, bruger SQL Server en logbuffer i hukommelsen. Logskrivninger udføres først til logbufferen, og visse forhold får SQL Server til at tømme eller hærde logbufferen til disken. Den hærdede enhed (alias logblok) kan variere fra minimum en sektorstørrelse (512 bytes) til maksimalt 60 KB. Følgende er forhold, der udløser en logbufferskylning (ignorer de dele, der vises i firkantede parenteser indtil videre):
- SQL Server modtager en commit-anmodning om en [fuldstændig holdbar] transaktion, der ændrer data [i en anden database end tempdb]
- Logbufferen fyldes op og når sin kapacitet på 60 KB
- SQL Server skal hærde snavsede datasider, f.eks. under en checkpoint-proces, og logposterne, der repræsenterer ændringerne på disse sider, er endnu ikke hærdet (skriv forud for logning , eller kort sagt WAL)
- Du anmoder manuelt om en logbufferrensning ved at udføre proceduren sys.sp_flush_log
- SQL Server skriver en ny sekvenscache-relateret gendannelsesværdi [i en anden database end tempdb]
De første fire betingelser burde være ret klare, hvis du indtil videre ignorerer oplysningerne i firkantede parenteser. Den sidste er måske ikke klar endnu, men jeg vil forklare den i detaljer senere i artiklen.
Den tid, SQL Server venter på, at en I/O-operation, der håndterer en logbufferflush, er fuldført, afspejles af WRITELOG-ventetypen.
Så hvorfor er denne information så interessant, og hvad gør vi med den? Forståelse af betingelserne, der udløser logbufferskylninger, kan hjælpe dig med at finde ud af, hvorfor visse arbejdsbelastninger oplever relaterede flaskehalse. I nogle tilfælde er der også handlinger, du kan tage for at reducere eller eliminere sådanne flaskehalse. Jeg vil dække en række eksempler som en stor transaktion versus mange små transaktioner, fuldt holdbare versus forsinkede varige transaktioner, brugerdatabase versus tempdb og sekvensobjektcache.
Én stor transaktion versus mange små transaktioner
Som nævnt er en af de betingelser, der udløser en log buffer flush, når du foretager en transaktion for at garantere transaktionens holdbarhed. Dette betyder, at arbejdsbelastninger, der involverer mange små transaktioner, såsom OLTP-arbejdsbelastninger, potentielt kan opleve log-skrive-relaterede flaskehalse.
Selvom dette ofte ikke er tilfældet, hvis du har en enkelt session, der sender en masse små ændringer, er en enkel og effektiv måde at optimere arbejdet på at anvende ændringerne i en enkelt stor transaktion i stedet for flere små.
Overvej følgende forenklede eksempel (download PerformanceV3 her):
INDSTIL ANTAL TIL; BRUG PerformanceV3; ALTER DATABASE PerformanceV3 SET DELAYED_DURABILITY =Deaktiveret; -- standard DROP TABLE HVIS FINDER dbo.T1; CREATE TABLE dbo.T1(col1 INT NOT NULL); DEKLARE @i SOM INT =1; WHILE @i <=1000000BEGIN BEGIN TRAN INSERT INTO dbo.T1(col1) VALUES(@i); FORBINDE TRAN; SET @i +=1;END;
Denne kode udfører 1.000.000 små transaktioner, der ændrer data i en brugerdatabase. Dette arbejde vil udløse mindst 1.000.000 log buffer skylninger. Du kan få et par ekstra på grund af logbufferen, der fyldes op. Du kan bruge følgende testskabelon til at tælle antallet af logbufferskylninger og måle den tid, det tog arbejdet at fuldføre:
-- Testskabelon -- ... Forberedelse går her ... -- Tæl log skylninger og mål timeDECLARE @logflushes AS INT, @starttime AS DATETIME2, @duration AS INT; -- Stats beforeSET @logflushes =(VÆLG cntr_value FROM sys.dm_os_performance_counters WHERE counter_name ='Log Flushes/sek' OG instance_name =@db ); SET @starttid =SYSDATETIME(); -- ... Faktisk arbejde går her ... -- Stats afterSET @duration =DATEDIFF(second, @starttime, SYSDATETIME());SET @logflushes =( SELECT cntr_value FROM sys.dm_os_performance_counters WHERE counter_name ='Log Flushes/sek. ' OG instansnavn =@db ) - @logflushes; VÆLG @duration AS durationinseconds, @logflushes AS logflushes;
Selvom præstationstællernavnet er Log Flushes/sek., bliver det faktisk ved med at akkumulere antallet af log buffer flushes indtil videre. Så koden trækker tællingen før arbejde fra tællingen efter arbejde for at finde ud af antallet af log skylninger genereret af arbejdet. Denne kode måler også den tid i sekunder, det tog arbejdet at fuldføre. Selvom jeg ikke gør det her, kunne du, hvis du ville, på samme måde finde ud af antallet af logposter og størrelsen skrevet til loggen af værket ved at forespørge før-arbejde og efter-arbejde-tilstande i fn_dblog funktion.
For vores eksempel ovenfor er følgende den del, du skal placere i forberedelsessektionen af testskabelonen:
-- ForberedelseSET NOCOUNT ON;USE PerformanceV3; ALTER DATABASE PerformanceV3 SET DELAYED_DURABILITY =Deaktiveret; DROP TABEL HVIS FINDER 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;
Og følgende er den del, du skal placere i selve arbejdssektionen:
-- Faktisk workDECLARE @i AS INT =1; WHILE @i <=1000000BEGIN BEGIN TRAN INSERT INTO dbo.T1(col1) VALUES(@i); FORBINDE TRAN; SET @i +=1;END;
Alt i alt får du følgende kode:
-- Eksempeltest med mange små fuldt holdbare transaktioner i brugerdatabase-- ... Forberedelse går her ... -- ForberedelseSET NOCOUNT ON;USE PerformanceV3; ALTER DATABASE PerformanceV3 SET DELAYED_DURABILITY =Deaktiveret; DROP TABEL HVIS FINDER 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ÆLG cntr_value FROM sys.dm_os_performance_counters WHERE counter_name ='Log Flushes/sek' OG instance_name =@db ); SET @starttid =SYSDATETIME(); -- ... Faktisk arbejde går her ... -- Faktisk arbejdeDECLARE @i AS INT =1; WHILE @i <=1000000BEGIN BEGIN TRAN INSERT INTO dbo.T1(col1) VALUES(@i); FORBINDE TRAN; SET @i +=1;SLUT; -- Stats afterSET @varighed =DATODIFF(sekund, @starttidspunkt, SYSDATETID()); SET @logflushes =(VÆLG cntr_value FROM sys.dm_os_performance_counters WHERE counter_name ='Log Flushes/sek' OG instance_name =@db ) - @logflushes; VÆLG @duration AS durationinseconds, @logflushes AS logflushes;
Denne kode tog 193 sekunder at fuldføre på mit system og udløste 1.000.036 logbufferudskylninger. Det er meget langsomt, men kan forklares på grund af det store antal log skylninger.
I typiske OLTP-arbejdsbelastninger indsender forskellige sessioner små ændringer i forskellige små transaktioner samtidigt, så det er ikke sådan, at du virkelig har mulighed for at indkapsle masser af små ændringer i en enkelt stor transaktion. Men hvis din situation er, at alle de små ændringer sendes fra samme session, er en enkel måde at optimere arbejdet på at indkapsle det i en enkelt transaktion. Dette vil give dig to hovedfordele. Den ene er, at dit arbejde vil skrive færre logposter. Med 1.000.000 små transaktioner skriver hver transaktion faktisk tre logposter:en til at begynde transaktionen, en til ændringen og en til at udføre transaktionen. Så du ser på omkring 3.000.0000 transaktionslogposter mod lidt over 1.000.000, når de udføres som én stor transaktion. Men endnu vigtigere, med en stor transaktion udløses de fleste log-flush kun, når log-bufferen fyldes op, plus endnu en log-flush helt til sidst i transaktionen, når den forpligter sig. Ydeevneforskellen kan være ret betydelig. For at teste arbejdet i én stor transaktion skal du bruge følgende kode i selve arbejdsdelen af testskabelonen:
-- Faktisk arbejdeBEGIN TRAN; DEKLARE @i SOM INT =1; WHILE @i <=1000000BEGIN INSERT INTO dbo.T1(col1) VALUES(@i); SET @i +=1; ENDE; FORBIND OVERFØRSEL;
På mit system blev dette arbejde afsluttet på 7 sekunder og udløste 1.758 log skylninger. Her er en sammenligning mellem de to muligheder:
#transactions log skyller varighed i sekunder------- ------------ -------------- ------1000000 1000036 1931 1758 7
Men igen, i typiske OLTP-arbejdsbelastninger har du ikke rigtig mulighed for at erstatte mange små transaktioner indsendt fra forskellige sessioner med en stor transaktion indsendt fra samme session.
Fuldt holdbare kontra forsinkede varige transaktioner
Fra og med SQL Server 2014 kan du bruge en funktion kaldet forsinket holdbarhed, der giver dig mulighed for at forbedre ydeevnen af arbejdsbelastninger med mange små transaktioner, selv hvis de indsendes af forskellige sessioner, ved at ofre den normale fulde holdbarhedsgaranti. Når der udføres en forsinket varig transaktion, anerkender SQL Server commit, så snart commit-logposten er skrevet til logbufferen, uden at udløse en logbufferflush. Logbufferen tømmes på grund af nogen af de andre førnævnte forhold, f.eks. når den fyldes op, men ikke når en forsinket varig transaktion commits.
Før du bruger denne funktion, skal du tænke meget nøje over, om den er passende for dig. Med hensyn til ydeevne er dens indvirkning kun betydelig i arbejdsbelastninger med mange små transaktioner. Hvis din arbejdsbyrde til at begynde med primært involverer store transaktioner, vil du sandsynligvis ikke se nogen præstationsfordel. Endnu vigtigere er det, at du skal indse potentialet for tab af data. Lad os sige, at applikationen begår en forsinket varig transaktion. En commit-record skrives til logbufferen og bekræftes straks (kontrol gives tilbage til den, der ringer). Hvis SQL Server oplever et strømsvigt, før logbufferen tømmes, efter genstart, fortryder gendannelsesprocessen alle de ændringer, der blev foretaget af transaktionen, selvom applikationen mener, at den er blevet begået.
Så hvornår er det OK at bruge denne funktion? Et oplagt tilfælde er, når datatab ikke er et problem, som dette eksempel fra SentryOne's Melissa Connors. En anden er, når du efter en genstart har midlerne til at identificere, hvilke ændringer der ikke gjorde det til databasen, og du er i stand til at reproducere dem. Hvis din situation ikke falder ind under en af disse to kategorier, skal du ikke bruge denne funktion på trods af fristelsen.
For at arbejde med forsinkede varige transaktioner skal du indstille en databaseindstilling kaldet DELAYED_DURABILITY. Denne indstilling kan indstilles til en af tre værdier:
- Deaktiveret (standard):alle transaktioner i databasen er fuldt holdbare, og derfor udløser hver commit en log buffer flush
- Tvungen :alle transaktioner i databasen er forsinket varigt, og derfor udløser commits ikke en log buffer flush
- Tilladt :Medmindre andet er nævnt, er transaktioner fuldt ud holdbare, og begåelse af dem udløser en logbufferflush; Men hvis du bruger indstillingen DELAYED_DURABILITY =ON i enten en COMMIT TRAN-sætning eller en atomblok (af en indbygget kompileret proc), er den pågældende transaktion forsinket varig, og derfor udløser det ikke en logbufferskylning
Som en test skal du bruge følgende kode i forberedelsessektionen af vores testskabelon (bemærk, at databaseindstillingen er sat til Forced):
-- ForberedelseSET NOCOUNT ON;USE PerformanceV3; -- http://tsql.solidq.com/SampleDatabases/PerformanceV3.zip ALTER DATABASE PerformanceV3 SET DELAYED_DURABILITY =Tvunget; DROP TABEL HVIS FINDER dbo.T1; CREATE TABLE dbo.T1(col1 INT NOT NULL); DECLARE @db AS sysname =N'PerformanceV3';
Og brug følgende kode i selve arbejdssektionen (bemærk, 1.000.000 små transaktioner):
-- Faktisk workDECLARE @i AS INT =1; WHILE @i <=1000000BEGIN BEGIN TRAN INSERT INTO dbo.T1(col1) VALUES(@i); FORBINDE TRAN; SET @i +=1;END;
Alternativt kan du bruge Tilladt tilstand på databaseniveau og derefter i kommandoen COMMIT TRAN tilføje MED (DELAYED_DURABILITY =ON).
På mit system tog arbejdet 22 sekunder at fuldføre og udløste 95.407 log skylninger. Det er længere tid end at køre arbejdet som én stor transaktion (7 sekunder), da der genereres flere logposter (husk, pr. transaktion, én til at starte transaktionen, én til ændringen og én til at udføre transaktionen); det er dog meget hurtigere end de 193 sekunder, som det tog arbejdet at fuldføre ved at bruge 1.000.000 fuldt holdbare transaktioner, da antallet af log skylninger faldt fra over 1.000.000 til færre end 100.000. Plus, med forsinket holdbarhed ville du få præstationsgevinsten, selvom transaktionerne indsendes fra forskellige sessioner, hvor det ikke er en mulighed at bruge én stor transaktion.
For at demonstrere, at der ikke er nogen fordel at bruge forsinket holdbarhed, når du udfører arbejdet som store transaktioner, skal du beholde den samme kode i forberedelsesdelen af den sidste test og bruge følgende kode i selve arbejdsdelen:
-- Faktisk arbejdeBEGIN TRAN; DEKLARE @i SOM INT =1; WHILE @i <=1000000BEGIN INSERT INTO dbo.T1(col1) VALUES(@i); SET @i +=1;SLUT; FORBIND OVERFØRSEL;
Jeg fik 8 sekunders køretid (sammenlignet med 7 for en stor fuldt holdbar transaktion) og 1.759 log skylninger (sammenlignet med 1.758). Tallene er stort set de samme, men med den forsinkede varige transaktion har du risikoen for tab af data.
Her er en oversigt over præstationstallene for alle fire tests:
holdbarhed #transactions log skyller varighed i sekunder-------------------------------------------------------- ------ --------------------fuld 1000000 1000036 193fuld 1 1758 7forsinket 1000000 95407 22forsinket 1 1759 8
Lagringsklassehukommelse
Funktionen for forsinket holdbarhed kan markant forbedre ydeevnen af OLTP-lignende arbejdsbelastninger, der involverer et stort antal små opdateringstransaktioner, der kræver høj frekvens og lav latenstid. Problemet er, at du risikerer tab af data. Hvad hvis du ikke kan tillade noget datatab, men du stadig vil have forsinket holdbarhed-lignende præstationsgevinster, hvor logbufferen ikke bliver tømt for hver commit, snarere når den fyldes op? Vi kan alle godt lide at spise kagen og have den også, ikke?
Du kan opnå dette i SQL Server 2016 SP1 eller nyere ved at bruge lagerklassehukommelse, alias NVDIMM-N ikke-flygtig lager. Denne hardware er i bund og grund et hukommelsesmodul, der giver dig ydeevne i hukommelsesgrad, men informationen der er vedvarende og går derfor ikke tabt, når strømmen er væk. Tilføjelsen i SQL Server 2016 SP1 lader dig konfigurere logbufferen som en vedvarende på sådan hardware. For at gøre dette skal du konfigurere SCM'en som en diskenhed i Windows og formatere den som en DAX-diskenhed (Direct Access Mode). Du tilføjer derefter en logfil til databasen ved hjælp af den normale ALTER DATABASE
For flere detaljer om denne funktion, inklusive ydelsestal, se Transaction Commit latency acceleration ved hjælp af Storage Class Memory i Windows Server 2016/SQL Server 2016 SP1 af Kevin Farlee.
Mærkeligt nok forbedrer SQL Server 2019 understøttelsen af lagerklassehukommelse ud over blot det vedvarende logcache-scenarie. Det understøtter placering af datafiler, logfiler og OLTP-kontrolpunktfiler i hukommelsen på sådan hardware. Alt du skal gøre er at udsætte det som en volumen på OS-niveau og formatere som et DAX-drev. SQL Server 2019 genkender automatisk denne teknologi og fungerer i en oplyst tilstand, direkte adgang til enheden, uden om operativsystemets lagerstak. Velkommen til fremtiden!
Brugerdatabase versus tempdb
tempdb-databasen er naturligvis oprettet fra bunden som en frisk kopi af modeldatabasen, hver gang du genstarter SQL Server. Som sådan er der aldrig behov for at gendanne data, som du skriver til tempdb, uanset om du skriver dem til midlertidige tabeller, tabelvariabler eller brugertabeller. Det hele er væk efter genstart. Ved at vide dette kan SQL Server lempe en masse af de logningsrelaterede krav. For eksempel, uanset om du aktiverer indstillingen for forsinket holdbarhed eller ej, udløser commit-hændelser ikke en logbuffer-flush. Ydermere reduceres mængden af information, der skal logges, da SQL Server kun har brug for nok information til at understøtte tilbagerulning af transaktioner eller fortrydelse af arbejde, hvis det er nødvendigt, men ikke rullende transaktioner fremad eller gentage arbejde. Som et resultat har transaktionslogposter, der repræsenterer ændringer af et objekt i tempdb, tendens til at være mindre sammenlignet med, når den samme ændring anvendes på et objekt i en brugerdatabase.
For at demonstrere dette, vil du køre de samme tests, som du kørte tidligere i PerformanceV3, kun denne gang i tempdb. Vi starter med testen af mange små transaktioner, når databaseindstillingen DELAYED_DURABILITY er indstillet til Deaktiveret (standard). Brug følgende kode i forberedelsesafsnittet af testskabelonen:
-- ForberedelseSET NOCOUNT ON;BRUG tempdb; ALTER DATABASE tempdb SET DELAYED_DURABILITY =Deaktiveret; DROP TABEL HVIS FINDER dbo.T1; CREATE TABLE dbo.T1(col1 INT NOT NULL); DECLARE @db AS sysname =N'tempdb';
Brug følgende kode i selve arbejdsafsnittet:
-- Faktisk workDECLARE @i AS INT =1; WHILE @i <=1000000BEGIN BEGIN TRAN INSERT INTO dbo.T1(col1) VALUES(@i); FORBINDE TRAN; SET @i +=1;END;
Dette arbejde genererede 5.095 log skylninger, og det tog 19 sekunder at fuldføre. Det er sammenlignet med over en million log skylninger og 193 sekunder i en brugerdatabase med fuld holdbarhed. Det er endnu bedre end med forsinket holdbarhed i en brugerdatabase (95.407 log skylninger og 22 sekunder) på grund af den reducerede størrelse af logposterne.
For at teste en stor transaktion skal du lade forberedelsesafsnittet være uændret og bruge følgende kode i selve arbejdsafsnittet:
-- Faktisk arbejdeBEGIN TRAN; DEKLARE @i SOM INT =1; WHILE @i <=1000000BEGIN INSERT INTO dbo.T1(col1) VALUES(@i); SET @i +=1;SLUT; FORBIND OVERFØRSEL;
Jeg fik 1.228 log skylninger og 9 sekunders køretid. Det er sammenlignet med 1.758 log skylninger og 7 sekunders køretid i brugerdatabasen. Kørselstiden er ens, endda en smule hurtigere i brugerdatabasen, men det kan være små variationer mellem testene. Størrelsen på logposterne i tempdb er reduceret, og du får derfor færre log-flush sammenlignet med brugerdatabasen.
Du kan også prøve at køre testene med indstillingen DELAYED_DURABILITY sat til Forced, men dette vil ikke have nogen indflydelse i tempdb, da commit-hændelser, som nævnt, alligevel ikke udløser en log-flush i tempdb.
Her er ydeevnemålene for alle testene, både i brugerdatabasen og i tempdb:
databasens holdbarhed #transactions-log tømmer varighed i sekunder-------------------------------- ----- ---------- ------------ --------------------PerformanceV3 fuld 1000000 1000036 193PerformanceV3 fuld 1 1758 7PerformanceV3 forsinket 1000000 95407 22PerformanceV3 forsinket 1 1759 8tempdb fuld 1000000 5095 19tempdb fuld 1 1228 9tempdb forsinket 1000000 5091 18tempdb forsinkelse912>Caching af sekvensobjekt
Måske er et overraskende tilfælde, der udløser log buffer flushs, relateret til sekvensobjektets cache-indstilling. Betragt som eksempel følgende sekvensdefinition:
CREATE SEQUENCE dbo.Seq1 AS BIGINT MINVALUE 1 CACHE 50; -- standard cachestørrelsen er 50;Hver gang du har brug for en ny sekvensværdi, bruger du funktionen NEXT VALUE FOR, som sådan:
VÆLG NÆSTE VÆRDI FOR dbo.Seq1;CACHE-egenskaben er en præstationsfunktion. Uden det, hver gang der blev anmodet om en ny sekvensværdi, ville SQL Server have været nødt til at skrive den aktuelle værdi til disken til gendannelsesformål. Det er faktisk den adfærd, du får, når du bruger NO CACHE-tilstanden. I stedet, når indstillingen er indstillet til en værdi større end nul, skriver SQL Server kun en gendannelsesværdi til disken én gang for hvert antal anmodninger i cachestørrelse. SQL Server bevarer to medlemmer i hukommelsen, størrelse som sekvenstypen, en med den aktuelle værdi og en med antallet af værdier tilbage, før den næste diskskrivning af gendannelsesværdien er nødvendig. I tilfælde af strømsvigt, ved genstart, sætter SQL Server den aktuelle sekvensværdi til gendannelsesværdien.
Dette er nok meget nemmere at forklare med et eksempel. Overvej ovenstående sekvensdefinition med CACHE-indstillingen sat til 50 (standard). Du anmoder om en ny sekvensværdi for første gang ved at køre ovenstående SELECT-sætning. SQL Server indstiller de førnævnte medlemmer til følgende værdier:
På diskgendannelsesværdi:50, aktuel værdi i hukommelsen:1, værdier i hukommelsen tilbage:49, Du får:149 flere anmodninger vil ikke røre disken, snarere kun opdatere hukommelsesmedlemmerne. Efter 50 anmodninger i alt, sættes medlemmerne til følgende værdier:
På diskgendannelsesværdi:50, aktuel værdi i hukommelsen:50, værdier i hukommelsen tilbage:0, Du får:50Foretag endnu en anmodning om en ny sekvensværdi, og dette udløser en diskskrivning af gendannelsesværdien 100. Medlemmerne indstilles derefter til følgende værdier:
På diskgendannelsesværdi:100, aktuel værdi i hukommelsen:51, værdier i hukommelsen tilbage:49, Du får:51Hvis systemet på dette tidspunkt oplever et strømsvigt, efter genstart, sættes den aktuelle sekvensværdi til 100 (værdien gendannet fra disken). Den næste anmodning om en sekvensværdi frembringer 101 (skriver gendannelsesværdien 150 til disken). Du mistede alle værdier i intervallet 52 til 100. Det meste, du kan miste på grund af en uren afslutning af SQL Server-processen, som i tilfælde af strømsvigt, er lige så mange værdier som cachestørrelsen. Afvejningen er klar; jo større cachestørrelsen er, jo færre disken skriver af gendannelsesværdien, og dermed bedre ydeevne. Samtidig er det større mellemrum, der kan genereres mellem to sekvensværdier i tilfælde af strømsvigt.
Alt dette er ret ligetil, og måske er du udmærket bekendt med, hvordan det fungerer. Hvad der kan være overraskende er, at hver gang SQL Server skriver en ny gendannelsesværdi til disken (hver 50 anmodninger i vores eksempel), hærder den også logbufferen. Det er ikke tilfældet med egenskaben identitetskolonne, selvom SQL Server internt bruger den samme caching-funktion til identitet, som den gør for sekvensobjektet, lader den dig bare ikke kontrollere dens størrelse. Den er tændt som standard med størrelse 10000 for BIGINT og NUMERIC, 1000 for INT, 100 for SMALLINT og 10 for TINYINT. Hvis du vil, kan du slå det fra med sporingsflag 272 eller konfigurationsindstillingen IDENTITY_CACHE (2017+). Grunden til, at SQL Server ikke behøver at tømme logbufferen, når du skriver den identitetscache-relaterede gendannelsesværdi til disken, er, at en ny identitetsværdi kun kan oprettes, når en række indsættes i en tabel. I tilfælde af strømsvigt vil en række, der er indsat i en tabel af en transaktion, der ikke blev foretaget, blive trukket ud af tabellen som en del af databasegendannelsesprocessen, når systemet genstarter. Så selvom SQL Server efter genstart genererer den samme identitetsværdi som den, der blev oprettet i transaktionen, der ikke forpligtede, er der ingen chance for dubletter, da rækken blev trukket ud af bordet. Hvis transaktionen var begået, ville dette have udløst en log-flush, som også ville fortsætte med at skrive en cache-relateret gendannelsesværdi. Derfor følte Microsoft sig ikke tvunget til at tømme logbufferen, hver gang en identitetscache-relateret diskskrivning af gendannelsesværdien finder sted.
Med sekvensobjektet er situationen anderledes. En applikation kan anmode om en ny sekvensværdi og ikke gemme den i databasen. I tilfælde af et strømsvigt efter oprettelsen af en ny sekvensværdi i en transaktion, der ikke forpligtede sig, efter genstart, er der ingen måde for SQL Server at fortælle applikationen ikke at stole på denne værdi. For at undgå at oprette en ny sekvensværdi efter genstart, der er lig med en tidligere genereret sekvensværdi, tvinger SQL Server derfor en log-flush hver gang en ny sekvenscache-relateret gendannelsesværdi skrives til disken. En undtagelse fra denne regel er, når sekvensobjektet er oprettet i tempdb, selvfølgelig er der ikke behov for sådanne log flushes, da tempdb alligevel efter en systemgenstart oprettes igen.
En negativ præstationspåvirkning af de hyppige log-tømninger er især mærkbar, når der bruges en meget lille sekvenscachestørrelse og i en transaktion genererer masser af sekvensværdier, f.eks. når der indsættes masser af rækker i en tabel. Uden sekvensen ville en sådan transaktion for det meste hærde logbufferen, når den fyldes op, plus en gang mere, når transaktionen forpligtes. Men med sekvensen får du en log flush hver gang en diskskrivning af en gendannelsesværdi finder sted. Det er derfor, du vil undgå at bruge en lille cachestørrelse - for ikke at tale om NO CACHE-tilstanden.
For at demonstrere dette skal du bruge følgende kode i forberedelsesafsnittet i vores testskabelon:
-- ForberedelseSET NOCOUNT ON;USE PerformanceV3; -- prøv PerformanceV3, tempdb ALTER DATABASE PerformanceV3 -- prøv PerformanceV3, tempdb SET DELAYED_DURABILITY =Deaktiveret; -- prøv Disabled, Forced DROP TABLE HVIS FINDER dbo.T1; DROP SEQUENCE HVIS FINDER dbo.Seq1; OPRET SEKVENS dbo.Seq1 SOM BIGINT MINVALUE 1 CACHE 50; -- prøv NO CACHE, CACHE 50, CACHE 10000 DECLARE @db AS sysname =N'PerformanceV3'; -- prøv PerformanceV3, tempdbOg følgende kode i selve arbejdssektionen:
-- Faktisk workSELECT -- n -- for at teste uden seq NEXT VALUE FOR dbo.Seq1 AS n -- for at teste sequenceINTO dbo.T1FROM PerformanceV3.dbo.GetNums(1, 1000000) AS N;Denne kode bruger en transaktion til at skrive 1.000.000 rækker ind i en tabel ved hjælp af SELECT INTO-sætningen, der genererer lige så mange sekvensværdier som antallet af indsatte rækker.
Som anvist i kommentarerne, kør testen med NO CACHE, CACHE 50 og CACHE 10000, både i PerformanceV3 og i tempdb, og prøv både fuldt holdbare transaktioner og forsinkede holdbare.
Her er de præstationstal, jeg fik på mit system:
databasens holdbarhed cache-log tømmer varighed i sekunder-------------------------------- ------ --- ------------ --------------------PerformanceV3 fuld INGEN CACHE 1000047 171PerformanceV3 fuld 50 20008 4PerformanceV3 fuld 10000 339 <1tempdb FULL NO CACHE 96 4TEMPDB FULL 50 74 1TEMPDB Fuld 10000 8 <1Performancev3 Forsinket ingen cache 1000045 166Performancev3 Forsinket 50 20011 4performancev3 Forsinket 10000 334 <1TEMPDB forsinket ingen cache 91 4tempdb forsinket 50 74 1TEMPDB forsinket 10000 8 <1 /prepd>Der er en del interessante ting at bemærke.
Med NO CACHE får du en log flush for hver enkelt genereret sekvensværdi. Derfor anbefales det kraftigt at undgå det.
Med en lille sekvenscache-størrelse får du stadig masser af log flushes. Måske er situationen ikke så slem som med NO CACHE, men bemærk, at arbejdsbyrden tog 4 sekunder at fuldføre med standardcachestørrelsen på 50 sammenlignet med mindre end et sekund med størrelsen 10.000. Jeg bruger personligt 10.000 som min foretrukne værdi.
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.
Konklusion
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.