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

Comprender los vaciados del búfer de registro

Probablemente haya escuchado muchas veces antes que SQL Server proporciona una garantía para las propiedades de transacción ACID. Este artículo se centra en la parte D, que por supuesto significa durabilidad. Más específicamente, este artículo se centra en un aspecto de la arquitectura de registro de SQL Server que impone la durabilidad de las transacciones:vaciados de búfer de registro. Hablo sobre la función que cumple el búfer de registro, las condiciones que obligan a SQL Server a vaciar el búfer de registro en el disco, lo que puede hacer para optimizar el rendimiento de la transacción, así como las tecnologías relacionadas agregadas recientemente, como la durabilidad retardada y la memoria de clase de almacenamiento no volátil.

Vaciados de búfer de registro

La parte D en las propiedades de la transacción ACID representa durabilidad. En el nivel lógico, significa que cuando una aplicación envía a SQL Server una instrucción para confirmar una transacción (explícitamente o con una transacción de confirmación automática), SQL Server normalmente devuelve el control a la persona que llama solo después de que puede garantizar que la transacción es duradera. En otras palabras, una vez que la persona que llama recuperó el control después de realizar una transacción, puede confiar en que incluso si un momento después el servidor experimenta un corte de energía, los cambios de la transacción se trasladaron a la base de datos. Siempre que el servidor se reinicie correctamente y los archivos de la base de datos no estén dañados, encontrará que se han aplicado todos los cambios de transacción.

La forma en que SQL Server hace cumplir la durabilidad de las transacciones, en parte, es garantizar que todos los cambios de la transacción se escriban en el registro de transacciones de la base de datos en el disco. antes de devolver el control a la persona que llama. En caso de una falla de energía después de que se reconoció la confirmación de una transacción, sabe que todos esos cambios se escribieron al menos en el registro de transacciones en el disco. Ese es el caso incluso si las páginas de datos relacionadas se modificaron solo en el caché de datos (el grupo de búfer) pero aún no se descargaron a los archivos de datos en el disco. Cuando reinicia SQL Server, durante la fase de rehacer del proceso de recuperación, SQL Server usa la información registrada en el registro para reproducir los cambios que se aplicaron después del último punto de control y que no llegaron a los archivos de datos. Hay un poco más en la historia según el modelo de recuperación que esté usando y si las operaciones masivas se aplicaron después del último punto de control, pero para los fines de nuestra discusión, basta con centrarse en la parte que implica fortalecer los cambios en el registro de transacciones.

La parte complicada de la arquitectura de registro de SQL Server es que las escrituras de registro son secuenciales. Si SQL Server no hubiera utilizado algún tipo de búfer de registro para aliviar las escrituras de registros en el disco, los sistemas de escritura intensiva, especialmente los que involucran muchas transacciones pequeñas, se encontrarían rápidamente con terribles cuellos de botella de rendimiento relacionados con la escritura de registros.

Para aliviar el impacto negativo en el rendimiento de las frecuentes escrituras de registro secuenciales en el disco, SQL Server utiliza un búfer de registro en la memoria. Las escrituras de registro se realizan primero en el búfer de registro y ciertas condiciones hacen que SQL Server vacíe o endurezca el búfer de registro en el disco. La unidad reforzada (también conocida como bloque de registro) puede variar desde un tamaño mínimo de sector (512 bytes) hasta un máximo de 60 KB. Las siguientes son condiciones que desencadenan un vaciado del búfer de registro (ignore las partes que aparecen entre corchetes por ahora):

  • SQL Server recibe una solicitud de confirmación de una transacción [totalmente duradera] que cambia los datos [en una base de datos que no sea tempdb]
  • El búfer de registro se llena, alcanzando su capacidad de 60 KB
  • SQL Server necesita reforzar las páginas de datos sucios, por ejemplo, durante un proceso de punto de control, y los registros que representan los cambios en esas páginas aún no se han reforzado (registro de escritura anticipada , o WAL para abreviar)
  • Solicita manualmente un vaciado del búfer de registro ejecutando el procedimiento sys.sp_flush_log
  • SQL Server escribe un nuevo valor de recuperación relacionado con la caché de secuencias [en una base de datos que no sea tempdb]

Las primeras cuatro condiciones deberían ser bastante claras, si ignora por ahora la información entre corchetes. El último quizás aún no esté claro, pero lo explicaré en detalle más adelante en el artículo.

El tiempo que SQL Server espera para que se complete una operación de E/S que maneja un vaciado del búfer de registro se refleja en el tipo de espera WRITELOG.

