Buffer Pool scan en startup: cómo un servidor SQL con 2.7 TB de RAM tardó 9 horas en arrancar
> Caso real ocurrido en mayo de 2026 sobre el nodo primario de un cluster AlwaysOn de Data Warehouse en producción. Nombres de servidores y bases de datos enmascarados; los datos técnicos (RAM, tamaño de buffer pool, tiempos de recovery, números de buffers escaneados) son los reales del incidente.
## Las 9 horas más silenciosas de mi vida como DBA
Eran las 09:43 de la mañana cuando aplicamos el restart programado al nodo primario del Data Warehouse. Mantenimiento aprobado, ventana corta, todo el mundo avisado: "el servicio estará disponible en 30-45 minutos, máximo una hora". Una restart rutinario sobre una instancia SQL Server 2019. Nada nuevo bajo el sol.
A las 11:00 algunas bases de datos seguían en RECOVERING. Pensamos: "BDs grandes, paciencia". A las 13:00, nueve de las BDs más voluminosas seguían en recovery, sin emitir nuevos mensajes en el errorlog durante minutos enteros. CPU al 100% en varios cores, sin presión de memoria, sin bloqueos visibles, sin errores. El servidor no estaba colgado — estaba trabajando en silencio.
A las 18:30 — casi 9 horas después del restart — la última BD pasó a ONLINE. El recovery propiamente dicho de cada base de datos había durado segundos. El resto fueron horas de un proceso que no salía en ningún dashboard estándar: el buffer pool scan.
Si tu instancia tiene más de 1 TB de RAM asignada a SQL Server, esto te afecta directamente. Lo cuento porque ojalá alguien me lo hubiera contado a mí antes.
---
## El contexto
El servidor en cuestión es un nodo primario de un AlwaysOn dedicado a BI y reporting analítico:
- Versión: SQL Server 2019 (build con CU reciente).
- RAM asignada a SQL Server: 2,7 TB.
- Número de bases de datos: ~216.
- Snapshots activos: 9 (creados por jobs internos de reporting y validación).
- Tamaño del buffer pool: cientos de millones de buffers (8 KB por buffer → en torno a 165-346 millones por BD grande).
El restart no era opcional. Había que aplicar un cambio de configuración del Resource Governor que requería reinicio del servicio. Llevábamos meses preparándolo, con un plan de comunicación y una ventana de mantenimiento bendecida por negocio.
Lo que no estaba en el plan era que el servidor tardase casi un turno entero de trabajo en volver a aceptar conexiones útiles.
---
## El problema en números
Cuando empezamos a investigar a las 13:00, esto era lo que veíamos:
| BD | Estado a las 13:00 | Tiempo total en RECOVERING |
|----|--------------------|------------------------------|
| BD analítica (financiero) | RECOVERING | ~110 min |
| BD operacional (logística) | RECOVERING | ~63 min |
| BD de reporting (siniestros) | RECOVERING | ~64 min |
| BD analítica (clientes) | RECOVERING | 5 min (cascada acelerada) |
| ...y otras 5 BDs grandes | RECOVERING | entre 30 y 180 min |
El detalle más perturbador era este: el recovery propiamente dicho de cada BD, una vez arrancaba, duraba milisegundos o pocos segundos. Una de las BDs grandes con redo paralelo (16 workers) completó toda su fase de redo en 180 ms. 180 milisegundos. Pero antes de eso, había estado más de una hora esperando "algo".
Ese "algo" era el buffer pool scan. Y no aparecía como tal en ningún sitio obvio del SSMS.
---
## El diagnóstico: el mensaje que casi nadie mira
El primer breakthrough llegó cuando, en lugar de mirar sys.dm_exec_requests o el dashboard de AlwaysOn, fuimos directamente al errorlog buscando una cadena concreta:
`sql
SET LOCK_TIMEOUT 5000;
DECLARE @t TABLE(
LogDate datetime,
ProcessInfo nvarchar(100),
Text nvarchar(max)
);
INSERT INTO @t
EXEC xp_readerrorlog 0, 1, N'Buffer Pool scan';
SELECT TOP 30 LogDate, Text
FROM @t
ORDER BY LogDate DESC;
`
El resultado fueron docenas de líneas como estas:
`
Buffer Pool scan took 50 seconds: database ID 104, command 'DB STARTUP',
operation 'FlushCache', scanned buffers 3985,
total iterated buffers 165769652
Buffer Pool scan took 52 seconds: database ID 211, command 'DB STARTUP',
operation 'MarkBuffersCopyOnWrite', scanned buffers 12,
total iterated buffers 165395230
`
Lo importante de leer esa salida:
- total iterated buffers: 165 millones. Eso son ~1,3 TB de buffer pool iterados por cada BD, por cada operación, secuencialmente.
- scanned buffers: 3.985 (de 165 millones). El motor itera todo el buffer pool buscando los pocos buffers que realmente pertenecen a esa BD. El scan es lineal y no se puede saltar.
- 50 segundos: por cada operación. Cada BD con snapshot dispara dos operaciones (FlushCache + MarkBuffersCopyOnWrite). Con 9 BDs grandes y snapshots: 18 scans × 50 s = 15 minutos solo de scans en el mejor caso.
Y eso era el mejor caso. El peor llegaba cuando varias sesiones DB STARTUP se solapaban en paralelo.
---
## El factor multiplicador: contención de spinlocks
Con 8 sesiones DB STARTUP ejecutándose simultáneamente (porque SQL Server hace recovery en paralelo según MAXDOP y configuración), todos los scans competían por los mismos recursos internos. Una consulta a sys.dm_os_spinlock_stats durante el incidente reveló:
- Spinlock BUF_HASH: ~28 millones de spins.
- Spinlock FCB_REPLICA_SYNC: ~197 millones de spins.
Cuando ocho threads están iterando 165 millones de buffers cada uno y compitiendo por el mismo hash table, los tiempos no escalan linealmente. Escalan de forma cuadrática. Un scan que solo debería costar 10 segundos se convierte en 50. Cinco scans encadenados se convierten en una hora.
Y el motor no emite ningún mensaje durante todo ese tiempo. Ni Recovery of database X is N% complete, ni warnings, ni nada. Solo silencio y CPU al 100%.
---
## La causa raíz: KB 4566579
El comportamiento está documentado oficialmente por Microsoft en KB 4566579 — Operations that trigger buffer pool scan may run slowly on large-memory computers.
Citando textualmente la documentación oficial:
> "Prior to SQL Server 2022, there was no way to eliminate this problem."
Y a continuación:
> "In SQL Server 2022, this problem is mitigated because buffer pool scans are parallelized by utilizing multiple cores. There will be one task per 8 million buffers (64 GB) where a serial scan will still be used if there are less than 8 million buffers."
Resumiendo en lenguaje humano:
1. Operaciones como DB STARTUP, BACKUP DATABASE, CREATE DATABASE SNAPSHOT o DROP DATABASE disparan internamente un escaneo completo del buffer pool.
2. En SQL Server 2016, 2017, 2019 y builds antiguas de 2022, ese scan es single-thread y lineal sobre el número total de buffers.
3. En servidores con buffer pool de varios TB, eso son cientos de millones de buffers a iterar por cada operación.
4. SQL Server 2022 paraleliza el scan automáticamente: 1 task por cada 8M buffers (64 GB). Para 2,7 TB serían ~40 tareas paralelas. Reducción esperada del orden de 15-30x en el tiempo de cada scan.
El mensaje Buffer Pool scan took N seconds que vimos en el errorlog se introdujo en SQL Server 2016 SP3 / 2017 CU23 / 2019 CU9. Si tu instancia es más antigua, el problema existe pero no lo verás en el errorlog — y eso es aún peor.
---
## La mitigación: checklist pre-restart obligatorio
Mientras no se pueda migrar a SQL Server 2022, no hay solución definitiva. Solo hay mitigación paliativa. Este es el checklist que ahora aplicamos religiosamente antes de cualquier restart programado en servidores con >1 TB de buffer pool:
### 1. Eliminar snapshots activos ANTES del restart
Cada snapshot que dejes vivo añadirá un buffer pool scan completo (MarkBuffersCopyOnWrite) durante el siguiente arranque.
`sql
-- Listar todos los snapshots
SELECT
DB_NAME(database_id) AS snapshot_name,
DB_NAME(source_database_id) AS source_db,
state_desc,
create_date
FROM sys.databases
WHERE source_database_id IS NOT NULL;
-- Eliminar cada snapshot antes del restart
DROP DATABASE [snapshot_name];
`
Cada snapshot eliminado pre-restart ahorra un scan completo de 30-60 segundos post-restart.
### 2. Cerrar transacciones largas abiertas
Una transacción enorme con undo largo se traduce en una fase 3 de recovery (undo phase) muy lenta. En el incidente, una de las BDs operacionales tardó 78 minutos de recovery total, de los cuales 58 minutos fueron solo undo.
`sql
SELECT
s.session_id,
DATEDIFF(MINUTE, dt.database_transaction_begin_time, GETDATE()) AS min_abierta,
dt.database_transaction_log_bytes_used / 1024 / 1024 AS log_used_mb,
s.host_name,
s.program_name
FROM sys.dm_tran_database_transactions dt
JOIN sys.dm_tran_session_transactions st ON dt.transaction_id = st.transaction_id
JOIN sys.dm_exec_sessions s ON st.session_id = s.session_id
WHERE DATEDIFF(MINUTE, dt.database_transaction_begin_time, GETDATE()) > 30
ORDER BY min_abierta DESC;
`
### 3. CHECKPOINT manual antes del restart
Forzar un CHECKPOINT en las BDs grandes justo antes del restart minimiza el redo pendiente y reduce la duración de la fase 2 de recovery.
### 4. Verificar el tamaño real del buffer pool
Antes de cualquier mantenimiento, conviene saber con qué te enfrentas:
`sql
SELECT
total_physical_memory_kb / 1024 / 1024 AS server_ram_gb,
available_physical_memory_kb / 1024 / 1024 AS server_ram_libre_gb
FROM sys.dm_os_sys_memory;
SELECT name, value_in_use
FROM sys.configurations
WHERE name = 'max server memory (MB)';
`
Si max server memory > 1 TB, este patrón es un riesgo material y debe contemplarse explícitamente en la planificación.
### 5. Comunicar el downtime esperado
Con 2-3 TB de buffer pool, varias BDs grandes y snapshots activos, hay que comunicar a negocio un downtime esperado de 3-8 horas, no de "30-45 minutos". La diferencia entre una incidencia gestionada y una crisis es haber avisado a tiempo.
---
## El anti-patrón clásico (NO lo hagas)
`sql
-- DBCC DROPCLEANBUFFERS NO es solución.
-- Microsoft lo desaconseja explícitamente porque degrada la performance al
-- perder cache caliente, y no evita el scan posterior. Es peor el remedio
-- que la enfermedad.
`
Reiniciar el servicio "a ver qué pasa" con snapshots activos sobre BDs voluminosas es una receta para horas de downtime silencioso. Cada snapshot añade un scan completo. Multiplica por número de BDs grandes y tienes el incidente del caso real.
---
## Lo que aprendimos
Tres conclusiones operativas que aplicamos desde el día siguiente del incidente:
1. El errorlog tiene oro escondido. El mensaje Buffer Pool scan took N seconds existe desde 2019 CU9 y casi nadie lo monitoriza. Añadimos una alerta automática en SQL Agent que escanea el errorlog cada hora buscando esta cadena y la registra en una tabla histórica. Si en alguna ventana de mantenimiento futura vemos picos de 50+ segundos, ya sabemos qué esperar.
2. La RAM "gratis" no es gratis. Hace años, asignar 2,7 TB de RAM a SQL Server parecía la mejor decisión posible — "más cache caliente, más performance". Pero por encima de cierto umbral, la RAM tiene un coste operativo oculto: el tiempo de arranque crece linealmente con el tamaño del buffer pool en versiones <2022. En servidores nuevos seguimos asignando RAM generosa, pero ahora con la conciencia explícita de que cada TB adicional son ~30-60 segundos extra por cada operación que dispare buffer pool scan.
3. SQL Server 2022 no es opcional en servidores con RAM masiva. El cambio del scan paralelizado en 2022 no es una mejora cosmética. Para un servidor con 2,7 TB de buffer pool, es la diferencia entre 9 horas de recovery y 20-30 minutos. La migración a 2022 pasó de "lo haremos cuando toque" a estar en el roadmap del trimestre siguiente.
---
## Conclusión
Buffer pool scan en startup es uno de esos problemas que no existen hasta que llegas a cierta escala — y cuando llega, te cuesta un turno entero de trabajo descubrir qué demonios está pasando. No hay error, no hay bloqueo, no hay presión de memoria. Solo silencio.
Si tienes una instancia SQL Server con más de 1 TB de RAM asignada y estás en una versión anterior a 2022, asume que cualquier restart te va a costar horas, no minutos. Aplica el checklist pre-restart, elimina los snapshots antes, comunica el downtime real a negocio, y prioriza la migración a SQL Server 2022 si el negocio depende del tiempo de arranque.
El coste de no hacerlo lo pagas en horas de downtime el día que menos te lo esperas. Lo sé porque lo pagué.