Database
 sql >> Baza danych >  >> RDS >> Database

Zrozumienie opróżnień bufora dziennika

Prawdopodobnie słyszałeś już wiele razy, że SQL Server zapewnia gwarancję właściwości transakcji ACID. Ten artykuł skupia się na części D, która oczywiście oznacza trwałość. Dokładniej, ten artykuł koncentruje się na aspekcie architektury rejestrowania programu SQL Server, który wymusza trwałość transakcji — opróżnianie bufora dziennika. Mówię o funkcji, jaką obsługuje bufor dziennika, warunkach, które zmuszają SQL Server do opróżniania bufora dziennika na dysk, o tym, co można zrobić, aby zoptymalizować wydajność transakcji, a także o ostatnio dodanych powiązanych technologiach, takich jak opóźniona trwałość i nieulotna pamięć klasy magazynu.

Dziennik opróżnień bufora

Część D we właściwościach transakcyjnych ACID oznacza trwałość. Na poziomie logicznym oznacza to, że kiedy aplikacja wysyła SQL Serverowi instrukcję zatwierdzenia transakcji (jawnie lub z transakcją auto-commit), SQL Server normalnie zwraca kontrolę do wywołującego tylko wtedy, gdy może zagwarantować, że transakcja jest trwała. Innymi słowy, gdy dzwoniący odzyska kontrolę po zatwierdzeniu transakcji, może ufać, że nawet jeśli chwilę później nastąpi awaria zasilania serwera, zmiany w transakcji trafiły do ​​bazy danych. Dopóki serwer zostanie pomyślnie zrestartowany, a pliki bazy danych nie zostaną uszkodzone, przekonasz się, że wszystkie zmiany transakcji zostały zastosowane.

Sposób, w jaki SQL Server wymusza trwałość transakcji, polega po części na upewnieniu się, że wszystkie zmiany transakcji są zapisywane w dzienniku transakcji bazy danych na dysku przed zwróceniem kontroli dzwoniącemu. W przypadku awarii zasilania po potwierdzeniu zatwierdzenia transakcji, wiesz, że wszystkie te zmiany zostały przynajmniej zapisane w dzienniku transakcji na dysku. Dzieje się tak, nawet jeśli powiązane strony danych zostały zmodyfikowane tylko w pamięci podręcznej danych (puli buforów), ale nie zostały jeszcze opróżnione do plików danych na dysku. Po ponownym uruchomieniu SQL Server, podczas fazy ponownego wykonywania procesu odzyskiwania, SQL Server wykorzystuje informacje zapisane w dzienniku do odtworzenia zmian, które zostały zastosowane po ostatnim punkcie kontrolnym i które nie zostały wprowadzone do plików danych. Jest trochę więcej w tej historii w zależności od używanego modelu odzyskiwania i od tego, czy operacje zbiorcze zostały zastosowane po ostatnim punkcie kontrolnym, ale na potrzeby naszej dyskusji wystarczy skupić się na części, która obejmuje utrwalenie zmian w dziennik transakcji.

Trudną częścią architektury rejestrowania SQL Server jest to, że zapisy dziennika są sekwencyjne. Gdyby SQL Server nie używał jakiegoś buforu dziennika w celu złagodzenia zapisów dziennika na dysku, systemy intensywnie zapisujące — zwłaszcza te, które obejmują wiele małych transakcji — szybko napotkałyby straszliwe wąskie gardła wydajności związane z zapisem dziennika.