Entonces, ¿por qué esta información es tan interesante y qué hacemos con ella? Comprender las condiciones que activan los vaciados del búfer de registro puede ayudarlo a descubrir por qué ciertas cargas de trabajo experimentan cuellos de botella relacionados. Además, en algunos casos, existen acciones que puede realizar para reducir o eliminar dichos cuellos de botella. Cubriré una serie de ejemplos, como una transacción grande frente a muchas transacciones pequeñas, transacciones totalmente duraderas frente a transacciones duraderas retrasadas, base de datos de usuario frente a tempdb y almacenamiento en caché de objetos de secuencia.

Una transacción grande frente a muchas transacciones pequeñas

Como se mencionó, una de las condiciones que desencadena un vaciado del búfer de registro es cuando confirma una transacción para garantizar la durabilidad de la transacción. Esto significa que las cargas de trabajo que involucran muchas transacciones pequeñas, como las cargas de trabajo OLTP, pueden experimentar cuellos de botella relacionados con la escritura de registros.

Aunque este no suele ser el caso, si tiene una sola sesión que envía muchos cambios pequeños, una forma simple y efectiva de optimizar el trabajo es aplicar los cambios en una sola transacción grande en lugar de varias pequeñas.

Considere el siguiente ejemplo simplificado (descargue PerformanceV3 aquí):

SET NOCOUNT ON;
 
USE PerformanceV3;
 
ALTER DATABASE PerformanceV3 SET DELAYED_DURABILITY = Disabled; -- default
 
DROP TABLE IF EXISTS dbo.T1;
 
CREATE TABLE dbo.T1(col1 INT NOT NULL);
 
DECLARE @i AS INT = 1;
 
WHILE @i <= 1000000
BEGIN
 
  BEGIN TRAN
    INSERT INTO dbo.T1(col1) VALUES(@i);
  COMMIT TRAN;
 
  SET @i += 1;
END;

Este código ejecuta 1,000,000 de pequeñas transacciones que cambian datos en una base de datos de usuarios. Este trabajo desencadenará al menos 1 000 000 de vaciados del búfer de registro. Podría obtener algunos adicionales debido a que el búfer de registro se está llenando. Puede utilizar la siguiente plantilla de prueba para contar el número de vaciados del búfer de registro y medir el tiempo que tardó en completarse el trabajo:

-- Test template
 
-- ... Preparation goes here ...
 
-- Count log flushes and measure time
DECLARE @logflushes AS INT, @starttime AS DATETIME2, @duration AS INT;
 
-- Stats before
SET @logflushes = ( SELECT cntr_value FROM sys.dm_os_performance_counters
                    WHERE counter_name = 'Log Flushes/sec'
                      AND instance_name = @db );
 
SET @starttime = SYSDATETIME();
 
-- ... Actual work goes here ...
 
-- Stats after
SET @duration = DATEDIFF(second, @starttime, SYSDATETIME());
SET @logflushes = ( SELECT cntr_value FROM sys.dm_os_performance_counters
                    WHERE counter_name = 'Log Flushes/sec'
                      AND instance_name = @db ) - @logflushes;
 
SELECT 
  @duration AS durationinseconds,
  @logflushes AS logflushes;

Aunque el nombre del contador de rendimiento es Log Flushes/sec, en realidad sigue acumulando el número de descargas del búfer de registro hasta el momento. Por lo tanto, el código resta el recuento anterior al trabajo del recuento posterior al trabajo para calcular el recuento de vaciados de registros generados por el trabajo. Este código también mide el tiempo en segundos que tomó completar el trabajo. Aunque no hago esto aquí, podría, si quisiera, calcular de manera similar la cantidad de registros y el tamaño escrito en el registro por el trabajo consultando los estados antes y después del trabajo del fn_dblog función.

Para nuestro ejemplo anterior, la siguiente es la parte que debe colocar en la sección de preparación de la plantilla de prueba:

-- Preparation
SET NOCOUNT ON;
USE PerformanceV3;
 
ALTER DATABASE PerformanceV3 SET DELAYED_DURABILITY = Disabled;
 
DROP TABLE IF EXISTS dbo.T1;
 
CREATE TABLE dbo.T1(col1 INT NOT NULL);
 
DECLARE @db AS sysname = N'PerformanceV3';
 
DECLARE @logflushes AS INT, @starttime AS DATETIME2, @duration AS INT;

Y la siguiente es la parte que debe colocar en la sección de trabajo real:

-- Actual work
DECLARE @i AS INT = 1;
 
