Featured image of post Por qué tus datos sobreviven a un corte de energía al guardar: la ingeniería de la consistencia ante fallos

Por qué tus datos sobreviven a un corte de energía al guardar: la ingeniería de la consistencia ante fallos

Análisis técnico profundo de la consistencia ante fallos (Crash Consistency): por qué añadir un bloque de 4 KB requiere múltiples escrituras no atómicas en disco, el reordenamiento en la caché de páginas del SO y el FTL de los SSD, los límites de fsck y Soft Updates, Write-Ahead Logging (WAL) y ARIES, los modos de journaling en ext4, los árboles de Merkle Copy-on-Write (ZFS/Btrfs) y las trampas de fsync().

1. Introducción: el campo minado oculto tras añadir un solo bloque de 4 KB

Cuando pulsas Ctrl+S en un editor de texto o confirmas una transferencia bancaria, tu aplicación emite una llamada al sistema write(). Desde la perspectiva del programador de aplicaciones, guardar datos parece una operación única, instantánea e indivisible.

Sin embargo, bajo el capó del kernel del sistema operativo, el sistema de archivos, el motor de almacenamiento de la base de datos y el controlador SSD se esconde uno de los problemas más difíciles de la ingeniería de sistemas: el problema de la consistencia ante fallos (Crash Consistency Problem).

Consideremos la operación más simple posible en un sistema de archivos UNIX clásico (como UFS o ext2): añadir un único bloque de datos nuevo de $4\,\text{KB}$ ($D_{\text{new}}$) al final de un archivo existente. Incluso esta operación trivial requiere actualizar al menos tres estructuras físicamente separadas en el dispositivo de almacenamiento:

  1. El mapa de bits de datos (Data Bitmap $B$): cambiar el bit del bloque físico $k$ de $0$ (libre) a $1$ (asignado).
  2. El inodo ($I$): actualizar los metadatos del archivo (tamaño size, marca de tiempo mtime y el puntero a la dirección física del bloque $k$).
  3. El bloque de datos ($D_{\text{new}}$): escribir los $4\,\text{KB}$ reales del usuario en el sector físico $k$.

Aquí chocamos con una asimetría física fundamental del hardware: la memoria principal (DRAM) es rápida pero volátil, mientras que los discos y los SSD NAND son no volátiles pero incapaces de actualizar simultáneamente tres bloques distantes $(B, I, D_{\text{new}})$ en una sola acción física atómica. Si se corta la electricidad a mitad de estas tres escrituras ($2^3 - 2 = 6$ estados de escritura parcial), el sistema de archivos despierta al reiniciar en un estado de corrupción crítica:

Persistido en discoPerdido en RAMInconsistencia resultante y gravedad
Solo $D_{\text{new}}$$B,\; I$Pérdida de datos benigna: los datos están en el disco, pero ningún inodo ni bitmap apunta a ellos. La estructura del sistema de archivos permanece intacta.
Solo $B$$I,\; D_{\text{new}}$Fuga de espacio (Space Leak / bloque huérfano): el bloque $k$ figura como ocupado ($1$) en $B$, pero ningún archivo lo referencia; esa capacidad se pierde permanentemente.
Solo $I$ (o $I + D_{\text{new}}$)$B$Doble asignación (Corrupción catastrófica): el inodo $I$ apunta al bloque $k$, pero $B$ lo marca como libre ($0$). Más tarde, otro archivo tomará el bloque $k$ y ambos archivos se destruirán mutuamente.
$I$ y $B$$D_{\text{new}}$Exposición de datos obsoletos (Stale Data Exposure — Vulnerabilidad crítica): el inodo y el bitmap se actualizaron, pero $D_{\text{new}}$ nunca se escribió. ¡Leer el archivo devuelve datos antiguos eliminados de otro usuario (contraseñas o claves privadas) que ocupaban el sector $k$!

2. Por qué las escrituras no llegan al disco en el orden en que las escribiste

