Många människor har implementerat ASPState i sin miljö. Vissa människor använder in-memory-alternativet (InProc), men vanligtvis ser jag att databasalternativet används. Det finns några potentiella ineffektiviteter här som du kanske inte märker på webbplatser med låg volym, men som kommer att börja påverka prestandan när din webbvolym ökar.
Återställningsmodell
Se till att ASPState är inställt på enkel återställning – detta kommer dramatiskt att minska effekten på loggen som kan orsakas av den höga volymen (övergående och till stor del engångs) skrivningar som sannolikt kommer hit:
ÄNDRA DATABAS ASPState SET ÅTERSTÄLLNING ENKEL;
Vanligtvis behöver den här databasen inte vara i full återställning, särskilt eftersom om du är i katastrofåterställningsläge och återställer din databas, är det sista du bör oroa dig för att försöka underhålla sessioner för användare i din webbapp – som sannolikt kommer att vara borta för länge sedan du har återställt. Jag tror aldrig att jag har stött på en situation där punkt-i-tidsåterställning var en nödvändighet för en övergående databas som ASPState.
Minimera / isolera I/O
När du ställer in ASPState initialt kan du använda -sstype c
och -d
argument för att lagra sessionstillstånd i en anpassad databas som redan finns på en annan enhet (precis som du skulle göra med tempdb). Eller, om din tempdb-databas redan är optimerad, kan du använda -sstype t
argument. Dessa förklaras i detalj i dokumenten Session-State Modes och ASP.NET SQL Server Registration Tool på MSDN.
Om du redan har installerat ASPState och du har bestämt att du skulle tjäna på att flytta den till sin egen (eller åtminstone en annan) volym, kan du schemalägga eller vänta på en kort underhållsperiod och följa dessa steg:
ALTER DATABASE ASPState SET SINGLE_USER WITH ROLLBACK IMMEDIATE; ALTER DATABASE ASPState SET OFFLINE; ALTER DATABASE ASPState MODIFY FILE (NAME =ASPState, FILENAME ='{ny sökväg}\ASPSstate.mdf'); ALTER DATABASE ASPState MODIFY FILE (NAME =ASPState_log, FILENAME ='{ny sökväg}\ASPSstate_log.ldf'); före>Vid det här laget måste du manuellt flytta filerna till
, och sedan kan du göra databasen online igen: ÄNDRA DATABAS ASPState SET ONLINE; ALTER DATABASE ASPState SET MULTI_USER;Isolera applikationer
Det är möjligt att peka mer än en applikation på samma sessionstillståndsdatabas. Jag avråder från detta. Du kanske vill peka applikationer på olika databaser, kanske till och med på olika instanser, för att bättre isolera resursanvändning och ge största möjliga flexibilitet för alla dina webbegenskaper.
Om du redan har flera applikationer som använder samma databas är det okej, men du vill hålla reda på vilken effekt varje applikation kan ha. Microsofts Rex Tang publicerade en användbar fråga för att se utrymme som konsumeras av varje session; här är en modifiering som sammanfattar antalet sessioner och total/genomsnittlig sessionsstorlek per applikation:
SELECT a.AppName, SessionCount =COUNT(s.SessionId), TotalSessionSize =SUM(DATALENGTH(s.SessionItemLong)), AvgSessionSize =AVG(DATALENGTH(s.SessionItemLong))FRÅN dbo.ASPStateTempSessions AS 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.AppNameORDER BY TotalSessionSize DESC;Om du upptäcker att du har en skev distribution här, kan du skapa en annan ASPState-databas någon annanstans och rikta en eller flera applikationer till den databasen istället.
Gör mer användarvänliga borttagningar
Koden för
dbo.DeleteExpiredSessions
använder en markör och ersätter en endaDELETE
i tidigare implementeringar. (Detta, tror jag, baserades till stor del på detta inlägg av Greg Low.)Ursprungligen var koden:
SKAPA PROCEDUR DeleteExpiredSessionsAS DECLARE @now DATETIME SET @now =GETUTCDATE() DELETE ASPState..ASPSStateTempSessions WHERE Expires <@now RETURN 0GO(Och det kan det fortfarande vara, beroende på var du laddade ner källan, eller hur länge sedan du installerade ASPState. Det finns många föråldrade skript där ute för att skapa databasen, även om du verkligen borde använda aspnet_regsql.exe.)
För närvarande (från och med .NET 4.5) ser koden ut så här (någon som vet när Microsoft kommer att börja använda semikolon?).
ÄNDRA PROCEDUR [dbo].[DeleteExpiredSessions]SOM INSTÄLLT NOCOUNT ON SET DEADLOCK_PRIORITY LOW DECLARE @now datetime SET @now =GETUTCDATE() SKAPA TABELL #tblExpiredSessions (SessionsID nvarchar(88) PRIMARY INSERT KEYtblsSessionID INTE #tblsession ID ) VÄLJ Sessions-ID FRÅN [ASPState].dbo.ASPStateTempSessions WITH (READUNCOMMITTED) WHERE Expires <@now IF @@ROWCOUNT <> 0 BÖRJA DECLARE ExpiredSessionCursor CURSOR LOCAL FORWARD_ONLY READ_ONLY FORWARD_ONLY READ_ONLY FOR SELECT Session DEblSsession FCharCarsID NÄSTA FRÅN ExpiredSessionCursor INTO @SessionID WHILE @@FETCH_STATUS =0 BÖRJA DELETE FRÅN [ASPSstate].dbo.ASPSateTempSes sions WHERE SessionID =@SessionID AND Expires <@now HÄMTA NÄSTA FRÅN ExpiredSessionCursor INTO @SessionID END STÄNG ExpiredSessionCursor AVALLOKERA ExpiredSessionCursor END DROP TABLE #tblExpiredSessions RETUR 0Min idé är att ha ett lyckligt medium här – försök inte ta bort ALLA rader i ett svep, men spela inte en efter en mullvad heller. Ta i stället bort
n
rader åt gången i separata transaktioner – vilket minskar längden på blockering och minimerar även påverkan på loggen:ÄNDRA PROCEDUR dbo.DeleteExpiredSessions @top INT =1000AS BÖRJAN STÄLL IN NOCOUNT PÅ; DECLARE @nu DATUMTIDEN, @c INT; VÄLJ @nu =GETUTCDATE(), @c =1; BÖRJA TRANSAKTIONEN; WHILE @c <> 0 BEGIN;WITH x AS ( VÄLJ TOP (@top) SessionId FRÅN dbo.ASPstateTempSessions WHERE Expires <@now ORDER BY SessionId ) DELETE x; SET @c =@@ROWCOUNT; OM @@TRANCOUNT =1 BÖRJA BETA TRANSAKTION; BÖRJA TRANSAKTIONEN; SLUTA SLUTA OM @@TRANCOUNT =1 BÖRJA BETA TRANSAKTION; ENDENDGODu kommer att vilja experimentera med
TOPP
beroende på hur upptagen din server är och vilken inverkan den har på varaktighet och låsning. Du kanske också vill överväga att implementera ögonblicksbildsisolering – detta kommer att tvinga fram en viss påverkan på tempdb men kan minska eller eliminera blockering från appen.Som standard är jobbet också
ASPSState_Job_DeleteExpiredSessions
springer varje minut. Överväg att slå tillbaka det lite – minska schemat till kanske var 5:e minut (och återigen, mycket av detta kommer att bero på hur upptagna dina applikationer är och testa effekten av förändringen). Och på baksidan, se till att den är aktiverad – annars kommer din sessionstabell att växa och växa okontrollerat.Beröringssessioner mindre ofta
Varje gång en sida laddas (och, om webbappen inte har skapats korrekt, möjligen flera gånger per sidladdning), den lagrade proceduren
dbo.TempResetTimeout
anropas, vilket säkerställer att timeouten för just den sessionen förlängs så länge de fortsätter att generera aktivitet. På en upptagen webbplats kan detta orsaka en mycket hög volym av uppdateringsaktivitet mot tabellendbo.ASPStateTempSessions
. Här är den aktuella koden fördbo.TempResetTimeout
:ÄNDRA PROCEDUR [dbo].[TempResetTimeout] @id tSessionId AS UPDATE [ASPSate].dbo.ASPSateTempSessions SET Expires =DATEADD(n, Timeout, GETUTCDATE()) WHERE SessionId =@id RETURN 0Föreställ dig nu att du har en webbplats med 500 eller 5 000 användare, och de klickar alla galet från sida till sida. Detta är förmodligen en av de oftast kallade operationerna i någon ASPState-implementering, och medan tabellen är inskriven på
SessionId
– så påverkan av varje enskilt uttalande bör vara minimal – sammantaget kan detta vara avsevärt slöseri, inklusive på loggen. Om din sessionstimeout är 30 minuter och du uppdaterar timeouten för en session var 10:e sekund på grund av webbappens natur, vad är poängen med att göra det igen 10 sekunder senare? Så länge den sessionen uppdateras asynkront någon gång innan de 30 minuterna är slut, finns det ingen nettoskillnad för användaren eller applikationen. Så jag tänkte att du kunde implementera ett mer skalbart sätt att "touch"-sessioner för att uppdatera deras timeout-värden.En idé jag hade var att implementera en servicemäklarkö så att applikationen inte behöver vänta på att själva skrivningen ska ske – den anropar
dbo.TempResetTimeout
lagrad procedur, och sedan tar aktiveringsproceduren över asynkront. Men detta leder fortfarande till mycket fler uppdateringar (och loggaktivitet) än vad som verkligen är nödvändigt.En bättre idé, IMHO, är att implementera en kötabell som du bara infogar i, och enligt ett schema (så att processen slutför en hel cykel på någon tid kortare än timeouten), skulle den bara uppdatera timeouten för varje session. ser en gång , oavsett hur många gånger de *försökte* uppdatera sin timeout inom det intervallet. Så en enkel tabell kan se ut så här:
CREATE TABLE dbo.SessionStack( SessionId tSessionId, -- nvarchar(88) - naturligtvis var de tvungna att använda aliastyper EventTime DATETIME, Processed BIT NOT NULL DEFAULT 0); SKAPA CLUSTERED INDEX och PÅ dbo.SessionStack(EventTime);GOOch sedan skulle vi ändra lagerproceduren för att överföra sessionsaktivitet till denna stack istället för att röra sessionstabellen direkt:
ÄNDRA PROCEDUR dbo.TempResetTimeout @id tSessionIdASBEGIN STÄLL IN NOCOUNT PÅ; INSERT INTO dbo.SessionStack(SessionId, EventTime) SELECT @id, GETUTCDATE();ENDGODet klustrade indexet är på
smalldatetime
kolumn för att förhindra siddelning (till den potentiella kostnaden för en het sida), eftersom händelsetiden för en sessionsberöring alltid kommer att öka monotont.Sedan behöver vi en bakgrundsprocess för att periodvis sammanfatta nya rader i
dbo.SessionStack
och uppdateradbo.ASPStateTempSessions
i enlighet därmed.SKAPA PROCEDUR dbo.SessionStack_ProcessASBEGIN STÄLL IN NOCOUNT PÅ; -- om du inte vill lägga till tSessionId till modellen eller manuellt till tempdb -- efter varje omstart måste vi använda bastypen här:CREATE TABLE #s(SessionId NVARCHAR(88), EventTime SMALLDATETIME); -- stacken är nu din hotspot, så gå in och ut snabbt:UPPDATERA dbo.SessionStack SET Processed =1 OUTPUT inserted.SessionId, inserted.EventTime INTO #s WHERE Processed IN (0,1) -- ifall någon misslyckades senast tid OCH EventTimeDu kanske vill lägga till mer transaktionskontroll och felhantering kring det här – jag presenterar bara en idé som inte fungerar som den ska, och du kan bli så galen som du vill. :-)
Du kanske tror att du skulle vilja lägga till ett icke-klustrat index på
dbo.SessionStack(SessionId, EventTime DESC)
för att underlätta bakgrundsprocessen, men jag tror att det är bättre att fokusera även de minsta prestandavinsterna på den process som användare väntar på (varje sidladdning) i motsats till en som de inte väntar på (bakgrundsprocessen). Så jag betalar hellre kostnaden för en potentiell skanning under bakgrundsprocessen än att betala för ytterligare indexunderhåll under varje enskild insättning. Precis som med det klustrade indexet på #temp-tabellen finns det mycket "det beror på" här, så du kanske vill spela med dessa alternativ för att se var din tolerans fungerar bäst.Såvida inte frekvensen av de två operationerna behöver vara drastiskt olika, skulle jag schemalägga detta som en del av
ASPState_Job_DeleteExpiredSessions
jobb (och överväg att döpa om det i så fall) så att dessa två processer inte trampar på varandra.En sista idé här, om du upptäcker att du behöver skala ut ännu mer, är att skapa flera
SessionStack
tabeller, där var och en är ansvarig för en delmängd av sessioner (t.ex. hashad på det första tecknet iSessionId
). Sedan kan du bearbeta varje tabell i tur och ordning och hålla dessa transaktioner så mycket mindre. Faktum är att du kan göra något liknande för borttagningsjobbet också. Om det görs på rätt sätt bör du kunna lägga dessa i enskilda jobb och köra dem samtidigt, eftersom – i teorin – DML borde påverka helt andra uppsättningar av sidor.Slutsats
Det är mina idéer så här långt. Jag skulle gärna höra om dina erfarenheter av ASPState:Vilken typ av skala har du uppnått? Vilken typ av flaskhalsar har du observerat? Vad har du gjort för att lindra dem?