WHILE @i <= 1000000
BEGIN
 
  BEGIN TRAN
    INSERT INTO dbo.T1(col1) VALUES(@i);
  COMMIT TRAN;
 
  SET @i += 1;
END;

En total, obtienes el siguiente código:

-- Example test with many small fully durable transactions in user database
-- ... Preparation goes here ...
 
-- Preparation
SET NOCOUNT ON;
USE PerformanceV3;
 
ALTER DATABASE PerformanceV3 SET DELAYED_DURABILITY = Disabled;
 
DROP TABLE IF EXISTS dbo.T1;
 
CREATE TABLE dbo.T1(col1 INT NOT NULL);
 
DECLARE @db AS sysname = N'PerformanceV3';
 
DECLARE @logflushes AS INT, @starttime AS DATETIME2, @duration AS INT;
 
-- Stats before
SET @logflushes = ( SELECT cntr_value FROM sys.dm_os_performance_counters
 
                    WHERE counter_name = 'Log Flushes/sec'
 
                      AND instance_name = @db );
 
SET @starttime = SYSDATETIME();
 
-- ... Actual work goes here ...
 
-- Actual work
DECLARE @i AS INT = 1;
 
WHILE @i <= 1000000
BEGIN
 
  BEGIN TRAN
    INSERT INTO dbo.T1(col1) VALUES(@i);
  COMMIT TRAN;
 
  SET @i += 1;
END;
 
-- Stats after
SET @duration = DATEDIFF(second, @starttime, SYSDATETIME());
 
SET @logflushes = ( SELECT cntr_value FROM sys.dm_os_performance_counters
                    WHERE counter_name = 'Log Flushes/sec'
                      AND instance_name = @db ) - @logflushes;
 
SELECT 
  @duration AS durationinseconds,
  @logflushes AS logflushes;

Este código tardó 193 segundos en completarse en mi sistema y desencadenó 1 000 036 vaciados de búfer de registro. Eso es muy lento, pero puede explicarse debido a la gran cantidad de vaciados de registros.

En las cargas de trabajo típicas de OLTP, diferentes sesiones envían pequeños cambios en diferentes transacciones pequeñas al mismo tiempo, por lo que no es como si realmente tuviera la opción de encapsular muchos cambios pequeños en una sola transacción grande. Sin embargo, si su situación es que todos los pequeños cambios se envían desde la misma sesión, una forma sencilla de optimizar el trabajo es encapsularlo en una sola transacción. Esto le dará dos beneficios principales. Una es que su trabajo escribirá menos registros. Con 1 000 000 de transacciones pequeñas, cada transacción en realidad escribe tres registros:uno para comenzar la transacción, uno para el cambio y otro para confirmar la transacción. Entonces, está viendo alrededor de 3,000,0000 registros de registro de transacciones versus un poco más de 1,000,000 cuando se ejecuta como una gran transacción. Pero lo que es más importante, con una gran transacción, la mayoría de los vaciados de registro se activan solo cuando el búfer de registro se llena, más un vaciado de registro más al final de la transacción cuando se confirma. La diferencia de rendimiento puede ser bastante significativa. Para probar el trabajo en una gran transacción, use el siguiente código en la parte de trabajo real de la plantilla de prueba:

-- Actual work
BEGIN TRAN;
 
DECLARE @i AS INT = 1;
 
WHILE @i <= 1000000
BEGIN
 
  INSERT INTO dbo.T1(col1) VALUES(@i);
  SET @i += 1;
 
END;
 
COMMIT TRAN;

En mi sistema, este trabajo se completó en 7 segundos y desencadenó 1758 vaciados de registros. Aquí hay una comparación entre las dos opciones:

#transactions  log flushes  duration in seconds
-------------- ------------ --------------------
1000000        1000036      193
1              1758         7

Pero nuevamente, en las cargas de trabajo típicas de OLTP, realmente no tiene la opción de reemplazar muchas transacciones pequeñas enviadas desde diferentes sesiones con una transacción grande enviada desde la misma sesión.

Transacciones totalmente duraderas frente a transacciones duraderas diferidas

A partir de SQL Server 2014, puede usar una característica llamada durabilidad retrasada que le permite mejorar el rendimiento de las cargas de trabajo con muchas transacciones pequeñas, incluso si se envían en diferentes sesiones, al sacrificar la garantía normal de durabilidad total. Al confirmar una transacción duradera retrasada, SQL Server reconoce la confirmación tan pronto como el registro de confirmación se escribe en el búfer de registro, sin desencadenar un vaciado del búfer de registro. El búfer de registro se vacía debido a cualquiera de las otras condiciones antes mencionadas, como cuando se llena, pero no cuando se confirma una transacción duradera retrasada.

