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

Potencjalne ulepszenia ASPState

Wiele osób wdrożyło ASPState w swoim środowisku. Niektórzy używają opcji w pamięci (InProc), ale zazwyczaj widzę używaną opcję bazy danych. Istnieją tutaj pewne potencjalne nieefektywności, których możesz nie zauważyć w witrynach o małej liczbie wyświetleń, ale zaczną one wpływać na wydajność w miarę zwiększania się liczby stron internetowych.

Model odzyskiwania

Upewnij się, że ASPState jest ustawione na proste odzyskiwanie – drastycznie zmniejszy to wpływ na dziennik, który może być spowodowany dużą liczbą (przejściowych i w dużej mierze jednorazowych) zapisów, które mogą się tutaj znaleźć:

ALTER DATABASE ASPState SET RECOVERY SIMPLE;

Zwykle ta baza danych nie musi być w trybie pełnego odzyskiwania, zwłaszcza że jeśli jesteś w trybie odzyskiwania po awarii i przywracasz bazę danych, ostatnią rzeczą, o którą powinieneś się martwić, jest próba utrzymania sesji dla użytkowników w Twojej aplikacji internetowej — którzy prawdopodobnie będą już dawno zniknie, zanim się odnowisz. Nie wydaje mi się, żebym kiedykolwiek spotkał się z sytuacją, w której odzyskiwanie do określonego momentu było koniecznością dla przejściowej bazy danych, takiej jak ASPState.

Minimalizuj / izoluj we/wy

Podczas początkowej konfiguracji ASPState możesz użyć -sstype c i -d argumenty do przechowywania stanu sesji w niestandardowej bazie danych, która jest już na innym dysku (tak jak w przypadku tempdb). Lub, jeśli baza danych tempdb jest już zoptymalizowana, możesz użyć -sstype t argument. Zostały one szczegółowo wyjaśnione w dokumentach dotyczących trybów stanu sesji i narzędzia rejestracji programu ASP.NET SQL Server w witrynie MSDN.

Jeśli zainstalowałeś już ASPState i stwierdziłeś, że skorzystasz na przeniesieniu go do własnego (lub przynajmniej innego) woluminu, możesz zaplanować lub poczekać na krótki okres konserwacji i wykonać następujące kroki:

ALTER DATABASE ASPState SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
 
ALTER DATABASE ASPState SET OFFLINE;
 
ALTER DATABASE ASPState MODIFY FILE (NAME = ASPState,     FILENAME = '{new path}\ASPState.mdf');
ALTER DATABASE ASPState MODIFY FILE (NAME = ASPState_log, FILENAME = '{new path}\ASPState_log.ldf');

W tym momencie będziesz musiał ręcznie przenieść pliki do , a następnie możesz przywrócić bazę danych do trybu online:

ALTER DATABASE ASPState SET ONLINE;
 
ALTER DATABASE ASPState SET MULTI_USER;

Izoluj aplikacje

Możliwe jest wskazanie więcej niż jednej aplikacji w tej samej bazie danych stanów sesji. Odradzam. Możesz chcieć wskazać aplikacje w różnych bazach danych, być może nawet w różnych instancjach, aby lepiej izolować użycie zasobów i zapewnić maksymalną elastyczność dla wszystkich swoich właściwości internetowych.

Jeśli masz już wiele aplikacji korzystających z tej samej bazy danych, to w porządku, ale będziesz chciał śledzić wpływ, jaki może mieć każda aplikacja. Rex Tang z Microsoftu opublikował przydatne zapytanie, aby zobaczyć miejsce zajmowane przez każdą sesję; oto modyfikacja, która podsumowuje liczbę sesji i całkowity/średni rozmiar sesji na aplikację:

SELECT 
  a.AppName, 
  SessionCount = COUNT(s.SessionId),
  TotalSessionSize = SUM(DATALENGTH(s.SessionItemLong)),
  AvgSessionSize = AVG(DATALENGTH(s.SessionItemLong))
FROM 
  dbo.ASPStateTempSessions AS s
LEFT OUTER JOIN 
  dbo.ASPStateTempApplications AS a 
  ON SUBSTRING(s.SessionId, 25, 8) = SUBSTRING(sys.fn_varbintohexstr(CONVERT(VARBINARY(8), a.AppId)), 3, 8) 
GROUP BY a.AppName
ORDER BY TotalSessionSize DESC;