Aby złagodzić negatywny wpływ na wydajność częstego sekwencyjnego zapisu dziennika na dysku, SQL Server używa buforu dziennika w pamięci. Zapisy dziennika są najpierw wykonywane w buforze dziennika, a pewne warunki powodują, że program SQL Server opróżnia lub wzmacnia bufor dziennika na dysk. Utwardzona jednostka (aka blok dziennika) może mieć zakres od minimalnego rozmiaru sektora (512 bajtów) do maksymalnie 60 KB. Poniżej przedstawiono warunki, które wyzwalają opróżnianie bufora dziennika (na razie zignoruj ​​części, które pojawiają się w nawiasach kwadratowych):

  • SQL Server otrzymuje żądanie zatwierdzenia [w pełni trwałej] transakcji, która zmienia dane [w bazie danych innej niż tempdb]
  • Bufor dziennika zapełnia się, osiągając pojemność 60 KB
  • SQL Server musi wzmocnić brudne strony danych, np. podczas procesu punktu kontrolnego, a rekordy dziennika reprezentujące zmiany na tych stronach nie zostały jeszcze wzmocnione (rejestrowanie zapisu z wyprzedzeniem lub w skrócie WAL)
  • Ręcznie żądasz opróżnienia bufora dziennika, wykonując procedurę sys.sp_flush_log
  • SQL Server zapisuje nową wartość odzyskiwania związaną z pamięcią podręczną sekwencji [w bazie danych innej niż tempdb]

Pierwsze cztery warunki powinny być całkiem jasne, jeśli na razie zignorujesz informacje w nawiasach kwadratowych. To ostatnie być może nie jest jeszcze jasne, ale wyjaśnię to szczegółowo w dalszej części artykułu.

Czas oczekiwania serwera SQL na zakończenie operacji we/wy obsługującej opróżnianie bufora dziennika jest odzwierciedlany przez typ oczekiwania WRITELOG.

Dlaczego więc te informacje są tak interesujące i co z nimi robimy? Zrozumienie warunków, które wyzwalają opróżnianie bufora dziennika, może pomóc w ustaleniu, dlaczego niektóre obciążenia napotykają związane z nimi wąskie gardła. Ponadto w niektórych przypadkach można podjąć działania w celu zmniejszenia lub wyeliminowania takich wąskich gardeł. Omówię wiele przykładów, takich jak jedna duża transakcja kontra wiele małych transakcji, transakcje w pełni trwałe kontra opóźnione transakcje trwałe, baza danych użytkowników kontra tempdb i buforowanie obiektów sekwencji.

Jedna duża transakcja kontra wiele małych transakcji

Jak wspomniano, jednym z warunków wyzwalających opróżnianie bufora dziennika jest zatwierdzenie transakcji w celu zagwarantowania trwałości transakcji. Oznacza to, że obciążenia, które obejmują wiele małych transakcji, takie jak obciążenia OLTP, mogą potencjalnie napotkać wąskie gardła związane z zapisem dziennika.

Chociaż często tak nie jest, jeśli podczas jednej sesji przesyłasz wiele małych zmian, prostym i skutecznym sposobem optymalizacji pracy jest zastosowanie zmian w jednej dużej transakcji zamiast wielu małych.

Rozważ następujący uproszczony przykład (pobierz PerformanceV3 tutaj):

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;

Ten kod wykonuje 1 000 000 małych transakcji, które zmieniają dane w bazie danych użytkownika. Ta praca wyzwoli co najmniej 1 000 000 opróżnień bufora dziennika. Możesz uzyskać kilka dodatkowych z powodu zapełnienia bufora dziennika. Możesz użyć następującego szablonu testu, aby policzyć liczbę opróżnień bufora dziennika i zmierzyć czas potrzebny na ukończenie pracy:

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

Mimo że nazwa licznika wydajności to Opróżnienia dziennika/s, w rzeczywistości gromadzi on dotychczas liczbę opróżnień bufora dziennika. Tak więc kod odejmuje liczbę przed pracą od liczby po pracy, aby obliczyć liczbę spłukiwania dziennika wygenerowanego przez pracę. Ten kod mierzy również czas w sekundach, jaki zajęło ukończenie pracy. Nawet jeśli nie robię tego tutaj, możesz, jeśli chcesz, podobnie obliczyć liczbę rekordów dziennika i rozmiar zapisywanych w dzienniku przez pracę, sprawdzając stany przed pracą i po pracy w fn_dblog funkcja.

