Eliminar el rendimiento de los datos LOB en SQL Server

16

Esta pregunta está relacionada con este hilo del foro .

Ejecuto SQL Server 2008 Developer Edition en mi estación de trabajo y un clúster de máquina virtual de dos nodos Enterprise Edition donde me refiero a "clúster alfa".

El tiempo que lleva eliminar filas con una columna varbinary (max) está directamente relacionado con la longitud de los datos en esa columna. Eso puede sonar intuitivo al principio, pero después de la investigación, choca con mi comprensión de cómo SQL Server realmente elimina filas en general y trata este tipo de datos.

El problema surge de un problema de tiempo de espera de eliminación (> 30 segundos) que estamos viendo en nuestra aplicación web .NET, pero lo he simplificado en aras de esta discusión.

Cuando se elimina un registro, SQL Server lo marca como un fantasma para ser limpiado por una tarea de limpieza de Ghost en un momento posterior después de que se confirme la transacción (vea el blog de Paul Randal ). En una prueba que elimina tres filas con datos de 16 KB, 4 MB y 50 MB en una columna varbinary (max), respectivamente, veo que esto sucede en la página con la parte de fila de los datos, así como en la transacción Iniciar sesión.

Lo que me parece extraño es que los bloqueos X se colocan en todas las páginas de datos LOB durante la eliminación, y las páginas se desasignan en el PFS. Veo esto en el registro de transacciones, así como con sp_locky los resultados del dm_db_index_operational_statsDMV ( page_lock_count).

Esto crea un cuello de botella de E / S en mi estación de trabajo y nuestro clúster alfa si esas páginas aún no están en la memoria caché del búfer. De hecho, el page_io_latch_wait_in_msdel mismo DMV es prácticamente la duración total de la eliminación, y page_io_latch_wait_countcorresponde con el número de páginas bloqueadas. Para el archivo de 50 MB en mi estación de trabajo, esto se traduce en más de 3 segundos al comenzar con un caché de búfer vacío ( checkpoint/ dbcc dropcleanbuffers), y no tengo dudas de que sería más largo por una gran fragmentación y bajo carga.

Traté de asegurarme de que no se tratara de asignar espacio en el caché para ocupar ese tiempo. Leí en 2 GB de datos de otras filas antes de ejecutar la eliminación en lugar del checkpointmétodo, que es más de lo que se asigna al proceso de SQL Server. No estoy seguro de si esa es una prueba válida o no, ya que no sé cómo SQL Server baraja los datos. Asumí que siempre expulsaría lo viejo en favor de lo nuevo.

Además, ni siquiera modifica las páginas. Esto lo puedo ver con dm_os_buffer_descriptors. Las páginas están limpias después de la eliminación, mientras que el número de páginas modificadas es inferior a 20 para las tres eliminaciones pequeñas, medianas y grandes. También comparé la salida de DBCC PAGEuna muestra de las páginas buscadas, y no hubo cambios (solo ALLOCATEDse eliminó el bit de PFS). Simplemente los desasigna.

Para probar aún más que las búsquedas de páginas / las ubicaciones de negocios están causando el problema, probé la misma prueba usando una columna de flujo de archivos en lugar de varilla binaria (max). Las eliminaciones fueron de tiempo constante, independientemente del tamaño de LOB.

Entonces, primero mis preguntas académicas:

  1. ¿Por qué SQL Server necesita buscar todas las páginas de datos LOB para bloquearlas con X? ¿Es eso solo un detalle de cómo se representan los bloqueos en la memoria (almacenados de alguna manera con la página)? Esto hace que el impacto de E / S dependa en gran medida del tamaño de los datos si no se almacena en caché por completo.
  2. ¿Por qué la X se bloquea en absoluto, solo para desasignarlos? ¿No es suficiente bloquear solo la hoja de índice con la parte de la fila, ya que la desasignación no necesita modificar las páginas en sí? ¿Hay alguna otra forma de obtener los datos LOB contra los que protege el bloqueo?
  3. ¿Por qué desasignar las páginas por adelantado, dado que ya hay una tarea de fondo dedicada a este tipo de trabajo?

