sql >> Base de Datos >  >> RDS >> Database

Mejoras potenciales a ASPState

Muchas personas han implementado ASPState en su entorno. Algunas personas usan la opción en memoria (InProc), pero generalmente veo que se usa la opción de base de datos. Aquí hay algunas ineficiencias potenciales que quizás no note en sitios de bajo volumen, pero que comenzarán a afectar el rendimiento a medida que aumente su volumen web.

Modelo de recuperación

Asegúrese de que ASPState esté configurado en recuperación simple; esto reducirá drásticamente el impacto en el registro que puede causar el alto volumen de escrituras (transitorias y en gran parte desechables) que probablemente vayan aquí:

ALTER DATABASE ASPState SET RECOVERY SIMPLE;

Por lo general, esta base de datos no necesita estar en recuperación completa, especialmente porque si está en modo de recuperación de desastres y está restaurando su base de datos, lo último que debería preocuparle es tratar de mantener sesiones para los usuarios en su aplicación web, que es probable que habrá pasado mucho tiempo para cuando hayas restaurado. No creo haberme encontrado nunca con una situación en la que la recuperación de un punto en el tiempo fuera una necesidad para una base de datos transitoria como ASPState.

Minimizar/aislar E/S

Al configurar ASPState inicialmente, puede usar -sstype c y -d argumentos para almacenar el estado de la sesión en una base de datos personalizada que ya está en una unidad diferente (tal como lo haría con tempdb). O, si su base de datos tempdb ya está optimizada, puede usar -sstype t argumento. Estos se explican en detalle en los documentos Modos de estado de sesión y Herramienta de registro de ASP.NET SQL Server en MSDN.

Si ya ha instalado ASPState y ha determinado que se beneficiaría de moverlo a su propio volumen (o al menos a uno diferente), puede programar o esperar un breve período de mantenimiento y seguir estos pasos:

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

En este punto, deberá mover manualmente los archivos a <new path> y luego puede volver a poner la base de datos en línea:

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

Aislar aplicaciones

Es posible apuntar más de una aplicación a la misma base de datos de estado de sesión. Recomiendo contra esto. Es posible que desee apuntar aplicaciones a diferentes bases de datos, quizás incluso a diferentes instancias, para aislar mejor el uso de recursos y proporcionar la máxima flexibilidad para todas sus propiedades web.

Si ya tiene varias aplicaciones que usan la misma base de datos, está bien, pero querrá realizar un seguimiento del impacto que cada aplicación podría tener. Rex Tang de Microsoft publicó una consulta útil para ver el espacio consumido por cada sesión; aquí hay una modificación que resumirá el número de sesiones y el tamaño de sesión total/promedio por aplicación:

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;

Si encuentra que tiene una distribución desequilibrada aquí, puede configurar otra base de datos ASPState en otro lugar y apuntar una o más aplicaciones a esa base de datos en su lugar.

Hacer eliminaciones más amigables

El código para dbo.DeleteExpiredSessions usa un cursor, reemplazando un solo DELETE en implementaciones anteriores. (Esto, creo, se basó en gran medida en esta publicación de Greg Low).

Originalmente el código era:

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

(Y aún puede serlo, dependiendo de dónde descargó la fuente o hace cuánto tiempo instaló ASPState. Hay muchos scripts obsoletos para crear la base de datos, aunque realmente debería usar aspnet_regsql.exe).

Actualmente (a partir de .NET 4.5), el código se ve así (¿alguien sabe cuándo Microsoft comenzará a usar punto y coma?).

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

Mi idea es tener un punto medio feliz aquí:no intente eliminar TODAS las filas de una sola vez, pero tampoco juegue uno por uno. En su lugar, elimine n filas a la vez en transacciones separadas, lo que reduce la duración del bloqueo y también minimiza el impacto en el 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

Querrás experimentar con TOP dependiendo de qué tan ocupado esté su servidor y qué impacto tiene en la duración y el bloqueo. También es posible que desee considerar la implementación del aislamiento de instantáneas; esto generará cierto impacto en tempdb, pero puede reducir o eliminar los bloqueos que se ven desde la aplicación.

Además, de forma predeterminada, el trabajo ASPState_Job_DeleteExpiredSessions corre cada minuto. Considere la posibilidad de retroceder un poco:reduzca el cronograma a quizás cada 5 minutos (y nuevamente, mucho de esto se reducirá a cuán ocupadas están sus aplicaciones y a probar el impacto del cambio). Y por otro lado, asegúrate de que esté habilitado – de lo contrario, su tabla de sesiones crecerá y crecerá sin control.

Sesiones táctiles con menos frecuencia