W powyższym przykładzie poniżej znajduje się część, którą należy umieścić w sekcji przygotowania szablonu testu:

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

Poniżej znajduje się część, którą musisz umieścić w sekcji prac rzeczywistych:

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

W sumie otrzymujesz następujący kod:

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

Ukończenie tego kodu w moim systemie zajęło 193 sekundy i wywołało 1 000 036 opróżnień bufora dziennika. To bardzo powolne, ale można to wytłumaczyć dużą liczbą spłukiwania kłód.

W typowych obciążeniach OLTP różne sesje przesyłają jednocześnie małe zmiany w różnych małych transakcjach, więc nie jest tak, że naprawdę masz możliwość enkapsulacji wielu małych zmian w jednej dużej transakcji. Jeśli jednak Twoja sytuacja jest taka, że ​​wszystkie drobne zmiany są przesyłane z tej samej sesji, prostym sposobem na zoptymalizowanie pracy jest zawarcie jej w jednej transakcji. Daje to dwie główne korzyści. Jednym z nich jest to, że twoja praca zapisze mniej rekordów dziennika. Przy 1 000 000 małych transakcji każda transakcja zapisuje w rzeczywistości trzy rekordy dziennika:jeden dla rozpoczęcia transakcji, jeden dla zmiany i jeden dla zatwierdzenia transakcji. Tak więc patrzysz na około 3 000 000 rekordów dziennika transakcji w porównaniu z nieco ponad 1 000 000, gdy są wykonywane jako jedna duża transakcja. Ale co ważniejsze, przy jednej dużej transakcji większość opróżnień dziennika jest wyzwalana tylko wtedy, gdy bufor dziennika się zapełni, plus jeszcze jedno opróżnianie dziennika na samym końcu transakcji, gdy zostanie ona zatwierdzona. Różnica w wydajności może być dość znaczna. Aby przetestować pracę w jednej dużej transakcji, użyj następującego kodu w rzeczywistej części szablonu testu:

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

W moim systemie ta praca zakończyła się w 7 sekund i spowodowała 1758 opróżnień dziennika. Oto porównanie dwóch opcji:

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

Ale znowu, w typowych obciążeniach OLTP tak naprawdę nie masz możliwości zastąpienia wielu małych transakcji przesłanych z różnych sesji jedną dużą transakcją przesłaną z tej samej sesji.

W pełni trwałe a opóźnione trwałe transakcje

Począwszy od programu SQL Server 2014, można użyć funkcji zwanej opóźnioną trwałością, która umożliwia zwiększenie wydajności obciążeń z wieloma małymi transakcjami, nawet jeśli są one przesyłane przez różne sesje, przez poświęcenie normalnej pełnej gwarancji trwałości. Podczas zatwierdzania opóźnionej trwałej transakcji SQL Server potwierdza zatwierdzenie, gdy tylko rekord dziennika zatwierdzenia zostanie zapisany w buforze dziennika, bez wyzwalania opróżniania bufora dziennika. Bufor dziennika jest opróżniany z powodu któregokolwiek z wyżej wymienionych warunków, na przykład gdy się zapełnia, ale nie w przypadku zatwierdzenia opóźnionej trwałej transakcji.

Zanim skorzystasz z tej funkcji, musisz bardzo dokładnie zastanowić się, czy jest ona dla Ciebie odpowiednia. Pod względem wydajności jego wpływ jest znaczący tylko w przypadku obciążeń z dużą ilością małych transakcji. Jeśli na początku Twoje obciążenie pracą obejmuje głównie duże transakcje, prawdopodobnie nie zobaczysz żadnej przewagi wydajności. Co ważniejsze, musisz zdać sobie sprawę z możliwości utraty danych. Załóżmy, że aplikacja dokonuje opóźnionej trwałej transakcji. Rekord zatwierdzenia jest zapisywany w buforze dziennika i natychmiast potwierdzany (kontrola zwrócona wywołującemu). Jeśli SQL Server doświadczy awarii zasilania przed opróżnieniem bufora dziennika, po ponownym uruchomieniu proces odzyskiwania cofa wszystkie zmiany wprowadzone przez transakcję, nawet jeśli aplikacja uważa, że ​​została zatwierdzona.