Jeśli stwierdzisz, że masz tutaj krzywą dystrybucję, możesz skonfigurować inną bazę danych ASPState w innym miejscu i zamiast tego wskazać jedną lub więcej aplikacji w tej bazie danych.

Twórz bardziej przyjazne usunięcia

Kod dla dbo.DeleteExpiredSessions używa kursora, zastępując pojedyncze DELETE we wcześniejszych wdrożeniach. (Myślę, że było to oparte w dużej mierze na tym poście Grega Lowa.)

Pierwotnie kod był:

CREATE PROCEDURE DeleteExpiredSessions
AS
  DECLARE @now DATETIME
  SET @now = GETUTCDATE()
 
  DELETE ASPState..ASPStateTempSessions
  WHERE Expires < @now
 
  RETURN 0
GO

(I nadal może być, w zależności od tego, gdzie pobrałeś źródło lub jak dawno zainstalowałeś ASPState. Istnieje wiele przestarzałych skryptów do tworzenia bazy danych, chociaż naprawdę powinieneś używać aspnet_regsql.exe.)

Obecnie (od .NET 4.5) kod wygląda tak (ktoś wie, kiedy Microsoft zacznie używać średników?).

ALTER PROCEDURE [dbo].[DeleteExpiredSessions]
AS
            SET NOCOUNT ON
            SET DEADLOCK_PRIORITY LOW 
 
            DECLARE @now datetime
            SET @now = GETUTCDATE() 
 
            CREATE TABLE #tblExpiredSessions 
            ( 
                SessionID nvarchar(88) NOT NULL PRIMARY KEY
            )
 
            INSERT #tblExpiredSessions (SessionID)
                SELECT SessionID
                FROM [ASPState].dbo.ASPStateTempSessions WITH (READUNCOMMITTED)
                WHERE Expires < @now
 
            IF @@ROWCOUNT <> 0 
            BEGIN 
                DECLARE ExpiredSessionCursor CURSOR LOCAL FORWARD_ONLY READ_ONLY
                FOR SELECT SessionID FROM #tblExpiredSessions 
 
                DECLARE @SessionID nvarchar(88)
 
                OPEN ExpiredSessionCursor
 
                FETCH NEXT FROM ExpiredSessionCursor INTO @SessionID
 
                WHILE @@FETCH_STATUS = 0 
                    BEGIN
                        DELETE FROM [ASPState].dbo.ASPStateTempSessions WHERE SessionID = @SessionID AND Expires < @now
                        FETCH NEXT FROM ExpiredSessionCursor INTO @SessionID
                    END
 
                CLOSE ExpiredSessionCursor
 
                DEALLOCATE ExpiredSessionCursor
 
            END 
 
            DROP TABLE #tblExpiredSessions
 
        RETURN 0

Moim pomysłem jest, aby mieć tutaj szczęśliwy środek – nie próbuj usuwać WSZYSTKICH wierszy za jednym zamachem, ale nie graj też jeden po drugim. Zamiast tego usuń n wierszy na raz w osobnych transakcjach – skracając czas blokowania, a także minimalizując wpływ na log:

ALTER PROCEDURE dbo.DeleteExpiredSessions
  @top INT = 1000
AS
BEGIN
  SET NOCOUNT ON;
 
  DECLARE @now DATETIME, @c INT;
  SELECT @now = GETUTCDATE(), @c = 1;
 
  BEGIN TRANSACTION;
 
  WHILE @c <> 0
  BEGIN
    ;WITH x AS 
    (
      SELECT TOP (@top) SessionId
        FROM dbo.ASPStateTempSessions
        WHERE Expires < @now
        ORDER BY SessionId
    ) 
    DELETE x;
 
    SET @c = @@ROWCOUNT;
 
    IF @@TRANCOUNT = 1
    BEGIN
      COMMIT TRANSACTION;
      BEGIN TRANSACTION;
    END
  END
 
  IF @@TRANCOUNT = 1
  BEGIN
    COMMIT TRANSACTION;
  END
END
GO

Będziesz chciał poeksperymentować z TOP w zależności od tego, jak zajęty jest twój serwer i jaki ma to wpływ na czas trwania i blokowanie. Możesz również rozważyć zaimplementowanie izolacji migawki – wymusi to pewien wpływ na tempdb, ale może zmniejszyć lub wyeliminować blokowanie widoczne z aplikacji.