En la arquitectura moderna, el orden cronológico de las llamadas write() casi nunca coincide con el orden físico de escritura en el medio no volátil debido a cuatro capas de almacenamiento en búfer volátil y reordenamiento (Reordering):

  flowchart TD
    APP["1. Espacio de usuario<br/>Búferes de tiempo de ejecución (stdio fwrite) / Buffer Pool de BD"]
    PC["2. Caché de páginas del kernel SO (Dirty DRAM Pages)<br/>Write-Back Caching: fusiona y retrasa escrituras durante segundos"]
    SCHED["3. Capa de bloques del SO y planificador de E/S (mq-deadline / BFQ)<br/>Ordena peticiones por dirección LBA para minimizar latencia de búsqueda"]
    DEV["4. Caché DRAM volátil integrada en el disco y colas NCQ/NVMe<br/>El controlador FTL del SSD reordena escrituras entre canales NAND"]
    MEDIA[("5. Medio físico no volátil<br/>Celdas Flash NAND / Platos magnéticos (¡Solo esto sobrevive a un apagón!)")]

    APP -->|"Llamada al sistema write()"| PC
    PC -->|"Flush en segundo plano / fsync()"| SCHED
    SCHED -->|"Comandos NVMe / SATA"| DEV
    DEV -->|"Barrera FLUSH CACHE / FUA"| MEDIA

Para garantizar el orden y la durabilidad, el sistema debe emitir barreras de hardware explícitas (NVMe Flush, SATA FLUSH CACHE EXT o el bit FUA: Force Unit Access) que obliguen a la unidad a vaciar su propio búfer DRAM volátil hacia las celdas NAND.


3. Generación 1: Actualizaciones en el lugar, fsck y Soft Updates

  1. fsck (File System Consistency Check) y el muro de escalabilidad: Los primeros sistemas UNIX (FFS, ext2) reparaban las inconsistencias tras un fallo escaneando todos los inodos, bitmaps y directorios del disco antes de montar (fsck). Como su tiempo de ejecución es proporcional a la capacidad total del disco $O(N_{\text{disk}})$, verificar un volumen moderno de varios terabytes lleva horas o días de inactividad, ¡incluso si solo se estaba modificando un archivo de $4\,\text{KB}$ cuando falló la corriente!
  2. Soft Updates (Ganger & McKusick 1994): Adoptado en FreeBSD UFS, evitaba el registro en diario rastreando las dependencias de punteros en memoria y rompiendo dependencias cíclicas mediante operaciones temporales de roll-back y roll-forward en RAM antes de escribir el bloque en disco.

4. El pilar del almacenamiento moderno: Write-Ahead Logging (WAL) y Journaling

Para reducir el tiempo de recuperación de $O(N_{\text{disk}})$ al trabajo en vuelo en el instante del fallo $O(N_{\text{active}})$ (¡milisegundos!), los sistemas de archivos (ext3/ext4, XFS, NTFS) y las bases de datos emplean Write-Ahead Logging (WAL) / Journaling:

  1. Journal Write: antes de tocar las ubicaciones definitivas (Home Locations), el sistema añade secuencialmente al Journal un bloque descriptor de inicio TxB (con TID) seguido de las imágenes actualizadas $(I, B, D_{\text{new}})$.
  2. Journal Commit (Punto de confirmación atómica): tras una barrera de vaciado de caché de hardware, se escribe un único bloque de confirmación TxE (Transaction End). ¡La escritura de ese único sector TxE es el interruptor atómico que valida toda la transacción!
  3. Checkpoint: una vez que TxE está en el disco, $(I, B, D_{\text{new}})$ se copian de forma asíncrona a sus ubicaciones definitivas.
  flowchart LR
    subgraph Journal ["Diario circular en disco (Adición secuencial)"]
        TXB["1. TxB (TID=42)<br/>Descriptor"] --> JB["2. Bitmap B registrado"]
        JB --> JI["3. Inodo I registrado"]
        JI --> JD["4. Datos D_new registrados"]
        JD ==>|"★Barrera de hardware FLUSH"| TXE["5. TxE (TID=42)<br/>Bloque Commit atómico"]
    end

    subgraph Home ["Ubicaciones definitivas (Home Locations)"]
        HB["Bitmap definitivo"]
        HI["Tabla de inodos definitiva"]
        HD["Bloque de datos definitivo"]
    end

    TXE ==>|"6. Checkpoint (Escritura asíncrona)"| HB
    TXE ==>|"6. Checkpoint"| HI
    TXE ==>|"6. Checkpoint"| HD
  • Apagón ANTES de TxE: las ubicaciones definitivas están intactas; al reiniciar, la entrada incompleta simplemente se descarta.
  • Apagón DESPUÉS de TxE: al reiniciar, el gestor de recuperación reproduce (Redo replay, operación idempotente) los bloques del diario hacia sus ubicaciones definitivas en milisegundos. ext4 elimina incluso la barrera intermedia mediante sumas de comprobación del diario (CRC32C) dentro de TxE.

