Database
 sql >> Database >  >> RDS >> Database

Potenziali miglioramenti ad ASPState

Molte persone hanno implementato ASPSate nel loro ambiente. Alcune persone usano l'opzione in-memory (InProc), ma di solito vedo l'opzione del database in uso. Ci sono alcune potenziali inefficienze che potresti non notare su siti a basso volume, ma che inizieranno a influire sulle prestazioni man mano che il volume web aumenta.

Modello di recupero

Assicurati che ASPState sia impostato su un ripristino semplice:ciò ridurrà drasticamente l'impatto sul registro che può essere causato dall'elevato volume di scritture (transitorie e in gran parte usa e getta) che probabilmente andranno qui:

ALTER DATABASE ASPState SET RECOVERY SIMPLE;

Di solito questo database non ha bisogno di essere in pieno ripristino, soprattutto perché se sei in modalità di ripristino di emergenza e stai ripristinando il tuo database, l'ultima cosa di cui dovresti preoccuparti è cercare di mantenere le sessioni per gli utenti nella tua app Web, che probabilmente lo faranno essere passato da tempo quando hai ripristinato. Non credo di essermi mai imbattuto in una situazione in cui il ripristino point-in-time fosse una necessità per un database transitorio come ASPState.

Ridurre/isolare I/O

Quando si configura ASPState inizialmente, è possibile utilizzare il -sstype c e -d argomenti per archiviare lo stato della sessione in un database personalizzato che si trova già su un'unità diversa (proprio come faresti con tempdb). Oppure, se il tuo database tempdb è già ottimizzato, puoi utilizzare il -sstype t discussione. Questi sono spiegati in dettaglio nei documenti Session-State Modes e ASP.NET SQL Server Registration Tool su MSDN.

Se hai già installato ASPState e hai stabilito che trarresti vantaggio dal spostarlo sul proprio volume (o almeno su un altro), puoi pianificare o attendere un breve periodo di manutenzione e seguire questi passaggi:

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');

A questo punto dovrai spostare manualmente i file in <new path> , quindi puoi riportare il database online:

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

Isola le applicazioni

È possibile puntare più di un'applicazione allo stesso database dello stato della sessione. Mi raccomando contro questo. Potresti voler indirizzare le applicazioni a database diversi, magari anche su istanze diverse, per isolare meglio l'utilizzo delle risorse e fornire la massima flessibilità per tutte le tue proprietà web.

Se hai già più applicazioni che utilizzano lo stesso database, va bene, ma ti consigliamo di tenere traccia dell'impatto che ciascuna applicazione potrebbe avere. Rex Tang di Microsoft ha pubblicato un'utile query per vedere lo spazio consumato da ogni sessione; ecco una modifica che riassumerà il numero di sessioni e la dimensione totale/media della sessione per applicazione:

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;

Se scopri di avere una distribuzione sbilenca qui, puoi configurare un altro database ASPState altrove e puntare invece una o più applicazioni a quel database.

Esegui eliminazioni più amichevoli

Il codice per dbo.DeleteExpiredSessions usa un cursore, sostituendo un singolo DELETE nelle implementazioni precedenti. (Questo, credo, fosse basato in gran parte su questo post di Greg Low.)

In origine il codice era:

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

(E potrebbe ancora esserlo, a seconda di dove hai scaricato il sorgente o da quanto tempo hai installato ASPState. Esistono molti script obsoleti per la creazione del database, anche se dovresti davvero usare aspnet_regsql.exe.)

Attualmente (a partire da .NET 4.5), il codice è simile a questo (qualcuno sa quando Microsoft inizierà a utilizzare il punto e virgola?).

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

La mia idea è quella di avere una via di mezzo qui:non provare a cancellare TUTTE le righe in un colpo solo, ma non giocare nemmeno a colpi di talpa uno per uno. Elimina invece n righe alla volta in transazioni separate, riducendo la durata del blocco e riducendo al minimo l'impatto sul registro:

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

Ti consigliamo di sperimentare con TOP a seconda di quanto è occupato il tuo server e quale impatto ha sulla durata e sul blocco. Potresti anche prendere in considerazione l'implementazione dell'isolamento degli snapshot:ciò imporrà un certo impatto su tempdb ma potrebbe ridurre o eliminare il blocco visualizzato dall'app.

Inoltre, per impostazione predefinita, il lavoro ASPState_Job_DeleteExpiredSessions corre ogni minuto. Prendi in considerazione la possibilità di riattivarlo un po':riduci la pianificazione a forse ogni 5 minuti (e di nuovo, molto di questo dipenderà dall'impegno delle tue applicazioni e dal test dell'impatto della modifica). E d'altra parte, assicurati che sia abilitato – altrimenti la tua tabella delle sessioni aumenterà e crescerà senza controllo.

Sessioni di tocco meno frequenti