Antes de usar esta función, debe pensar con mucho cuidado si es adecuada para usted. En términos de rendimiento, su impacto es significativo solo en cargas de trabajo con muchas transacciones pequeñas. Si, para empezar, su carga de trabajo implica principalmente grandes transacciones, probablemente no verá ninguna ventaja en el rendimiento. Más importante aún, debe darse cuenta del potencial de pérdida de datos. Digamos que la aplicación comete una transacción duradera retrasada. Se escribe un registro de compromiso en el búfer de registro y se reconoce de inmediato (el control se devuelve a la persona que llama). Si SQL Server experimenta un corte de energía antes de que se vacíe el búfer de registro, después del reinicio, el proceso de recuperación deshace todos los cambios realizados por la transacción, aunque la aplicación crea que se confirmó.

Entonces, ¿cuándo está bien usar esta función? Un caso obvio es cuando la pérdida de datos no es un problema, como este ejemplo de Melissa Connors de SentryOne. Otra es cuando, después de un reinicio, tiene los medios para identificar qué cambios no llegaron a la base de datos y puede reproducirlos. Si su situación no cae en una de estas dos categorías, no use esta función a pesar de la tentación.

Para trabajar con transacciones duraderas retrasadas, debe configurar una opción de base de datos llamada DELAYED_DURABILITY. Esta opción se puede establecer en uno de estos tres valores:

  • Deshabilitado (predeterminado):todas las transacciones en la base de datos son completamente duraderas y, por lo tanto, cada confirmación desencadena un vaciado del búfer de registro
  • Obligado :todas las transacciones en la base de datos tienen un retraso duradero y, por lo tanto, las confirmaciones no desencadenan un vaciado del búfer de registro
  • Permitido :a menos que se indique lo contrario, las transacciones son totalmente duraderas y al realizarlas se activa un vaciado del búfer de registro; sin embargo, si usa la opción DELAYED_DURABILITY =ON en una instrucción COMMIT TRAN o en un bloque atómico (de un proceso compilado de forma nativa), esa transacción en particular se retrasa y, por lo tanto, confirmarla no activa un vaciado del búfer de registro

Como prueba, use el siguiente código en la sección de preparación de nuestra plantilla de prueba (observe que la opción de la base de datos está establecida en Forzado):

-- Preparation
SET NOCOUNT ON;
USE PerformanceV3; -- http://tsql.solidq.com/SampleDatabases/PerformanceV3.zip
 
ALTER DATABASE PerformanceV3 SET DELAYED_DURABILITY = Forced;
 
DROP TABLE IF EXISTS dbo.T1;
 
CREATE TABLE dbo.T1(col1 INT NOT NULL);
 
DECLARE @db AS sysname = N'PerformanceV3';

Y use el siguiente código en la sección de trabajo real (aviso, 1,000,000 transacciones pequeñas):

-- Actual work
DECLARE @i AS INT = 1;
 
WHILE @i <= 1000000
BEGIN
 
  BEGIN TRAN
    INSERT INTO dbo.T1(col1) VALUES(@i);
  COMMIT TRAN;
 
  SET @i += 1;
END;

Como alternativa, puede usar el modo Permitido en el nivel de la base de datos y luego, en el comando COMMIT TRAN, agregar CON (DELAYED_DURABILITY =ON).

En mi sistema, el trabajo tardó 22 segundos en completarse y provocó 95 407 vaciados de registro. Eso es más tiempo que ejecutar el trabajo como una gran transacción (7 segundos) ya que se generan más registros (recuerde, por transacción, uno para comenzar la transacción, uno para el cambio y uno para confirmar la transacción); sin embargo, es mucho más rápido que los 193 segundos que tardó el trabajo en completarse utilizando 1 000 000 de transacciones totalmente duraderas, ya que la cantidad de vaciados de registros se redujo de más de 1 000 000 a menos de 100 000. Además, con la durabilidad retrasada, obtendrá una mejora en el rendimiento incluso si las transacciones se envían desde diferentes sesiones en las que no es una opción usar una transacción grande.

Para demostrar que no hay ningún beneficio en usar la durabilidad retrasada cuando se hace el trabajo como grandes transacciones, mantenga el mismo código en la parte de preparación de la última prueba y use el siguiente código en la parte de trabajo real:

-- Actual work
BEGIN TRAN;
 
DECLARE @i AS INT = 1;
 