Cada vez que se carga una página (y, si la aplicación web no se ha creado correctamente, posiblemente varias veces por carga de página), el procedimiento almacenado dbo.TempResetTimeout se llama, asegurándose de que el tiempo de espera para esa sesión en particular se extienda mientras continúe generando actividad. En un sitio web ocupado, esto puede causar un volumen muy alto de actividad de actualización en la tabla dbo.ASPStateTempSessions . Aquí está el código actual para dbo.TempResetTimeout :

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

Ahora, imagina que tienes un sitio web con 500 o 5000 usuarios, y todos ellos están haciendo clic como locos de una página a otra. Esta es probablemente una de las operaciones llamadas con más frecuencia en cualquier implementación de ASPState, y mientras la tabla está tecleada en SessionId – por lo tanto, el impacto de cualquier declaración individual debe ser mínimo; en conjunto, esto puede ser un desperdicio sustancial, incluso en el registro. Si el tiempo de espera de su sesión es de 30 minutos y actualiza el tiempo de espera de una sesión cada 10 segundos debido a la naturaleza de la aplicación web, ¿cuál es el punto de volver a hacerlo 10 segundos después? Siempre que esa sesión se actualice de forma asincrónica en algún momento antes de que transcurran los 30 minutos, no hay una diferencia neta para el usuario o la aplicación. Así que pensé que podría implementar una forma más escalable de "tocar" las sesiones para actualizar sus valores de tiempo de espera.

Una idea que tuve fue implementar una cola de agente de servicio para que la aplicación no tenga que esperar a que ocurra la escritura real:llama a dbo.TempResetTimeout procedimiento almacenado y, a continuación, el procedimiento de activación se hace cargo de forma asincrónica. Pero esto todavía conduce a muchas más actualizaciones (y actividad de registro) de lo que realmente es necesario.

Una mejor idea, en mi humilde opinión, es implementar una tabla de cola en la que solo inserte, y en un horario (de modo que el proceso complete un ciclo completo en un tiempo más corto que el tiempo de espera), solo actualizaría el tiempo de espera para cualquier sesión que ve una vez , sin importar cuántas veces *intentaron* actualizar su tiempo de espera dentro de ese lapso. Entonces, una tabla simple podría verse así:

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

Y luego cambiaríamos el procedimiento de stock para empujar la actividad de la sesión a esta pila en lugar de tocar la tabla de sesiones directamente:

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

El índice agrupado está en smalldatetime columna para evitar divisiones de página (al costo potencial de una página activa), ya que el tiempo del evento para un toque de sesión siempre aumentará de forma monótona.

Luego, necesitaremos un proceso en segundo plano para resumir periódicamente nuevas filas en dbo.SessionStack y actualice dbo.ASPStateTempSessions en consecuencia.

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

Es posible que desee agregar más control transaccional y manejo de errores en torno a esto:solo estoy presentando una idea improvisada, y puede volverse tan loco con esto como quiera. :-)

Podría pensar que le gustaría agregar un índice no agrupado en dbo.SessionStack(SessionId, EventTime DESC) para facilitar el proceso en segundo plano, pero creo que es mejor enfocar incluso las ganancias de rendimiento más minúsculas en el proceso que los usuarios esperan (cada carga de página) en lugar de uno que no esperan (el proceso en segundo plano). Por lo tanto, prefiero pagar el costo de un escaneo potencial durante el proceso en segundo plano que pagar el mantenimiento adicional del índice durante cada inserción. Al igual que con el índice agrupado en la tabla #temp, aquí hay mucho "depende", por lo que es posible que desee jugar con estas opciones para ver dónde funciona mejor su tolerancia.

A menos que la frecuencia de las dos operaciones deba ser drásticamente diferente, programaría esto como parte de ASPState_Job_DeleteExpiredSessions trabajo (y considere cambiar el nombre de ese trabajo si es así) para que estos dos procesos no se pisoteen entre sí.

Una idea final aquí, si encuentra que necesita escalar aún más, es crear múltiples SessionStack tablas, donde cada una es responsable de un subconjunto de sesiones (digamos, hash en el primer carácter de SessionId ). Luego puede procesar cada tabla por turno y mantener esas transacciones mucho más pequeñas. De hecho, también podría hacer algo similar para el trabajo de eliminación. Si se hace correctamente, debería poder colocarlos en trabajos individuales y ejecutarlos simultáneamente, ya que, en teoría, el DML debería afectar conjuntos de páginas completamente diferentes.

Conclusión

Esas son mis ideas hasta ahora. Me encantaría conocer sus experiencias con ASPState:¿Qué tipo de escala ha logrado? ¿Qué tipo de cuellos de botella ha observado? ¿Qué has hecho para mitigarlos?