Y quizás más importante, mi pregunta práctica:

  • ¿Hay alguna forma de hacer que las eliminaciones funcionen de manera diferente? Mi objetivo es la eliminación constante del tiempo, independientemente del tamaño, similar a la secuencia de archivos, donde cualquier limpieza ocurre en segundo plano después del hecho. ¿Es una cosa de configuración? ¿Estoy almacenando cosas de forma extraña?

Aquí se explica cómo reproducir la prueba descrita (ejecutada a través de la ventana de consulta SSMS):

CREATE TABLE [T] (
    [ID] [uniqueidentifier] NOT NULL PRIMARY KEY,
    [Data] [varbinary](max) NULL
)

DECLARE @SmallID uniqueidentifier
DECLARE @MediumID uniqueidentifier
DECLARE @LargeID uniqueidentifier

SELECT @SmallID = NEWID(), @MediumID = NEWID(), @LargeID = NEWID()
-- May want to keep these IDs somewhere so you can use them in the deletes without var declaration

INSERT INTO [T] VALUES (@SmallID, CAST(REPLICATE(CAST('a' AS varchar(max)), 16 * 1024) AS varbinary(max)))
INSERT INTO [T] VALUES (@MediumID, CAST(REPLICATE(CAST('a' AS varchar(max)), 4 * 1024 * 1024) AS varbinary(max)))
INSERT INTO [T] VALUES (@LargeID, CAST(REPLICATE(CAST('a' AS varchar(max)), 50 * 1024 * 1024) AS varbinary(max)))

-- Do this before test
CHECKPOINT
DBCC DROPCLEANBUFFERS
BEGIN TRAN

-- Do one of these deletes to measure results or profile
DELETE FROM [T] WHERE ID = @SmallID
DELETE FROM [T] WHERE ID = @MediumID
DELETE FROM [T] WHERE ID = @LargeID

-- Do this after test
ROLLBACK

Estos son algunos resultados del perfil de las eliminaciones en mi estación de trabajo:

El | Tipo de columna | Eliminar tamaño | Duración (ms) | Lee | Escribe | CPU |
-------------------------------------------------- ------------------
El | VarBinary | 16 KB | 40 | 13 2 | 0 |
El | VarBinary | 4 MB | 952 | 2318 | 2 | 0 |
El | VarBinary | 50 MB | 2976 | 28594 | 1 | 62 |
-------------------------------------------------- ------------------
El | FileStream | 16 KB | 1 | 12 | 1 | 0 |
El | FileStream | 4 MB | 0 | 9 | 0 | 0 |
El | FileStream | 50 MB | 1 | 9 | 0 | 0 |

No podemos necesariamente usar filestream en su lugar porque:

  1. Nuestra distribución del tamaño de los datos no lo garantiza.
  2. En la práctica, agregamos datos en muchos fragmentos, y filestream no admite actualizaciones parciales. Tendríamos que diseñar alrededor de esto.

Actualización 1

Probé una teoría de que los datos se escriben en el registro de transacciones como parte de la eliminación, y este no parece ser el caso. ¿Estoy probando esto incorrectamente? Vea abajo.

SELECT MAX([Current LSN]) FROM fn_dblog(NULL, NULL)
--0000002f:000001d9:0001

BEGIN TRAN
DELETE FROM [T] WHERE ID = @ID

SELECT
    SUM(
        DATALENGTH([RowLog Contents 0]) +
        DATALENGTH([RowLog Contents 1]) +
        DATALENGTH([RowLog Contents 3]) +
        DATALENGTH([RowLog Contents 4])
    ) [RowLog Contents Total],
    SUM(
        DATALENGTH([Log Record])
    ) [Log Record Total]