Ponadto domyślnie zadanie ASPState_Job_DeleteExpiredSessions biegnie co minutę. Zastanów się nad tym cofnięciem – skróć harmonogram do być może co 5 minut (i znowu, wiele z tego będzie sprowadzać się do tego, jak zajęte są twoje aplikacje i testowanie wpływu zmiany). Z drugiej strony upewnij się, że jest włączony – w przeciwnym razie Twoja tabela sesji będzie rosła i rosła niezaznaczona.

Rzadsze sesje dotykowe

Za każdym razem, gdy strona jest ładowana (i jeśli aplikacja internetowa nie została utworzona poprawnie, prawdopodobnie wiele razy podczas ładowania strony), procedura składowana dbo.TempResetTimeout jest wywoływana, zapewniając, że limit czasu dla tej konkretnej sesji jest przedłużony, o ile nadal generują aktywność. W przypadku ruchliwej witryny internetowej może to spowodować bardzo dużą liczbę aktualizacji w tabeli dbo.ASPStateTempSessions . Oto aktualny kod dla dbo.TempResetTimeout :

ALTER PROCEDURE [dbo].[TempResetTimeout]
            @id     tSessionId
        AS
            UPDATE [ASPState].dbo.ASPStateTempSessions
            SET Expires = DATEADD(n, Timeout, GETUTCDATE())
            WHERE SessionId = @id
            RETURN 0

Teraz wyobraź sobie, że masz witrynę internetową z 500 lub 5000 użytkowników i wszyscy szaleńczo klikają ze strony na stronę. Jest to prawdopodobnie jedna z najczęściej wywoływanych operacji w dowolnej implementacji ASPState, a kluczem tabeli jest SessionId – więc wpływ każdego indywidualnego oświadczenia powinien być minimalny – łącznie może to być znacznie marnotrawne, w tym w dzienniku. Jeśli limit czasu sesji wynosi 30 minut i aktualizujesz limit czasu sesji co 10 sekund ze względu na charakter aplikacji internetowej, jaki jest sens robienia tego ponownie 10 sekund później? Dopóki ta sesja jest aktualizowana asynchronicznie w pewnym momencie przed upływem 30 minut, nie ma różnicy netto dla użytkownika lub aplikacji. Pomyślałem więc, że można zaimplementować bardziej skalowalny sposób „dotykania” sesji, aby zaktualizować ich wartości limitu czasu.

Jednym z pomysłów, jaki miałem, było zaimplementowanie kolejki brokera usług, aby aplikacja nie musiała czekać na rzeczywisty zapis — wywołuje on dbo.TempResetTimeout procedura składowana, a następnie procedura aktywacji przejmuje asynchronicznie. Ale to nadal prowadzi do znacznie większej liczby aktualizacji (i aktywności dzienników), niż jest to naprawdę konieczne.

Lepszym pomysłem, IMHO, jest zaimplementowanie tabeli kolejki, do której tylko wstawiasz i zgodnie z harmonogramem (tak, aby proces zakończył pełny cykl w czasie krótszym niż limit czasu), zaktualizowałby limit czasu tylko dla dowolnej sesji. widzi raz , bez względu na to, ile razy *próbowali* zaktualizować swój limit czasu w tym zakresie. Tak więc prosta tabela może wyglądać tak:

CREATE TABLE dbo.SessionStack
(
  SessionId  tSessionId,    -- nvarchar(88) - of course they had to use alias types
  EventTime  DATETIME, 
  Processed  BIT NOT NULL DEFAULT 0
);
 
CREATE CLUSTERED INDEX et ON dbo.SessionStack(EventTime);
GO

Następnie zmienilibyśmy podstawową procedurę, aby przenieść aktywność sesji na ten stos zamiast bezpośrednio dotykać tabeli sesji:

ALTER PROCEDURE dbo.TempResetTimeout
  @id tSessionId
AS
BEGIN
  SET NOCOUNT ON;
 
  INSERT INTO dbo.SessionStack(SessionId, EventTime)
    SELECT @id, GETUTCDATE();
END
GO

Indeks klastrowy znajduje się w smalldatetime kolumna, aby zapobiec podziałom strony (potencjalnym kosztem gorącej strony), ponieważ czas zdarzenia dla dotknięcia sesji zawsze będzie monotonnie wzrastał.