WHILE @i <= 1000000
BEGIN
  INSERT INTO dbo.T1(col1) VALUES(@i);
 
  SET @i += 1;
END;
 
COMMIT TRAN;

Obtuve 8 segundos de tiempo de ejecución (en comparación con 7 para una gran transacción completamente duradera) y 1759 vaciados de registro (en comparación con 1758). Los números son esencialmente los mismos, pero con la transacción duradera demorada, existe el riesgo de pérdida de datos.

Este es un resumen de las cifras de rendimiento de las cuatro pruebas:

durability          #transactions  log flushes  duration in seconds
------------------- -------------- ------------ --------------------
full                1000000        1000036      193
full                1              1758         7
delayed             1000000        95407        22
delayed             1              1759         8

Memoria de clase de almacenamiento

La función de durabilidad retrasada puede mejorar significativamente el rendimiento de las cargas de trabajo de estilo OLTP que involucran una gran cantidad de pequeñas transacciones de actualización que requieren alta frecuencia y baja latencia. El problema es que corre el riesgo de perder datos. ¿Qué sucede si no puede permitir ninguna pérdida de datos, pero aún desea ganancias de rendimiento similares a las de durabilidad retrasada, donde el búfer de registro no se vacía para cada confirmación, sino cuando se llena? A todos nos gusta comer el pastel y tenerlo también, ¿verdad?

Puede lograr esto en SQL Server 2016 SP1 o posterior mediante el uso de memoria de clase de almacenamiento, también conocida como almacenamiento no volátil NVDIMM-N. Este hardware es esencialmente un módulo de memoria que le brinda un rendimiento de nivel de memoria, pero la información allí persiste y, por lo tanto, no se pierde cuando se corta la energía. La adición en SQL Server 2016 SP1 le permite configurar el búfer de registro como persistente en dicho hardware. Para hacer esto, configure el SCM como un volumen en Windows y formatéelo como un volumen de modo de acceso directo (DAX). Luego, agrega un archivo de registro a la base de datos mediante el comando normal ALTER DATABASE ADD LOG FILE, con la ruta del archivo que reside en el volumen DAX, y establece el tamaño en 20 MB. SQL Server, a su vez, reconoce que es un volumen DAX y, desde ese momento, trata el búfer de registro como uno persistente en ese volumen. Los eventos de confirmación de transacciones ya no desencadenan vaciados del búfer de registro, sino que una vez que la confirmación se registró en el búfer de registro, SQL Server sabe que en realidad persiste y, por lo tanto, devuelve el control a la persona que llama. Cuando el búfer de registro se llena, SQL Server lo vacía a los archivos de registro de transacciones en el almacenamiento tradicional.

Para obtener más detalles sobre esta función, incluidas las cifras de rendimiento, consulte Aceleración de la latencia de confirmación de transacciones mediante la memoria de clase de almacenamiento en Windows Server 2016/SQL Server 2016 SP1 por Kevin Farlee.

Curiosamente, SQL Server 2019 mejora la compatibilidad con la memoria de clase de almacenamiento más allá del escenario de caché de registro persistente. Admite la colocación de archivos de datos, archivos de registro y archivos de punto de control OLTP en memoria en dicho hardware. Todo lo que necesita hacer es exponerlo como un volumen en el nivel del sistema operativo y formatearlo como una unidad DAX. SQL Server 2019 reconoce automáticamente esta tecnología y funciona de una manera iluminada modo, accediendo directamente al dispositivo, sin pasar por la pila de almacenamiento del sistema operativo. ¡Bienvenido al futuro!

Base de datos de usuarios frente a tempdb

Por supuesto, la base de datos tempdb se crea desde cero como una copia nueva de la base de datos modelo cada vez que reinicia SQL Server. Como tal, nunca es necesario recuperar ningún dato que escriba en tempdb, ya sea que lo escriba en tablas temporales, variables de tabla o tablas de usuario. Todo se ha ido después de reiniciar. Sabiendo esto, SQL Server puede relajar muchos de los requisitos relacionados con el registro. Por ejemplo, independientemente de si habilita o no la opción de durabilidad retrasada, los eventos de confirmación no desencadenan un vaciado del búfer de registro. Además, la cantidad de información que debe registrarse se reduce, ya que SQL Server solo necesita la información suficiente para respaldar las transacciones revertidas o deshacer el trabajo, si es necesario, pero no revertir las transacciones ni rehacer el trabajo. Como resultado, los registros de transacciones que representan cambios en un objeto en tempdb tienden a ser más pequeños en comparación con cuando el mismo cambio se aplica a un objeto en una base de datos de usuario.