FROM fn_dblog(NULL, NULL)
WHERE [Current LSN] > '0000002f:000001d9:0001'

Para un archivo de más de 5 MB de tamaño, esto regresó 1651 | 171860.

Además, esperaría que las páginas en sí estuvieran sucias si los datos se escribieran en el registro. Parece que solo se registran las asignaciones de negocios, lo que coincide con lo que está sucio después de la eliminación.

Actualización 2

Recibí una respuesta de Paul Randal. Afirmó el hecho de que tiene que leer todas las páginas para atravesar el árbol y encontrar qué páginas desasignar, y afirmó que no hay otra manera de buscar qué páginas. Esta es una media respuesta a 1 y 2 (aunque no explica la necesidad de bloqueos en los datos fuera de la fila, pero eso es poca cosa).

La pregunta 3 todavía está abierta: ¿por qué desasignar las páginas por adelantado si ya hay una tarea en segundo plano para hacer la limpieza de las eliminaciones?

Y, por supuesto, la pregunta más importante: ¿hay alguna forma de mitigar directamente (es decir, no evitar) este comportamiento de eliminación dependiente del tamaño? Creo que este sería un problema más común, a menos que realmente seamos los únicos que almacenemos y eliminemos filas de 50 MB en SQL Server. ¿Todos los demás trabajan alrededor de esto con algún tipo de trabajo de recolección de basura?

Jeremy Rosenberg
fuente
Desearía que hubiera una solución mejor, pero no he encontrado una. Tengo una situación de registro de grandes volúmenes de filas de diferentes tamaños, hasta 1 MB +, y tengo un proceso de "purga" para eliminar registros antiguos. Debido a que las eliminaciones fueron muy lentas, tuve que dividirlo en dos pasos: primero elimine las referencias entre tablas (que es muy rápido), luego elimine las filas huérfanas. El trabajo de eliminación promedió ~ 2.2 segundos / MB para eliminar datos. Por supuesto, tuve que reducir la contención, así que tengo un procedimiento almacenado con "DELETE TOP (250)" dentro de un bucle hasta que ya no se eliminen filas.
Abacus

Respuestas:

5

No puedo decir por qué exactamente sería mucho más ineficiente eliminar un VARBINARIO (MAX) en comparación con la secuencia de archivos, pero una idea que podría considerar si solo está tratando de evitar los tiempos de espera de su aplicación web al eliminar estos LOBS. Puede almacenar los valores VARBINARY (MAX) en una tabla separada (llamemos tblLOB) a la que hace referencia la tabla original (llamemos a esto tblParent).

Desde aquí, cuando elimine un registro, puede eliminarlo del registro principal y luego tener un proceso ocasional de recolección de basura para ingresar y limpiar los registros en la tabla LOB. Puede haber actividad adicional en el disco duro durante este proceso de recolección de basura, pero al menos estará separado de la web front-end y se puede realizar durante las horas no pico.

Ian Chamberland
fuente
Gracias. Esa es exactamente una de nuestras opciones en el tablero. La tabla es un sistema de archivos, y actualmente estamos en el proceso de separar los datos binarios en una base de datos completamente separada del meta de jerarquía. Podríamos hacer lo que usted dijo y eliminar la fila de la jerarquía, y hacer que un proceso de GC limpie las filas LOB huérfanas. O tenga una marca de tiempo de eliminación con los datos para lograr el mismo objetivo. Esta es la ruta que podemos tomar si no hay una respuesta satisfactoria al problema.
Jeremy Rosenberg
1
Sería cauteloso de tener solo una marca de tiempo para indicar que se ha eliminado. Eso funcionará, pero eventualmente tendrá mucho espacio ocupado ocupado en filas activas. Necesitará tener algún tipo de proceso de gc en algún momento, dependiendo de cuánto se elimine, y será menos impactante eliminar menos de forma regular en lugar de lotes de forma ocasional.
Ian Chamberland