Kiedy więc można korzystać z tej funkcji? Jednym z oczywistych przypadków jest to, że utrata danych nie stanowi problemu, jak w tym przykładzie Melissa Connors z SentryOne. Innym jest, gdy po ponownym uruchomieniu masz środki, aby zidentyfikować, które zmiany nie zostały wprowadzone do bazy danych i możesz je odtworzyć. Jeśli Twoja sytuacja nie należy do jednej z tych dwóch kategorii, nie używaj tej funkcji pomimo pokusy.

Aby pracować z opóźnionymi trwałymi transakcjami, musisz ustawić opcję bazy danych o nazwie DELAYED_DURABILITY. Tę opcję można ustawić na jedną z trzech wartości:

  • Wyłączone (domyślnie):wszystkie transakcje w bazie danych są w pełni trwałe, dlatego każde zatwierdzenie powoduje opróżnienie bufora dziennika
  • Wymuszone :wszystkie transakcje w bazie danych są opóźnione i trwałe, dlatego zatwierdzenia nie powodują opróżnienia bufora dziennika
  • Dozwolone :o ile nie zaznaczono inaczej, transakcje są w pełni trwałe i ich zatwierdzenie powoduje opróżnienie bufora dziennika; jednak jeśli użyjesz opcji DELAYED_DURABILITY =ON w instrukcji COMMIT TRAN lub bloku atomowym (natywnie skompilowanej procedury), ta konkretna transakcja jest opóźniona i dlatego jej zatwierdzenie nie wyzwala opróżniania bufora dziennika

Jako test użyj następującego kodu w sekcji przygotowania naszego szablonu testu (zwróć uwagę, że opcja bazy danych jest ustawiona na Wymuszone):

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

I użyj następującego kodu w sekcji rzeczywista praca (uwaga, 1 000 000 małych transakcji):

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

Alternatywnie możesz użyć trybu Dozwolone na poziomie bazy danych, a następnie w poleceniu COMMIT TRAN dodać WITH (DELAYED_DURABILITY =ON).

W moim systemie praca trwała 22 sekundy i spowodowała 95 407 opróżnień dziennika. To dłużej niż uruchamianie pracy jako jednej dużej transakcji (7 sekund), ponieważ generowanych jest więcej rekordów dziennika (pamiętaj, na transakcję, jeden dla rozpoczęcia transakcji, jeden dla zmiany i jeden dla zatwierdzenia transakcji); jednak jest to znacznie szybsze niż 193 sekundy, które zajęło ukończenie pracy przy użyciu 1 000 000 w pełni trwałych transakcji, ponieważ liczba opróżnień dziennika spadła z ponad 1 000 000 do mniej niż 100 000. Dodatkowo, dzięki opóźnionej trwałości, uzyskasz wzrost wydajności, nawet jeśli transakcje są przesyłane z różnych sesji, w których nie ma możliwości użycia jednej dużej transakcji.

Aby zademonstrować, że korzystanie z opóźnionej trwałości podczas wykonywania pracy jako dużych transakcji nie przynosi żadnych korzyści, zachowaj ten sam kod w części przygotowania ostatniego testu i użyj następującego kodu w części rzeczywistej pracy:

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

Uzyskałem 8 sekund czasu działania (w porównaniu do 7 dla jednej dużej w pełni trwałej transakcji) i 1759 opróżnień dziennika (w porównaniu do 1758). Liczby są zasadniczo takie same, ale w przypadku opóźnionej trwałej transakcji istnieje ryzyko utraty danych.