Ogni volta che viene caricata una pagina (e, se la web app non è stata creata correttamente, eventualmente più volte per caricamento della pagina), la stored procedure dbo.TempResetTimeout viene chiamato, assicurando che il timeout per quella particolare sessione venga esteso fintanto che continuano a generare attività. In un sito Web occupato, ciò può causare un volume molto elevato di attività di aggiornamento rispetto alla tabella dbo.ASPStateTempSessions . Ecco il codice corrente per dbo.TempResetTimeout :

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

Ora, immagina di avere un sito web con 500 o 5.000 utenti e che stiano tutti cliccando follemente da una pagina all'altra. Questa è probabilmente una delle operazioni chiamate più frequentemente in qualsiasi implementazione di ASPstate e mentre la tabella è digitata su SessionId – quindi l'impatto di ogni singola affermazione dovrebbe essere minimo – nel complesso questo può essere sostanzialmente uno spreco, anche sul registro. Se il timeout della sessione è di 30 minuti e aggiorni il timeout per una sessione ogni 10 secondi a causa della natura dell'app Web, qual è lo scopo di farlo di nuovo 10 secondi dopo? Finché tale sessione viene aggiornata in modo asincrono a un certo punto prima che i 30 minuti siano scaduti, non vi è alcuna differenza netta per l'utente o l'applicazione. Quindi ho pensato che potresti implementare un modo più scalabile per "toccare" le sessioni per aggiornare i loro valori di timeout.

Un'idea che ho avuto è stata quella di implementare una coda del broker di servizi in modo che l'applicazione non debba attendere che avvenga la scrittura effettiva:chiama dbo.TempResetTimeout stored procedure, quindi la procedura di attivazione subentra in modo asincrono. Ma questo porta ancora a molti più aggiornamenti (e attività di registro) di quanto sia veramente necessario.

Un'idea migliore, IMHO, è implementare una tabella di coda in cui si inserisce solo e in base a una pianificazione (in modo tale che il processo completi un ciclo completo in un tempo inferiore al timeout), aggiornerebbe solo il timeout per qualsiasi sessione vede una volta , non importa quante volte hanno *provato* ad aggiornare il loro timeout entro quell'intervallo. Quindi una semplice tabella potrebbe assomigliare a questa:

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

E poi cambieremmo la procedura delle azioni per spingere l'attività della sessione su questo stack invece di toccare direttamente la tabella delle sessioni:

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

L'indice cluster si trova su smalldatetime colonna per evitare divisioni di pagina (al potenziale costo di una pagina attiva), poiché il tempo dell'evento per un tocco di sessione aumenterà sempre in modo monotono.

Quindi avremo bisogno di un processo in background per riepilogare periodicamente le nuove righe in dbo.SessionStack e aggiorna dbo.ASPStateTempSessions di conseguenza.

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

Potresti voler aggiungere più controllo transazionale e gestione degli errori su questo:sto solo presentando un'idea spontanea e puoi impazzire quanto vuoi. :-)

Potresti pensare di voler aggiungere un indice non cluster su dbo.SessionStack(SessionId, EventTime DESC) per facilitare il processo in background, ma penso che sia meglio concentrare anche i più piccoli guadagni di prestazioni sul processo che gli utenti aspettano (ogni caricamento della pagina) piuttosto che su uno che non aspettano (il processo in background). Quindi preferirei pagare il costo di una potenziale scansione durante il processo in background piuttosto che pagare una manutenzione aggiuntiva dell'indice durante ogni singolo inserimento. Come con l'indice cluster sulla tabella #temp, qui c'è molto "dipende", quindi potresti voler giocare con queste opzioni per vedere dove funziona meglio la tua tolleranza.

A meno che la frequenza delle due operazioni non debba essere drasticamente diversa, la pianificherei come parte di ASPState_Job_DeleteExpiredSessions lavoro (e valuta la possibilità di rinominarlo in caso affermativo) in modo che questi due processi non si calpestino a vicenda.

Un'ultima idea qui, se trovi la necessità di aumentare ulteriormente la scalabilità, è creare più SessionStack tabelle, in cui ognuna è responsabile di un sottoinsieme di sessioni (ad esempio, hash sul primo carattere di SessionId ). Quindi puoi elaborare ogni tabella a turno e mantenere quelle transazioni molto più piccole. In effetti potresti fare qualcosa di simile anche per il lavoro di eliminazione. Se fatto correttamente, dovresti essere in grado di inserirli in singoli lavori ed eseguirli contemporaneamente, poiché, in teoria, il DML dovrebbe interessare set di pagine completamente diversi.

Conclusione

Queste sono le mie idee finora. Mi piacerebbe conoscere le tue esperienze con ASPState:che tipo di scala hai raggiunto? Che tipo di colli di bottiglia hai osservato? Cosa hai fatto per mitigarli?