Veel mensen hebben ASPState in hun omgeving geïmplementeerd. Sommige mensen gebruiken de in-memory optie (InProc), maar meestal zie ik dat de database-optie wordt gebruikt. Er zijn hier enkele potentiële inefficiënties die u misschien niet opmerkt op sites met een laag volume, maar die van invloed zullen zijn op de prestaties naarmate uw webvolume toeneemt.
Herstelmodel
Zorg ervoor dat ASPState is ingesteld op eenvoudig herstel - dit zal de impact op het logboek drastisch verminderen die kan worden veroorzaakt door het grote aantal (tijdelijke en grotendeels wegwerpbare) schrijfbewerkingen die waarschijnlijk hier terechtkomen:
ALTER DATABASE ASPState SET RECOVERY SIMPLE;
Gewoonlijk hoeft deze database niet volledig te worden hersteld, vooral omdat als u zich in de noodherstelmodus bevindt en uw database herstelt, het laatste waar u zich zorgen over moet maken, is proberen sessies te behouden voor gebruikers in uw webapp - die waarschijnlijk lang weg zijn tegen de tijd dat je hersteld bent. Ik denk niet dat ik ooit een situatie ben tegengekomen waarin herstel op een bepaald tijdstip een noodzaak was voor een tijdelijke database zoals ASPState.
Minimaliseren / isoleren van I/O
Wanneer u ASPState aanvankelijk instelt, kunt u de -sstype c
. gebruiken en -d
argumenten om de sessiestatus op te slaan in een aangepaste database die al op een andere schijf staat (net zoals je zou doen met tempdb). Of, als uw tempdb-database al is geoptimaliseerd, kunt u de -sstype t
gebruiken argument. Deze worden in detail uitgelegd in de Session-State Modes en ASP.NET SQL Server Registration Tool-documenten op MSDN.
Als u ASPState al hebt geïnstalleerd en u hebt vastgesteld dat u er baat bij zou hebben het naar zijn eigen (of in ieder geval een ander) volume te verplaatsen, kunt u een korte onderhoudsperiode plannen of wachten en deze stappen volgen:
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');
Op dit punt moet u de bestanden handmatig verplaatsen naar <new path>
, en dan kunt u de database weer online brengen:
ALTER DATABASE ASPState SET ONLINE; ALTER DATABASE ASPState SET MULTI_USER;
Isoleer applicaties
Het is mogelijk om meer dan één applicatie naar dezelfde sessiestatusdatabase te verwijzen. Ik raad dit af. Misschien wilt u applicaties naar verschillende databases verwijzen, misschien zelfs naar verschillende instanties, om het gebruik van bronnen beter te isoleren en de grootst mogelijke flexibiliteit te bieden voor al uw webproperty's.
Als je al meerdere applicaties hebt die dezelfde database gebruiken, is dat oké, maar je wilt wel bijhouden welke impact elke applicatie kan hebben. Microsoft's Rex Tang publiceerde een nuttige query om te zien hoeveel ruimte elke sessie verbruikt; hier is een wijziging die het aantal sessies en de totale/gem. sessiegrootte per applicatie samenvat:
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;
Als u merkt dat u hier een scheve distributie heeft, kunt u elders een andere ASPState-database opzetten en in plaats daarvan een of meer toepassingen naar die database verwijzen.
Maak vriendelijkere verwijderingen
De code voor dbo.DeleteExpiredSessions
gebruikt een cursor, ter vervanging van een enkele DELETE
bij eerdere uitvoeringen. (Dit was, denk ik, grotendeels gebaseerd op dit bericht van Greg Low.)
Oorspronkelijk was de code:
CREATE PROCEDURE DeleteExpiredSessions AS DECLARE @now DATETIME SET @now = GETUTCDATE() DELETE ASPState..ASPStateTempSessions WHERE Expires < @now RETURN 0 GO
(En het kan nog steeds zo zijn, afhankelijk van waar je de broncode hebt gedownload of hoe lang geleden je ASPState hebt geïnstalleerd. Er zijn veel verouderde scripts voor het maken van de database, hoewel je eigenlijk aspnet_regsql.exe zou moeten gebruiken.)
Momenteel (vanaf .NET 4.5) ziet de code er als volgt uit (weet iemand wanneer Microsoft puntkomma's gaat gebruiken?).
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
Mijn idee is om hier een gulden middenweg te hebben - probeer niet ALLE rijen in één klap te verwijderen, maar speel ook niet één voor één een mep. Verwijder in plaats daarvan n
rijen tegelijk in afzonderlijke transacties - waardoor de blokkeringsduur wordt verkort en ook de impact op het logboek wordt geminimaliseerd:
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
U wilt experimenteren met TOP
afhankelijk van hoe druk uw server is en welke impact deze heeft op duur en vergrendeling. U kunt ook overwegen om snapshot-isolatie te implementeren - dit zal enige impact hebben op tempdb, maar kan de blokkering van de app verminderen of elimineren.
Ook is standaard de taak ASPState_Job_DeleteExpiredSessions
loopt elke minuut. Overweeg om dat een beetje terug te draaien - verminder het schema tot misschien elke 5 minuten (en nogmaals, veel hiervan zal afhangen van hoe druk uw applicaties zijn en het testen van de impact van de wijziging). En aan de andere kant, zorg ervoor dat het is ingeschakeld – anders zal je sessietafel groeien en groeien ongecontroleerd.
Raak sessies minder vaak aan
Elke keer dat een pagina wordt geladen (en, als de web-app niet correct is gemaakt, mogelijk meerdere keren per pagina die wordt geladen), wordt de opgeslagen procedure dbo.TempResetTimeout
wordt aangeroepen, zodat de time-out voor die specifieke sessie wordt verlengd zolang ze activiteit blijven genereren. Op een drukke website kan dit een zeer hoog volume aan update-activiteit veroorzaken tegen de tabel dbo.ASPStateTempSessions
. Hier is de huidige code voor dbo.TempResetTimeout
:
ALTER PROCEDURE [dbo].[TempResetTimeout] @id tSessionId AS UPDATE [ASPState].dbo.ASPStateTempSessions SET Expires = DATEADD(n, Timeout, GETUTCDATE()) WHERE SessionId = @id RETURN 0
Stel je nu voor dat je een website hebt met 500 of 5000 gebruikers, en ze klikken allemaal waanzinnig van pagina naar pagina. Dit is waarschijnlijk een van de meest genoemde bewerkingen in elke ASPState-implementatie, en hoewel de tabel is ingetoetst op SessionId
- dus de impact van een individuele verklaring moet minimaal zijn - in totaal kan dit aanzienlijk verspillend zijn, ook op het logboek. Als uw sessietime-out 30 minuten is en u de time-out voor een sessie elke 10 seconden bijwerkt vanwege de aard van de web-app, wat heeft het dan voor zin om het 10 seconden later opnieuw te doen? Zolang die sessie asynchroon wordt bijgewerkt voordat de 30 minuten voorbij zijn, is er geen netto verschil voor de gebruiker of de toepassing. Dus ik dacht dat je een meer schaalbare manier zou kunnen implementeren om sessies te 'aanraken' om hun time-outwaarden bij te werken.
Een idee dat ik had was om een service broker-wachtrij te implementeren, zodat de toepassing niet hoeft te wachten op het daadwerkelijke schrijven - het roept de dbo.TempResetTimeout
aan opgeslagen procedure, waarna de activeringsprocedure asynchroon overneemt. Maar dit leidt nog steeds tot veel meer updates (en logactiviteit) dan echt nodig is.
Een beter idee, IMHO, is om een wachtrijtabel te implementeren die u alleen invoegt en volgens een schema (zodat het proces een volledige cyclus voltooit in een tijd die korter is dan de time-out), het zou alleen de time-out bijwerken voor elke sessie die het ziet eenmaal , ongeacht hoe vaak ze *probeerden* om hun time-out binnen die periode bij te werken. Een eenvoudige tabel kan er dus als volgt uitzien:
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
En dan zouden we de voorraadprocedure veranderen om sessie-activiteit naar deze stapel te duwen in plaats van de sessietafel rechtstreeks aan te raken:
ALTER PROCEDURE dbo.TempResetTimeout @id tSessionId AS BEGIN SET NOCOUNT ON; INSERT INTO dbo.SessionStack(SessionId, EventTime) SELECT @id, GETUTCDATE(); END GO
De geclusterde index staat op de smalldatetime
kolom om paginasplitsingen te voorkomen (ten koste van een hot-page), aangezien de gebeurtenistijd voor een sessieaanraking altijd monotoon zal toenemen.
Dan hebben we een achtergrondproces nodig om periodiek nieuwe rijen samen te vatten in dbo.SessionStack
en update dbo.ASPStateTempSessions
dienovereenkomstig.
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
Misschien wil je hier meer transactiecontrole en foutafhandeling aan toevoegen - ik presenteer slechts een kant-en-klaar idee, en je kunt hier zo gek van worden als je wilt. :-)
Je zou kunnen denken dat je een niet-geclusterde index zou willen toevoegen aan dbo.SessionStack(SessionId, EventTime DESC)
om het achtergrondproces te vergemakkelijken, maar ik denk dat het beter is om zelfs de meest minuscule prestatiewinst te concentreren op het proces waarop gebruikers wachten (elke pagina die wordt geladen) in plaats van op een proces waar ze niet op wachten (het achtergrondproces). Dus ik betaal liever de kosten van een mogelijke scan tijdens het achtergrondproces dan voor extra indexonderhoud tijdens elke afzonderlijke insert. Net als bij de geclusterde index op de #temp-tabel, is er hier veel "het hangt ervan af", dus misschien wil je met deze opties spelen om te zien waar je tolerantie het beste werkt.
Tenzij de frequentie van de twee bewerkingen drastisch moet verschillen, zou ik dit plannen als onderdeel van de ASPState_Job_DeleteExpiredSessions
taak (en overweeg de naam van die taak te wijzigen) zodat deze twee processen elkaar niet vertrappen.
Een laatste idee hier, als je merkt dat je nog meer moet uitschalen, is om meerdere SessionStack
te maken tabellen, waarbij elk verantwoordelijk is voor een subset van sessies (bijvoorbeeld gehasht op het eerste teken van de SessionId
). Vervolgens kunt u elke tabel om de beurt verwerken en die transacties veel kleiner houden. In feite zou je ook iets soortgelijks kunnen doen voor de verwijdertaak. Als u het goed doet, zou u deze in afzonderlijke taken moeten kunnen plaatsen en gelijktijdig kunnen uitvoeren, aangezien - in theorie - de DML van invloed zou moeten zijn op totaal verschillende reeksen pagina's.
Conclusie
Dat zijn mijn ideeën tot nu toe. Ik zou graag horen over uw ervaringen met ASPState:Wat voor schaal heeft u bereikt? Wat voor knelpunten heb je geconstateerd? Wat heb je gedaan om ze te verminderen?