Oto podsumowanie wyników dla wszystkich czterech testów:

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

Pamięć klasy pamięci

Funkcja opóźnionej trwałości może znacznie poprawić wydajność obciążeń w stylu OLTP, które obejmują dużą liczbę małych transakcji aktualizacji, które wymagają wysokiej częstotliwości i małych opóźnień. Problem polega na tym, że ryzykujesz utratę danych. Co zrobić, jeśli nie możesz pozwolić na utratę danych, ale nadal chcesz uzyskać wzrost wydajności podobny do opóźnionej trwałości, w którym bufor dziennika nie jest opróżniany po każdym zatwierdzeniu, a raczej po jego zapełnieniu? Wszyscy lubimy jeść ciasto i też je mamy, prawda?

Można to osiągnąć w programie SQL Server 2016 z dodatkiem SP1 lub nowszym, używając pamięci klasy magazynu, czyli magazynu nieulotnego NVDIMM-N. Ten sprzęt jest zasadniczo modułem pamięci zapewniającym wydajność na poziomie pamięci, ale zawarte tam informacje są utrwalane i dlatego nie są tracone po zaniku zasilania. Dodatek w dodatku SP1 dla programu SQL Server 2016 umożliwia skonfigurowanie buforu dziennika jako utrwalonego na takim sprzęcie. Aby to zrobić, skonfiguruj SCM jako wolumin w systemie Windows i sformatuj go jako wolumin w trybie bezpośredniego dostępu (DAX). Następnie dodajesz plik dziennika do bazy danych za pomocą normalnego polecenia ALTER DATABASE ADD LOG FILE, ze ścieżką do pliku rezydującą w woluminie DAX i ustawiając rozmiar na 20 MB. Z kolei SQL Server rozpoznaje, że jest woluminem DAX i od tego momentu traktuje bufor dziennika jako utrwalony na tym woluminie. Zdarzenia zatwierdzenia transakcji nie wyzwalają już opróżniania bufora dziennika, a gdy zatwierdzenie zostało zapisane w buforze dziennika, SQL Server wie, że jest faktycznie utrwalone, i dlatego zwraca kontrolę do wywołującego. Kiedy bufor dziennika się zapełni, SQL Server opróżnia go do plików dziennika transakcji w tradycyjnym magazynie.

Aby uzyskać więcej informacji na temat tej funkcji, w tym liczby dotyczące wydajności, zobacz Przyspieszenie opóźnienia transakcji przy użyciu pamięci klasy magazynu w systemie Windows Server 2016/SQL Server 2016 z dodatkiem SP1 autorstwa Kevina Farlee.

Co ciekawe, SQL Server 2019 rozszerza obsługę pamięci klasy magazynu poza scenariusz utrwalonej pamięci podręcznej dzienników. Obsługuje umieszczanie plików danych, plików dziennika i plików punktów kontrolnych OLTP w pamięci na takim sprzęcie. Wszystko, co musisz zrobić, to udostępnić go jako wolumin na poziomie systemu operacyjnego i sformatować jako dysk DAX. SQL Server 2019 automatycznie rozpoznaje tę technologię i działa w oświeconej tryb, bezpośredni dostęp do urządzenia, z pominięciem stosu pamięci systemu operacyjnego. Witamy w przyszłości!

Baza danych użytkowników a tempdb