Para demostrar esto, ejecutará las mismas pruebas que ejecutó anteriormente en PerformanceV3, solo que esta vez en tempdb. Comenzaremos con la prueba de muchas transacciones pequeñas cuando la opción de la base de datos DELAYED_DURABILITY esté configurada como Deshabilitada (predeterminada). Utilice el siguiente código en la sección de preparación de la plantilla de prueba:

-- Preparation
SET NOCOUNT ON;
USE tempdb;
 
ALTER DATABASE tempdb SET DELAYED_DURABILITY = Disabled;
 
DROP TABLE IF EXISTS dbo.T1;
 
CREATE TABLE dbo.T1(col1 INT NOT NULL);
 
DECLARE @db AS sysname = N'tempdb';

Utilice el siguiente código en la sección de trabajo real:

-- Actual work
DECLARE @i AS INT = 1;
 
WHILE @i <= 1000000
BEGIN
 
  BEGIN TRAN
    INSERT INTO dbo.T1(col1) VALUES(@i);
  COMMIT TRAN;
 
  SET @i += 1;
END;

Este trabajo generó 5095 vaciados de registros y tardó 19 segundos en completarse. Eso se compara con más de un millón de descargas de registros y 193 segundos en una base de datos de usuario con total durabilidad. Eso es incluso mejor que con la durabilidad retrasada en una base de datos de usuario (95 407 vaciados de registros y 22 segundos) debido al tamaño reducido de los registros.

Para probar una transacción grande, deje la sección de preparación sin cambios y use el siguiente código en la sección de trabajo real:

-- Actual work
BEGIN TRAN;
 
DECLARE @i AS INT = 1;
 
WHILE @i <= 1000000
BEGIN
  INSERT INTO dbo.T1(col1) VALUES(@i);
 
  SET @i += 1;
END;
 
COMMIT TRAN;

Obtuve 1.228 descargas de registro y 9 segundos de tiempo de ejecución. Eso se compara con 1758 vaciados de registro y 7 segundos de tiempo de ejecución en la base de datos del usuario. El tiempo de ejecución es similar, incluso un poco más rápido en la base de datos del usuario, pero podría haber pequeñas variaciones entre las pruebas. Los tamaños de los registros en tempdb se reducen y, por lo tanto, obtiene menos vaciados de registros en comparación con la base de datos de usuarios.

También puede intentar ejecutar las pruebas con la opción DELAYED_DURABILITY configurada en Forced, pero esto no tendrá impacto en tempdb ya que, como se mencionó, de todos modos, los eventos de confirmación no desencadenan un vaciado de registros en tempdb.

Estas son las medidas de rendimiento para todas las pruebas, tanto en la base de datos de usuarios como en tempdb:

database       durability          #transactions  log flushes  duration in seconds
-------------- ------------------- -------------- ------------ --------------------
PerformanceV3  full                1000000        1000036      193
PerformanceV3  full                1              1758         7
PerformanceV3  delayed             1000000        95407        22
PerformanceV3  delayed             1              1759         8
tempdb         full                1000000        5095         19
tempdb         full                1              1228         9
tempdb         delayed             1000000        5091         18
tempdb         delayed             1              1226         9

Almacenamiento en caché de objetos de secuencia

Quizás un caso sorprendente que desencadena vaciados del búfer de registro está relacionado con la opción de caché de objetos de secuencia. Considere como ejemplo la siguiente definición de secuencia:

CREATE SEQUENCE dbo.Seq1 AS BIGINT MINVALUE 1 CACHE 50; -- the default cache size is 50;

Cada vez que necesite un nuevo valor de secuencia, use la función PRÓXIMO VALOR PARA, así:

SELECT NEXT VALUE FOR dbo.Seq1;

La propiedad CACHE es una característica de rendimiento. Sin él, cada vez que se solicitaba un nuevo valor de secuencia, SQL Server habría tenido que escribir el valor actual en el disco con fines de recuperación. De hecho, ese es el comportamiento que obtienes cuando usas el modo SIN CACHE. En cambio, cuando la opción se establece en un valor mayor que cero, SQL Server escribe un valor de recuperación en el disco solo una vez por cada número de solicitudes del tamaño de la memoria caché. SQL Server mantiene dos miembros en la memoria, con el tamaño del tipo de secuencia, uno que contiene el valor actual y otro que contiene la cantidad de valores que quedan antes de que se necesite la siguiente escritura en disco del valor de recuperación. En caso de un corte de energía, al reiniciar, SQL Server establece el valor de secuencia actual en el valor de recuperación.

