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

Serializowanie usunięć z klastrowanych indeksów magazynu kolumn

W Stack Overflow mamy kilka tabel korzystających z klastrowych indeksów magazynu kolumn, które działają świetnie w przypadku większości naszego obciążenia. Ale ostatnio natknęliśmy się na sytuację, w której „perfekcyjne burze” — wiele procesów, które próbują usunąć z tego samego CCI — przeciążyłyby procesor, ponieważ wszystkie działały równolegle i walczyły o dokończenie operacji. Oto jak to wyglądało w SolarWinds SQL Sentry:

A oto interesujące oczekiwania związane z tymi zapytaniami:

Wszystkie konkurujące zapytania miały następującą formę:

DELETE dbo.LargeColumnstoreTable WHERE col1 = @p1 AND col2 = @p2;

Plan wyglądał tak:

A ostrzeżenie na skanie poinformowało nas o dość ekstremalnych szczątkowych I/O:

Tabela ma 1,9 miliarda wierszy, ale ma tylko 32 GB (dziękujemy, pamięć kolumnowa!). Mimo to każde usuwanie pojedynczych wierszy zajęłoby od 10 do 15 sekund, przy czym większość tego czasu poświęcono na SOS_SCHEDULER_YIELD .

Na szczęście, ponieważ w tym scenariuszu operacja usuwania może być asynchroniczna, byliśmy w stanie rozwiązać problem za pomocą dwóch zmian (choć tutaj rażąco upraszczam):

  • Ograniczyliśmy MAXDOP na poziomie bazy danych, więc te usunięcia nie mogą przebiegać tak równolegle
  • Poprawiliśmy serializację procesów pochodzących z aplikacji (w zasadzie kolejkowaliśmy usunięcia przez jednego dyspozytora)

Jako DBA możemy łatwo kontrolować MAXDOP , chyba że zostanie zastąpione na poziomie zapytania (kolejna królicza nora na kolejny dzień). Niekoniecznie możemy kontrolować aplikację w takim zakresie, zwłaszcza jeśli jest ona dystrybuowana lub nie nasza. Jak możemy serializować zapisy w tym przypadku bez drastycznej zmiany logiki aplikacji?

Próbna konfiguracja

Nie zamierzam tworzyć lokalnie dwumiliardowej tabeli — nie mówiąc już o dokładnej tabeli — ale możemy przybliżyć coś na mniejszą skalę i spróbować odtworzyć ten sam problem.

Załóżmy, że to jest SuggestedEdits stół (w rzeczywistości tak nie jest). Ale jest to łatwy przykład do użycia, ponieważ możemy pobrać schemat z Eksploratora danych Stack Exchange. Używając tego jako podstawy, możemy utworzyć równoważną tabelę (z kilkoma drobnymi zmianami, aby ułatwić wypełnianie) i rzucić na nią klastrowany indeks magazynu kolumn:

CREATE TABLE dbo.FakeSuggestedEdits
(
  Id            int IDENTITY(1,1),
  PostId        int NOT NULL DEFAULT CONVERT(int, ABS(CHECKSUM(NEWID()))) % 200,
  CreationDate  datetime2 NOT NULL DEFAULT sysdatetime(),
  ApprovalDate  datetime2 NOT NULL DEFAULT sysdatetime(),
  RejectionDate datetime2 NULL,
  OwnerUserId   int NOT NULL DEFAULT 7,
  Comment       nvarchar (800)   NOT NULL DEFAULT NEWID(),
  Text          nvarchar (max)   NOT NULL DEFAULT NEWID(),
  Title         nvarchar (250)   NOT NULL DEFAULT NEWID(),
  Tags          nvarchar (250)   NOT NULL DEFAULT NEWID(),
  RevisionGUID  uniqueidentifier NOT NULL DEFAULT NEWSEQUENTIALID(),
  INDEX CCI_FSE CLUSTERED COLUMNSTORE
);

Aby wypełnić go 100 milionami wierszy, możemy skrzyżować złącze sys.all_objects i sys.all_columns pięć razy (w moim systemie za każdym razem wygeneruje to 2,68 miliona wierszy, ale YMMV):

-- 2680350 * 5 ~ 3 minutes
 
INSERT dbo.FakeSuggestedEdits(CreationDate)
  SELECT TOP (10) /*(2000000) */ modify_date
  FROM sys.all_objects AS o
  CROSS JOIN sys.columns AS c;
GO 5

Następnie możemy sprawdzić spację:

EXEC sys.sp_spaceused @objname = N'dbo.FakeSuggestedEdits';

To tylko 1,3 GB, ale to powinno wystarczyć:

