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?