Baza danych tempdb jest oczywiście tworzona od podstaw jako świeża kopia bazy danych modelu za każdym razem, gdy ponownie uruchamiasz SQL Server. W związku z tym nigdy nie ma potrzeby odzyskiwania danych, które zapisujesz w tempdb, niezależnie od tego, czy zapisujesz je do tabel tymczasowych, zmiennych tabel lub tabel użytkowników. Po ponownym uruchomieniu wszystko zniknęło. Wiedząc o tym, SQL Server może złagodzić wiele wymagań związanych z rejestrowaniem. Na przykład, niezależnie od tego, czy włączysz opcję opóźnionej trwałości, czy nie, zdarzenia zatwierdzenia nie powodują opróżniania bufora dziennika. Co więcej, ilość informacji, które muszą być rejestrowane, jest zmniejszona, ponieważ SQL Server potrzebuje tylko wystarczającej ilości informacji do obsługi wycofywania transakcji lub cofania pracy, jeśli to konieczne, ale bez przewijania transakcji do przodu lub ponownego wykonywania pracy. W rezultacie rekordy dziennika transakcji reprezentujące zmiany obiektu w tempdb są zwykle mniejsze w porównaniu do sytuacji, gdy ta sama zmiana jest stosowana do obiektu w bazie danych użytkownika.

Aby to zademonstrować, uruchomisz te same testy, które uruchamiałeś wcześniej w PerformanceV3, tylko tym razem w tempdb. Zaczniemy od testowania wielu małych transakcji, gdy opcja bazy danych DELAYED_DURABILITY jest ustawiona na Disabled (domyślnie). Użyj następującego kodu w sekcji przygotowania szablonu testu:

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

Użyj następującego kodu w sekcji pracy rzeczywistej:

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

Ta praca wygenerowała 5095 opróżnień dziennika, a jej ukończenie zajęło 19 sekund. W porównaniu z ponad milionem opróżnień logów i 193 sekundami w bazie danych użytkowników o pełnej trwałości. To nawet lepiej niż w przypadku opóźnionej trwałości w bazie danych użytkowników (95 407 opróżnień dziennika i 22 sekundy) ze względu na zmniejszony rozmiar rekordów dziennika.

Aby przetestować jedną dużą transakcję, pozostaw sekcję przygotowania niezmienioną i użyj następującego kodu w sekcji pracy rzeczywistej:

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

Mam 1228 log flushes i 9 sekund czasu pracy. W porównaniu z 1758 opróżnieniami dzienników i 7 sekundowym czasem działania w bazie danych użytkowników. Czas działania jest podobny, nawet nieco szybszy w bazie danych użytkownika, ale mogą to być niewielkie różnice między testami. Rozmiary rekordów dziennika w tempdb są zmniejszone, a zatem uzyskuje się mniej opróżnień dziennika w porównaniu z bazą danych użytkowników.

Możesz również spróbować uruchomić testy z opcją DELAYED_DURABILITY ustawioną na Wymuszone, ale nie będzie to miało wpływu na tempdb, ponieważ, jak wspomniano, zdarzenia zatwierdzenia i tak nie wywołują opróżniania dziennika w tempdb.

Oto miary wydajności dla wszystkich testów, zarówno w bazie danych użytkownika, jak i w 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

Buforowanie obiektów sekwencji

Być może zaskakujący przypadek, który wyzwala opróżnianie bufora dziennika, jest związany z opcją pamięci podręcznej obiektu sekwencji. Rozważmy jako przykład następującą definicję sekwencji:

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

Za każdym razem, gdy potrzebujesz nowej wartości sekwencji, używasz funkcji NEXT VALUE FOR, tak jak na przykład:

SELECT NEXT VALUE FOR dbo.Seq1;

Właściwość CACHE jest funkcją wydajności. Bez niego za każdym razem, gdy żądano nowej wartości sekwencji, SQL Server musiałby zapisać bieżącą wartość na dysku w celu odzyskania. Rzeczywiście, jest to zachowanie, które uzyskujesz podczas korzystania z trybu NO CACHE. Zamiast tego, gdy opcja jest ustawiona na wartość większą niż zero, SQL Server zapisuje wartość odzyskiwania na dysku tylko raz na każdą liczbę żądań rozmiaru pamięci podręcznej. SQL Server przechowuje w pamięci dwa elementy członkowskie o rozmiarze odpowiadającym typowi sekwencji, jeden przechowujący bieżącą wartość, a drugi przechowujący liczbę wartości pozostałych do następnego zapisu na dysku wartości odzyskiwania. W przypadku awarii zasilania, po ponownym uruchomieniu, SQL Server ustawia bieżącą wartość sekwencji na wartość odzyskiwania.