Para evitar duplicar los bytes escritos ($2\times$ Write Amplification), el modo predeterminado data=ordered de ext4 solo registra metadatos $(I, B)$ en el diario, pero obliga a que el bloque de datos $D_{\text{new}}$ se escriba por completo en su ubicación definitiva ANTES de permitir que el bloque TxE de los metadatos se confirme en el diario. En bases de datos relacionales, el protocolo ARIES (Mohan et al., 1992) gestiona la política Steal / No-Force mediante marcas pageLSN y tres fases: Analysis, Redo (Repeating History) y Undo (con Compensation Log Records, CLRs).


5. Generación 3: Copy-on-Write (CoW) y árboles de Merkle (ZFS / Btrfs)

Sistemas de archivos como ZFS, Btrfs y APFS prohíben sobrescribir bloques existentes (In-Place Overwrite) y adoptan Copy-on-Write (CoW / Shadow Paging):

  1. Todo bloque modificado $D_{2,\text{new}}$ y su inodo padre $I_{v2}$ se escriben siempre en espacio libre nuevo, manteniendo intactos los bloques antiguos $D_{2,\text{old}}$.
  2. Cuando todos los bloques hijos nuevos están seguros en disco, una única escritura atómica actualiza el bloque raíz (Uberblock en ZFS) de $U_1$ a $U_2$.
  3. Ventajas: recuperación instantánea en $0$ segundos sin reproducir diarios, instantáneas (Snapshots) en $O(1)$ sin costo de copia y autorreparación (Self-Healing) contra la degradación silenciosa de bits (Bit Rot) mediante árboles de Merkle con sumas criptográficas de 256 bits en cada puntero padre.

6. Las trampas de fsync() para desarrolladores y las pruebas de inyección de fallos

Incluso sobre un sistema con journaling o CoW, reemplazar un archivo mediante rename("config.json.tmp", "config.json") sin llamar antes a fsync() produce archivos vacíos de $0$ bytes tras un apagón bajo Delayed Allocation (ext4), porque el cambio de nombre de los metadatos se confirma antes de que se vacíen las páginas de datos en caché.

El protocolo completo de 6 pasos para un guardado atómico a prueba de fallos es: (1) write() en el archivo temporal $\to$ (2) fsync(fd) sobre el archivo temporal $\to$ (3) close(fd) $\to$ (4) rename() atómico $\to$ (5) abrir el directorio padre y ejecutar fsync(dir_fd) para persistir la entrada del directorio $\to$ (6) close(dir_fd).

Como demostró el escándalo fsyncgate de PostgreSQL en 2018 (donde el kernel de Linux marcaba una página sucia como Clean tras un error de E/S EIO durante fsync(), corregido mediante errseq_t), la ingeniería de almacenamiento moderna depende de marcos de inyección sistemática de fallos como ALICE y CrashMonkey, que exploran exhaustivamente todas las permutaciones de reordenamiento de hardware ante cortes de energía.

comments powered by Disqus