Esto es probablemente mucho más fácil de explicar con un ejemplo. Considere la definición de secuencia anterior con la opción CACHE establecida en 50 (predeterminada). Solicita un nuevo valor de secuencia por primera vez ejecutando la instrucción SELECT anterior. SQL Server establece los miembros antes mencionados en los siguientes valores:

On disk recovery value: 50, In-memory current value: 1, In-memory values left: 49, You get: 1

49 solicitudes más no tocarán el disco, sino que solo actualizarán los miembros de la memoria. Después de 50 solicitudes en total, los miembros se establecen en los siguientes valores:

On disk recovery value: 50, In-memory current value: 50, In-memory values left: 0, You get: 50

Realice otra solicitud de un nuevo valor de secuencia y esto desencadena una escritura en disco del valor de recuperación 100. Los miembros se establecen en los siguientes valores:

On disk recovery value: 100, In-memory current value: 51, In-memory values left: 49, You get: 51

Si en este punto el sistema experimenta un corte de energía, después de reiniciar, el valor de la secuencia actual se establece en 100 (el valor recuperado del disco). La siguiente solicitud de un valor de secuencia produce 101 (escribiendo el valor de recuperación 150 en el disco). Perdió todos los valores en el rango de 52 a 100. Lo máximo que puede perder debido a una terminación no limpia del proceso de SQL Server, como en el caso de un corte de energía, es tantos valores como el tamaño de la memoria caché. La compensación es clara; cuanto mayor sea el tamaño de la memoria caché, menos escrituras en disco del valor de recuperación y, por lo tanto, mejor será el rendimiento. Al mismo tiempo, mayor será la brecha que se puede generar entre dos valores de secuencia en caso de un corte de energía.

Todo esto es bastante sencillo, y tal vez esté muy familiarizado con su funcionamiento. Lo que podría sorprender es que cada vez que SQL Server escribe un nuevo valor de recuperación en el disco (cada 50 solicitudes en nuestro ejemplo), también fortalece el búfer de registro. Ese no es el caso con la propiedad de la columna de identidad, aunque SQL Server usa internamente la misma función de almacenamiento en caché para la identidad como lo hace para el objeto de secuencia, simplemente no le permite controlar su tamaño. Está activado de forma predeterminada con el tamaño 10000 para BIGINT y NUMERIC, 1000 para INT, 100 para SMALLINT y 10 PARA TINYINT. Si lo desea, puede desactivarlo con el indicador de rastreo 272 o la opción de configuración de ámbito IDENTITY_CACHE (2017+). La razón por la que SQL Server no necesita vaciar el búfer de registro al escribir el valor de recuperación relacionado con la memoria caché de identidad en el disco es que solo se puede crear un nuevo valor de identidad al insertar una fila en una tabla. En caso de un corte de energía, una fila insertada en una tabla por una transacción que no se comprometió se extraerá de la tabla como parte del proceso de recuperación de la base de datos cuando se reinicie el sistema. Por lo tanto, incluso si después del reinicio, SQL Server genera el mismo valor de identidad que el creado en la transacción que no se comprometió, no hay posibilidad de duplicados ya que la fila se eliminó de la tabla. Si la transacción se hubiera confirmado, esto habría desencadenado un vaciado de registros, que también persistiría en la escritura de un valor de recuperación relacionado con la memoria caché. Por lo tanto, Microsoft no se sintió obligado a vaciar el búfer de registro cada vez que se produce una escritura del valor de recuperación relacionada con la caché de identidad.

Con el objeto de secuencia la situación es diferente. Una aplicación puede solicitar un nuevo valor de secuencia y no almacenarlo en la base de datos. En caso de una falla de energía después de la creación de un nuevo valor de secuencia en una transacción que no se comprometió, después del reinicio, SQL Server no tiene forma de decirle a la aplicación que no confíe en ese valor. Por lo tanto, para evitar la creación de un nuevo valor de secuencia después del reinicio que sea igual a un valor de secuencia generado previamente, SQL Server fuerza un vaciado de registro cada vez que se escribe en el disco un nuevo valor de recuperación relacionado con la caché de secuencia. Una excepción a esta regla es cuando el objeto de secuencia se crea en tempdb, por supuesto que no hay necesidad de tales vaciados de registros ya que de todos modos, después de reiniciar el sistema, tempdb se crea de nuevo.