Zapewne łatwiej to wytłumaczyć na przykładzie. Rozważ powyższą definicję sekwencji z opcją CACHE ustawioną na 50 (domyślnie). Po raz pierwszy żądasz nowej wartości sekwencji, uruchamiając powyższą instrukcję SELECT. SQL Server ustawia wyżej wymienione elementy na następujące wartości:

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

Jeszcze 49 żądań nie dotknie dysku, a jedynie zaktualizuje członków pamięci. Po łącznie 50 żądaniach członkowie otrzymują następujące wartości:

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

Złóż kolejne żądanie nowej wartości sekwencji, a spowoduje to zapis na dysku wartości odzyskiwania 100. Elementy członkowskie są następnie ustawiane na następujące wartości:

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

Jeśli w tym momencie w systemie wystąpi awaria zasilania, po ponownym uruchomieniu bieżąca wartość sekwencji zostanie ustawiona na 100 (wartość odzyskana z dysku). Następne żądanie wartości sekwencji generuje 101 (zapis wartości odzyskiwania 150 na dysku). Straciłeś wszystkie wartości z zakresu od 52 do 100. Najwięcej możesz stracić z powodu nieprawidłowego zakończenia procesu SQL Server, jak w przypadku awarii zasilania, to tyle wartości, ile wynosi rozmiar pamięci podręcznej. Kompromis jest jasny; im większy rozmiar pamięci podręcznej, tym mniej zapisów na dysku wartości odzyskiwania, a tym samym lepsza wydajność. Jednocześnie, tym większa luka, która może zostać wygenerowana między dwiema wartościami sekwencji w przypadku awarii zasilania.

Wszystko to jest całkiem proste i być może dobrze znasz, jak to działa. Zaskakujące może być to, że za każdym razem, gdy SQL Server zapisuje na dysku nową wartość odzyskiwania (w naszym przykładzie co 50 żądań), utwardza ​​bufor dziennika. Nie jest tak w przypadku właściwości kolumny tożsamości, chociaż SQL Server wewnętrznie używa tej samej funkcji buforowania tożsamości, co w przypadku obiektu sekwencji, po prostu nie pozwala kontrolować jego rozmiaru. Domyślnie jest włączony w rozmiarze 10000 dla BIGINT i NUMERIC, 1000 dla INT, 100 dla SMALLINT i 10 dla TINYINT. Jeśli chcesz, możesz go wyłączyć za pomocą flagi śledzenia 272 lub opcji konfiguracji z zakresem IDENTITY_CACHE (2017+). Powodem, dla którego program SQL Server nie musi opróżniać bufora dziennika podczas zapisywania wartości odzyskiwania związanej z pamięcią podręczną tożsamości na dysk, jest to, że nową wartość tożsamości można utworzyć tylko podczas wstawiania wiersza do tabeli. W przypadku awarii zasilania wiersz wstawiony do tabeli przez niezatwierdzoną transakcję zostanie usunięty z tabeli w ramach procesu odzyskiwania bazy danych po ponownym uruchomieniu systemu. Tak więc, nawet jeśli po ponownym uruchomieniu SQL Server wygeneruje taką samą wartość tożsamości, jak ta utworzona w transakcji, która nie została zatwierdzona, nie ma szans na duplikaty, ponieważ wiersz został wyciągnięty z tabeli. Gdyby transakcja została zatwierdzona, spowodowałoby to opróżnienie dziennika, co spowodowałoby również utrwalenie zapisu wartości odzyskiwania związanej z pamięcią podręczną. Dlatego Microsoft nie czuł się zmuszony do opróżniania bufora dziennika za każdym razem, gdy ma miejsce zapis wartości odzyskiwania na dysku związanym z pamięcią podręczną tożsamości.