Następnie będziemy potrzebować procesu w tle, aby okresowo podsumowywać nowe wiersze w dbo.SessionStack i zaktualizuj dbo. ASPStateTempSessions odpowiednio.

CREATE PROCEDURE dbo.SessionStack_Process
AS
BEGIN
  SET NOCOUNT ON;
 
  -- unless you want to add tSessionId to model or manually to tempdb 
  -- after every restart, we'll have to use the base type here:
 
  CREATE TABLE #s(SessionId NVARCHAR(88), EventTime SMALLDATETIME);
 
  -- the stack is now your hotspot, so get in & out quickly:
 
  UPDATE dbo.SessionStack SET Processed = 1 
    OUTPUT inserted.SessionId, inserted.EventTime INTO #s
    WHERE Processed IN (0,1) -- in case any failed last time
    AND EventTime < GETUTCDATE(); -- this may help alleviate contention on last page
 
  -- this CI may be counter-productive; you'll have to experiment:
 
  CREATE CLUSTERED INDEX x ON #s(SessionId, EventTime);
 
  BEGIN TRY
    ;WITH summary(SessionId, Expires) AS 
    (
       SELECT SessionId, MAX(EventTime) 
         FROM #s GROUP BY SessionId
    )
    UPDATE src
      SET Expires = DATEADD(MINUTE, src.[Timeout], summary.Expires)
      FROM dbo.ASPStateTempSessions AS src
      INNER JOIN summary
      ON src.SessionId = summary.SessionId;
 
    DELETE dbo.SessionStack WHERE Processed = 1;
  END TRY
  BEGIN CATCH
    RAISERROR('Something went wrong, will try again next time.', 11, 1);
  END CATCH
END
GO

Możesz chcieć dodać więcej kontroli transakcyjnej i obsługi błędów wokół tego – przedstawiam po prostu niekonwencjonalny pomysł i możesz oszaleć wokół tego, jak chcesz. :-)

Możesz pomyśleć, że chcesz dodać indeks nieklastrowy na dbo.SessionStack(SessionId, EventTime DESC) aby ułatwić proces w tle, ale myślę, że lepiej jest skoncentrować się nawet na najmniejszym zwiększeniu wydajności na procesie, na który użytkownicy czekają (każde ładowanie strony), w przeciwieństwie do procesu, na który nie czekają (proces w tle). Więc wolałbym ponieść koszt potencjalnego skanowania podczas procesu w tle niż płacić za dodatkową konserwację indeksu podczas każdego wstawiania. Podobnie jak w przypadku klastrowego indeksu w tabeli #temp, tutaj jest wiele „to zależy”, więc możesz poeksperymentować z tymi opcjami, aby zobaczyć, gdzie twoja tolerancja działa najlepiej.

O ile częstotliwość tych dwóch operacji nie musi się drastycznie różnić, zaplanowałbym to jako część ASPState_Job_DeleteExpiredSessions zadanie (i rozważ zmianę nazwy tego zadania, jeśli tak), aby te dwa procesy się nie deptały.

Ostatnim pomysłem, jeśli okaże się, że musisz skalować się jeszcze bardziej, jest utworzenie wielu SessionStack tabele, gdzie każda odpowiada za podzbiór sesji (powiedzmy, zahaszowana na pierwszym znaku SessionId ). Następnie możesz przetwarzać każdą tabelę po kolei i utrzymywać te transakcje o wiele mniejsze. W rzeczywistości możesz zrobić coś podobnego również dla zadania usuwania. Jeśli zrobisz to poprawnie, powinieneś być w stanie umieścić je w poszczególnych zadaniach i uruchamiać je jednocześnie, ponieważ – teoretycznie – DML powinien wpływać na zupełnie różne zestawy stron.

Wniosek

To są moje dotychczasowe pomysły. Chciałbym usłyszeć o twoich doświadczeniach z ASPState:Jaką skalę osiągnąłeś? Jakie wąskie gardła zaobserwowałeś? Co zrobiłeś, aby je złagodzić?


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Normalizacja i wydajność w trybie wsadowym

  2. Przechowywana procedura usuwania zduplikowanych rekordów w tabeli SQL

  3. Dopasowanie podaży do popytu — rozwiązania, część 1

  4. Podejścia do bezpieczeństwa w modelowaniu danych. Część 4

  5. 5 sposobów na wyświetlenie tymczasowych tabel za pomocą T-SQL