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