Un impacto negativo en el rendimiento de los frecuentes vaciados de registros es especialmente notable cuando se usa un tamaño de caché de secuencia muy pequeño y en una transacción que genera mucho valor de secuencia, por ejemplo, cuando se insertan muchas filas en una tabla. Sin la secuencia, dicha transacción endurecería principalmente el búfer de registro cuando se llena, además de una vez más cuando se confirma la transacción. Pero con la secuencia, obtiene un vaciado de registro cada vez que se realiza una escritura en disco de un valor de recuperación. Es por eso que desea evitar el uso de un tamaño de caché pequeño, por no hablar del modo SIN CACHE.

Para demostrar esto, use el siguiente código en la sección de preparación de nuestra plantilla de prueba:

-- Preparation
SET NOCOUNT ON;
USE PerformanceV3; -- try PerformanceV3, tempdb
 
ALTER DATABASE PerformanceV3         -- try PerformanceV3, tempdb
  SET DELAYED_DURABILITY = Disabled; -- try Disabled, Forced
 
DROP TABLE IF EXISTS dbo.T1;
 
DROP SEQUENCE IF EXISTS dbo.Seq1;
 
CREATE SEQUENCE dbo.Seq1 AS BIGINT MINVALUE 1 CACHE 50; -- try NO CACHE, CACHE 50, CACHE 10000
 
DECLARE @db AS sysname = N'PerformanceV3'; -- try PerformanceV3, tempdb

Y el siguiente código en la sección de trabajo real:

-- Actual work
SELECT
  -- n -- to test without seq
  NEXT VALUE FOR dbo.Seq1 AS n -- to test sequence
INTO dbo.T1
FROM PerformanceV3.dbo.GetNums(1, 1000000) AS N;

Este código usa una transacción para escribir 1,000,000 de filas en una tabla usando la declaración SELECT INTO, generando tantos valores de secuencia como el número de filas insertadas.

Tal como se indica en los comentarios, ejecute la prueba con NO CACHE, CACHE 50 y CACHE 10000, tanto en PerformanceV3 como en tempdb, y pruebe tanto las transacciones duraderas como las duraderas retrasadas.

Estos son los números de rendimiento que obtuve en mi sistema:

database       durability          cache     log flushes  duration in seconds
-------------- ------------------- --------- ------------ --------------------
PerformanceV3  full                NO CACHE  1000047      171
PerformanceV3  full                50        20008        4
PerformanceV3  full                10000     339          < 1
tempdb         full                NO CACHE  96           4
tempdb         full                50        74           1
tempdb         full                10000     8            < 1
PerformanceV3  delayed             NO CACHE  1000045      166
PerformanceV3  delayed             50        20011        4
PerformanceV3  delayed             10000     334          < 1
tempdb         delayed             NO CACHE  91           4
tempdb         delayed             50        74           1
tempdb         delayed             10000     8            < 1
8 <100100 retrasado

Hay bastantes cosas interesantes para notar.

Con NO CACHE, obtiene un vaciado de registro para cada valor de secuencia único generado. Por lo tanto, se recomienda encarecidamente evitarlo.

Con un tamaño de caché de secuencia pequeño, aún obtiene muchos vaciados de registro. Tal vez la situación no sea tan mala como con NO CACHE, pero observe que la carga de trabajo tardó 4 segundos en completarse con el tamaño de caché predeterminado de 50 en comparación con menos de un segundo con el tamaño de 10 000. Yo personalmente uso 10,000 como mi valor preferido.

In tempdb you don’t get log flushes when a sequence cache-related recovery value is written to disk, but the recovery value is still written to disk every cache-sized number of requests. That’s perhaps surprising since such a value would never need to be recovered. Therefore, even when using a sequence object in tempdb, I’d still recommend using a large cache size.

Also notice that delayed durability doesn’t prevent the need for log flushes every time the sequence cache-related recovery value is written to disk.

Conclusión

This article focused on log buffer flushes. Understanding this aspect of SQL Server’s logging architecture is important especially in order to be able to optimize OLTP-style workloads that require high frequency and low latency. Workloads using In-Memory OLTP included, of course. You have more options with newer features like delayed durability and persisted log buffer with storage class memory. Make sure you’re very careful with the former, though, since it does incur potential for data loss unlike the latter.

Be careful not to use the sequence object with a small cache size, not to speak of the NO CACHE mode. I find the default size 50 too small and prefer to use 10,000. I’ve heard people expressing concerns that with a cache size 10000, after multiple power failures they might lose all the values in the type. However, even with a four-byte INT type, using only the positive range, 10,000 fits 214,748 times. If your system experience that many power failures, you have a completely different problem to worry about. Therefore, I feel very comfortable with a cache size of 10,000.