Naśladowanie usuwania naszego klastrowanego magazynu kolumn

Oto proste zapytanie z grubsza pasujące do tego, co nasza aplikacja robi z tabelą:

DECLARE @p1 int = ABS(CHECKSUM(NEWID())) % 10000000, @p2 int = 7;
DELETE dbo.FakeSuggestedEdits WHERE Id = @p1 AND OwnerUserId = @p2;

Plan nie jest jednak idealnie dopasowany:

Aby zmusić go do równoległego działania i wygenerowania podobnej rywalizacji na moim skromnym laptopie, musiałem trochę zmusić optymalizator do tej wskazówki:

OPTION (QUERYTRACEON 8649);

Teraz wygląda dobrze:

Odtworzenie problemu

Następnie możemy stworzyć gwałtowny wzrost jednoczesnej aktywności usuwania za pomocą SqlStressCmd, aby usunąć 1000 losowych wierszy przy użyciu 16 i 32 wątków:

sqlstresscmd -s docs/ColumnStore.json -t 16
sqlstresscmd -s docs/ColumnStore.json -t 32

Możemy zaobserwować obciążenie procesora:

Obciążenie procesora trwa odpowiednio przez około 64 i 130 sekund:

Uwaga:dane wyjściowe z SQLQueryStress są czasami nieco nieaktualne w iteracjach, ale potwierdziłem, że praca, o którą prosisz, została wykonana precyzyjnie.

Potencjalne obejście:kolejka usuwania

Początkowo myślałem o wprowadzeniu do bazy danych tabeli kolejek, której moglibyśmy użyć do odciążenia czynności usuwania:

CREATE TABLE dbo.SuggestedEditDeleteQueue
(
  QueueID       int IDENTITY(1,1) PRIMARY KEY,
  EnqueuedDate  datetime2 NOT NULL DEFAULT sysdatetime(),
  ProcessedDate datetime2 NULL,
  Id            int NOT NULL,
  OwnerUserId   int NOT NULL
);

Wszystko, czego potrzebujemy, to wyzwalacz INSTEAD OF, aby przechwycić te nieuczciwe usunięcia pochodzące z aplikacji i umieścić je w kolejce do przetwarzania w tle. Niestety, nie można utworzyć wyzwalacza w tabeli z klastrowanym indeksem magazynu kolumn:

Komunikat 35358, poziom 16, stan 1
CREATE TRIGGER w tabeli „dbo.FakeSuggestedEdits” nie powiódł się, ponieważ nie można utworzyć wyzwalacza w tabeli z klastrowanym indeksem magazynu kolumn. Rozważ wymuszenie logiki wyzwalacza w inny sposób lub jeśli musisz użyć wyzwalacza, zamiast tego użyj stosu lub indeksu B-drzewa.

Będziemy potrzebować minimalnej zmiany w kodzie aplikacji, tak aby wywołała ona procedurę składowaną do obsługi usuwania:

CREATE PROCEDURE dbo.DeleteSuggestedEdit
  @Id          int,
  @OwnerUserId int
AS
BEGIN
  SET NOCOUNT ON;
 
  DELETE dbo.FakeSuggestedEdits 
    WHERE Id = @Id AND OwnerUserId = @OwnerUserId;
END

To nie jest stan stały; to tylko po to, aby zachować to samo zachowanie, zmieniając tylko jedną rzecz w aplikacji. Po zmianie aplikacji i pomyślnym wywołaniu tej procedury składowanej zamiast wysyłania zapytań usuwania ad hoc, procedura składowana może ulec zmianie:

CREATE PROCEDURE dbo.DeleteSuggestedEdit
  @Id          int,
  @OwnerUserId int
AS
BEGIN
  SET NOCOUNT ON;
 
  INSERT dbo.SuggestedEditDeleteQueue(Id, OwnerUserId)
    SELECT @Id, @OwnerUserId;
END

Testowanie wpływu kolejki

Teraz, jeśli zmienimy SqlQueryStress na wywołanie procedury składowanej:

DECLARE @p1 int = ABS(CHECKSUM(NEWID())) % 10000000, @p2 int = 7;
EXEC dbo.DeleteSuggestedEdit @Id = @p1, @OwnerUserId = @p2;

I prześlij podobne partie (umieszczając 16 tys. lub 32 tys. wierszy w kolejce):

DECLARE @p1 int = ABS(CHECKSUM(NEWID())) % 10000000, @p2 int = 7;
EXEC dbo.@Id = @p1 AND OwnerUserId = @p2;

Wpływ procesora jest nieco wyższy:

Jednak zadania kończą się znacznie szybciej — odpowiednio 16 i 23 sekundy:

Jest to znaczne zmniejszenie bólu odczuwanego przez aplikacje, gdy wchodzą w okresy wysokiej współbieżności.

Nadal musimy wykonać usuwanie, chociaż

Nadal musimy przetwarzać te usunięcia w tle, ale teraz możemy wprowadzić grupowanie i mieć pełną kontrolę nad szybkością i wszelkimi opóźnieniami, które chcemy wprowadzić między operacjami. Oto bardzo podstawowa struktura procedury składowanej do przetwarzania kolejki (co prawda bez pełnej kontroli transakcji, obsługi błędów lub czyszczenia tabeli kolejki):

CREATE PROCEDURE dbo.ProcessSuggestedEditQueue
  @JobSize        int = 10000,
  @BatchSize      int = 100,
  @DelayInSeconds int = 2      -- must be between 1 and 59
AS
BEGIN
  SET NOCOUNT ON;
 
  DECLARE @d TABLE(Id int, OwnerUserId int);
  DECLARE @rc int = 1,
          @jc int = 0, 
          @wf nvarchar(100) = N'WAITFOR DELAY ' + CHAR(39) 
              + '00:00:' + RIGHT('0' + CONVERT(varchar(2), 
                @DelayInSeconds), 2) + CHAR(39);
 
  WHILE @rc > 0 AND @jc < @JobSize
  BEGIN 
    DELETE @d; 
 
    UPDATE TOP (@BatchSize) q SET ProcessedDate = sysdatetime() 
      OUTPUT inserted.Id, inserted.OwnerUserId INTO @d 
      FROM dbo.SuggestedEditDeleteQueue AS q WITH (UPDLOCK, READPAST) 
       WHERE ProcessedDate IS NULL; 
 
    SET @rc = @@ROWCOUNT; 
    IF @rc = 0 BREAK; 
 
    DELETE fse 
      FROM dbo.FakeSuggestedEdits AS fse 
      INNER JOIN @d AS d 
        ON fse.Id = d.Id 
       AND fse.OwnerUserId = d.OwnerUserId; 
 
    SET @jc += @rc; 
    IF @jc > @JobSize BREAK;
 
    EXEC sys.sp_executesql @wf;
  END
  RAISERROR('Deleted %d rows.', 0, 1, @jc) WITH NOWAIT;
END

Teraz usuwanie wierszy potrwa dłużej — średnia dla 10 000 wierszy to 223 sekundy, z czego ~100 to celowe opóźnienie. Ale żaden użytkownik nie czeka, więc kogo to obchodzi? Profil procesora jest prawie zerowy, a aplikacja może kontynuować dodawanie elementów w kolejce tak bardzo współbieżnie, jak chce, z prawie zerowym konfliktem z zadaniem w tle. Podczas przetwarzania 10 000 wierszy dodałem kolejne 16 tys. wierszy do kolejki i używało tego samego procesora co poprzednio — trwało to tylko o sekundę dłużej niż wtedy, gdy zadanie nie było uruchomione:

Plan wygląda teraz tak, ze znacznie lepszymi szacunkowymi/rzeczywistymi wierszami:

Widzę, że takie podejście do tabeli kolejek jest skutecznym sposobem radzenia sobie z wysoką współbieżnością DML, ale wymaga przynajmniej odrobiny elastyczności w przypadku aplikacji przesyłających DML — jest to jeden z powodów, dla których naprawdę lubię, gdy aplikacje wywołują procedury składowane, ponieważ daj nam znacznie większą kontrolę bliżej danych.

Inne opcje

Jeśli nie masz możliwości zmiany zapytań dotyczących usuwania pochodzących z aplikacji — lub jeśli nie możesz odroczyć usuwania do procesu w tle — możesz rozważyć inne opcje, aby zmniejszyć wpływ usuwania:

  • Indeks nieklastrowy w kolumnach predykatów do obsługi wyszukiwania punktów (możemy to zrobić w izolacji bez zmiany aplikacji)
  • Korzystanie tylko z miękkiego usuwania (nadal wymaga zmian w aplikacji)

Ciekawe będzie sprawdzenie, czy te opcje oferują podobne korzyści, ale zachowam je na przyszły wpis.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Migracja schematu:relacja do gwiazdy

  2. Potencjalne ulepszenie aktualizacji statystyk:MAXDOP

  3. Grupowanie danych za pomocą funkcji OVER i PARTITION BY

  4. Model danych dotyczących opieki nad zwierzętami

  5. Analiza śmierci o tysiąc zmniejsza obciążenie pracą