W przypadku obiektu sekwencyjnego sytuacja jest inna. Aplikacja może zażądać nowej wartości sekwencji i nie przechowywać jej w bazie danych. W przypadku awarii zasilania po utworzeniu nowej wartości sekwencji w transakcji, która nie została zatwierdzona, po ponownym uruchomieniu nie ma możliwości, aby SQL Server powiedział aplikacji, aby nie polegała na tej wartości. W związku z tym, aby uniknąć tworzenia nowej wartości sekwencji po ponownym uruchomieniu, która jest równa poprzednio wygenerowanej wartości sekwencji, program SQL Server wymusza opróżnianie dziennika za każdym razem, gdy na dysku jest zapisywana nowa wartość odzyskiwania związana z pamięcią podręczną sekwencji. Jedynym wyjątkiem od tej reguły jest sytuacja, gdy obiekt sekwencji jest tworzony w tempdb, oczywiście nie ma potrzeby takiego opróżniania dziennika, ponieważ i tak po ponownym uruchomieniu systemu tempdb jest tworzona od nowa.

Negatywny wpływ na wydajność częstego opróżniania dziennika jest szczególnie zauważalny przy użyciu bardzo małego rozmiaru pamięci podręcznej sekwencji i podczas jednej transakcji generującej wiele wartości sekwencji, np. podczas wstawiania wielu wierszy do tabeli. Bez sekwencji taka transakcja w większości utwardziłaby bufor dziennika, gdy się zapełni, plus jeszcze jeden raz, gdy transakcja zostanie zatwierdzona. Ale dzięki sekwencji otrzymujesz opróżnianie dziennika za każdym razem, gdy ma miejsce zapis na dysku wartości odzyskiwania. Dlatego chcesz uniknąć używania małego rozmiaru pamięci podręcznej — nie mówiąc już o trybie NO CACHE.

Aby to zademonstrować, użyj następującego kodu w sekcji przygotowania naszego szablonu testowego:

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

Oraz następujący kod w dziale rzeczywista praca:

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

Ten kod wykorzystuje jedną transakcję do zapisania 1 000 000 wierszy w tabeli za pomocą instrukcji SELECT INTO, generując tyle wartości sekwencji, ile jest wstawionych wierszy.

Zgodnie z instrukcją w komentarzach uruchom test z NO CACHE, CACHE 50 i CACHE 10000, zarówno w PerformanceV3, jak i w tempdb, i wypróbuj zarówno w pełni trwałe transakcje, jak i opóźnione transakcje trwałe.

Oto numery wydajności, które uzyskałem w moim systemie:

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

Jest kilka interesujących rzeczy do zauważenia.

Z NO CACHE otrzymujesz opróżnianie dziennika dla każdej wygenerowanej wartości sekwencji. Dlatego zdecydowanie zaleca się, aby tego unikać.

Przy małym rozmiarze pamięci podręcznej sekwencji nadal otrzymujesz wiele opróżnień dziennika. Być może sytuacja nie jest tak zła jak w przypadku NO CACHE, ale zauważ, że przy domyślnym rozmiarze pamięci podręcznej wynoszącym 50, obciążenie zajęło 4 sekundy, w porównaniu do mniej niż sekundy przy rozmiarze 10 000. Osobiście używam 10 000 jako preferowanej wartości.

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.

Wniosek

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. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Uważaj na wprowadzające w błąd dane z SET STATISTICS IO

  2. Skany zleceń alokacji

  3. Pierwsze kroki z Cloud Firestore na iOS

  4. Wskazówki dotyczące Wszechświata

  5. Ograniczanie ryzyka związanego z danymi poprzez maskowanie danych