Sistemas de archivos: XFS

📚 Sistemas de archivos (5 of 7)
  1. 1. Introducción
  2. 2. FAT32
  3. 3. Ext4
  4. 4. NTFS
  5. 5. XFS Estás aquí
  6. 6. Btrfs
  7. 7. ZFS
XFS

En el artículo anterior , exploramos NTFS—un sistema de archivos donde todo es un archivo, desde tus documentos hasta la propia Master File Table. NTFS centralizaba todos los metadatos en la MFT, usando registros basados en atributos y un único journal para mantener los volúmenes de Windows consistentes y con muchas funcionalidades.

Ahora déjame presentarte XFS, el sistema de archivos diseñado para escala extrema. Construido originalmente por Silicon Graphics en 1993 para sus estaciones de trabajo IRIX de gama alta, XFS fue diseñado para manejar sistemas de archivos medidos en terabytes cuando la mayoría de los sistemas todavía contaban en megabytes. Su idea central es dividir el disco en regiones independientes llamadas Allocation Groups—cada una con su propio seguimiento de espacio libre, su propia gestión de inodos y sus propios locks. Esta simple decisión de diseño es lo que permite a XFS escalar linealmente con el número de núcleos de CPU y soportar sistemas de archivos de hasta 8 exabytes.

La otra cosa que notarás de XFS es su amor por los árboles B+. XFS los usa para casi todo—espacio libre, inodos, extents de archivos, reverse mappings, conteo de referencias. Esta consistencia hace que el diseño de XFS sea elegante, aunque la cantidad de maquinaria bajo el capó sea impresionante.

Veamos cómo funciona todo esto, empezando por la estructura que lo hace posible.

El layout físico: Allocation Groups

Como mencionamos antes, XFS divide el disco en regiones llamadas Allocation Groups (AGs para abreviar). Un sistema de archivos pequeño puede tener solo unos pocos, mientras que uno muy grande puede tener miles. Veamos qué significa esto realmente en disco.

Lo primero que notaremos es que XFS no tiene sector de arranque. La mayoría de los sistemas de archivos reservan el primer sector para código de arranque—el fragmento de código máquina que inicia el sistema operativo. XFS se salta esto por completo. La partición comienza inmediatamente con el AG 0, y el AG 0 comienza inmediatamente con el superblock—la estructura que contiene los metadatos generales del sistema de archivos como el tamaño de bloque, el número total de bloques, el número de Allocation Groups y otros datos de configuración.

Layout físico de XFS

Cada AG es un mini sistema de archivos autocontenido. Comienza con cuatro cabeceras de metadatos en sectores secuenciales, seguidas de todos sus bloques de datos:

  1. El superblock, que ya hemos mencionado—tamaño de bloque, número de AGs, UUID, inodo del directorio raíz y otra configuración general
  2. El AGF (AG Free space header), que lleva el control del espacio libre. Contiene las raíces de los árboles B+ de espacio libre (BNO y CNT), así como los árboles de reverse mapping y conteo de referencias
  3. El AGI (AG Inode header), que gestiona los inodos. Contiene las raíces de los árboles B+ de inodos (INO y FINO)
  4. El AGFL (AG Free List), una pequeña reserva de bloques apartados para que los propios metadatos del AG puedan crecer

No te preocupes si esos nombres de árboles no significan nada todavía—los iremos viendo uno a uno. Lo importante es que todos los árboles B+ que gestionan los datos de un AG están enraizados en estas cabeceras. Después de las cabeceras viene el grueso del AG: los bloques de datos donde viven el contenido real de los archivos y los nodos de los árboles B+. Los metadatos del AG 0 no saben nada del AG 3, y viceversa.

Estructura interna de un AG

¿Por qué es tan importante esta independencia entre AGs? Porque permite el paralelismo. Como cada AG tiene sus propios metadatos y sus propios locks, la mayoría de las operaciones en diferentes AGs pueden avanzar sin bloquearse mutuamente. El hilo A puede crear un archivo en el AG 0 mientras el hilo B asigna bloques en el AG 1 al mismo tiempo—muy poca contención. Todavía hay algunos recursos compartidos—el journal y algunos contadores globales—pero el grueso del trabajo es por AG, que es lo que permite a XFS escalar bien con muchos núcleos de CPU.

Hemos mencionado los árboles B+ varias veces ya—aparecen por todas partes en XFS. Antes de ver qué rastrea cada árbol, entendamos cómo funcionan realmente en disco.

Árboles B+: la herramienta universal de XFS

Un árbol B+ es un árbol ordenado y balanceado donde todos los registros reales viven en los nodos hoja de la parte inferior, y los nodos internos de arriba solo contienen claves y punteros para guiarte hacia abajo. Las búsquedas son rápidas—incluso con millones de entradas, solo necesitas unos pocos niveles para llegar a la hoja correcta.

En XFS, cada nodo del árbol ocupa un bloque del sistema de archivos (típicamente 4 KB) del área de datos del AG. La cabecera del AG almacena el número de bloque de la raíz y la profundidad del árbol. XFS usa esta misma infraestructura para todos sus metadatos—espacio libre, inodos, reverse mapping, conteo de referencias, mapas de extents, directorios. Lo único que cambia entre ellos son las claves y los registros que almacenan.

Ahora veamos qué rastrea cada uno de estos árboles, empezando por el espacio libre.

Seguimiento de espacio libre: dos árboles son mejor que uno

Mencionamos que el AG Free space header (AGF) apunta a las raíces de dos árboles B+. Estos dos árboles indexan exactamente los mismos datos de espacio libre, solo que ordenados de forma diferente. El árbol BNO ordena los extents libres por número de bloque. El árbol CNT los ordena por tamaño. Dos árboles para los mismos datos cuesta un poco de overhead extra, pero la razón se hace evidente cuando ves cómo XFS asigna bloques.

XFS usa tres estrategias de asignación dependiendo de lo que necesite el que la solicita. Exact es para cuando necesitas un rango de bloques específico—XFS simplemente busca en el árbol BNO esa ubicación. Near es el caso más común, usado cuando se extiende un archivo existente—XFS busca en ambos árboles en paralelo, y el árbol BNO normalmente gana rápido para asignaciones pequeñas ya que la mayoría de los extents cercanos son suficientemente grandes, mientras que el árbol CNT funciona mejor para asignaciones más grandes, especialmente en sistemas de archivos fragmentados. Size es para cuando no te importa la ubicación, solo necesitas el espacio—por ejemplo, un archivo completamente nuevo. XFS busca en el árbol CNT el primer extent que sea suficientemente grande.

Cuando se asigna en el AG donde ya vive el archivo, XFS usa la estrategia near para mantener los datos juntos. Cuando recurre a otros AGs, cambia a size—la localidad entre AGs importa menos, así que simplemente encuentra el espacio.

El AGF también almacena información de resumen: el número total de bloques libres y el extent libre más largo en este AG. Esto permite a XFS saltar rápidamente AGs que no pueden satisfacer una petición sin siquiera tocar los árboles.

El espacio libre es solo la mitad de la historia—XFS también necesita gestionar inodos, y usa el mismo truco de doble árbol.

Seguimiento de inodos: otro par de árboles

Cada archivo o directorio en XFS tiene un inodo—una estructura que almacena sus metadatos (tamaño, marcas de tiempo, permisos, y punteros a sus datos). El AG Inode header (AGI) apunta a las raíces de otro par de árboles B+ que los gestionan.

XFS asigna inodos en chunks de 64 inodos. El árbol INO rastrea todos los chunks de inodos asignados—dónde viven en disco y qué inodos dentro de cada chunk están ocupados. El árbol FINO (Free INOde tree) es un subconjunto: solo rastrea los chunks que tienen al menos un hueco libre. De esta forma, cuando creas un archivo nuevo, XFS puede saltar directamente a un chunk con espacio disponible en lugar de escanearlos todos.

Sabemos cómo XFS encuentra y asigna inodos. Pero, ¿qué hay realmente dentro de uno?

Dentro de un inodo

Cada archivo, directorio y enlace simbólico en XFS está representado por un inodo. El inodo almacena los metadatos estándar que esperarías: el tamaño del archivo, marcas de tiempo (creación, modificación, acceso), propietario (usuario y grupo), y permisos.

Más allá de esos metadatos, el inodo tiene un área de almacenamiento flexible que XFS divide en hasta dos secciones, llamadas forks:

  • El data fork contiene el contenido principal asociado a este inodo. Lo que eso significa depende del tipo: para un archivo regular, es el mapa de qué bloques de disco contienen los bytes del archivo. Para un directorio, son las entradas del directorio. Para un enlace simbólico, es la ruta de destino.
  • El attribute fork es opcional y almacena metadatos extra que no caben en los campos estándar—cosas como listas de control de acceso (ACLs) o etiquetas personalizadas que las aplicaciones pueden adjuntar a un archivo (lo que Linux llama atributos extendidos).

Para directorios y enlaces simbólicos, si los datos son suficientemente pequeños pueden almacenarse directamente dentro del propio inodo—un enlace simbólico corto o un directorio con un puñado de entradas no necesita lecturas de disco adicionales. Cuando los datos crecen más allá de eso, se desbordan a bloques de disco. Los archivos regulares, sin embargo, siempre usan extents para apuntar a bloques de datos en disco—su contenido nunca se almacena inline en el inodo, incluso si el archivo es diminuto.

Ahora veamos cómo los directorios y archivos usan esta estructura.

Directorios

Un directorio en XFS es realmente un mapeo de nombres de archivo a números de inodo. Cada entrada contiene el nombre del archivo, el número de inodo del archivo o subdirectorio al que apunta, y una etiqueta de tipo de archivo (archivo regular, directorio, enlace simbólico, etc.). Los metadatos reales del archivo—tamaño, marcas de tiempo, permisos—no viven en la entrada del directorio; viven en el inodo al que apunta la entrada.

Cómo se almacenan estas entradas en el data fork depende de cuántas haya. Para directorios muy pequeños—solo unas pocas entradas—todo cabe directamente dentro del inodo. Sin bloques extra necesarios, sin lecturas de disco adicionales.

Cuando el directorio crece y ya no cabe en el inodo, XFS pasa a un formato basado en bloques donde las entradas se almacenan en bloques de datos. Para directorios grandes, XFS construye un árbol B+ indexado por un hash del nombre del archivo. Buscar un archivo en un directorio con un millón de entradas requiere aproximadamente 20 comparaciones para navegar el árbol, frente a escanear potencialmente cientos de miles de entradas linealmente. Esta es una implementación de árbol B+ separada de la usada para espacio libre e inodos—los árboles de directorios están indexados por hash y tienen necesidades de recorrido diferentes—pero el concepto es el mismo.

Los directorios nos dicen dónde están los archivos. Ahora veamos cómo los archivos almacenan sus datos reales en disco.

Archivos

Para archivos regulares, el data fork almacena extents—cada extent describe un rango contiguo de bloques en disco: “empezando en el offset de archivo X, los datos de este archivo viven en los bloques Y a Z.” Cada registro de extent empaqueta el offset del archivo, el número de bloque inicial, la longitud del extent, y un flag que marca si el espacio está preasignado pero aún no escrito. Todo esto cabe en solo 128 bits.

Para archivos pequeños con solo unos pocos extents, estos registros caben directamente dentro del inodo—una lectura de disco te da todo. Cuando un archivo crece y tiene demasiados extents para caber en el inodo, XFS los mueve a un árbol B+ almacenado en bloques separados en disco. El árbol está indexado por offset de archivo, así que encontrar “¿qué bloque de disco almacena el byte 1.000.000 de este archivo?” es un recorrido rápido del árbol.

Ahora sabemos cómo XFS mapea archivos a disco—pero ¿cuándo decide dónde poner los datos?

Delayed allocation y speculative preallocation

XFS usa delayed allocation: cuando escribes en un archivo, los datos van a memoria y XFS reserva espacio en los contadores de espacio libre, pero no elige bloques de disco específicos todavía. La asignación real ocurre después, cuando el kernel vuelca los datos a disco. Para entonces XFS conoce el tamaño completo de la escritura, así que puede asignar un gran extent contiguo en lugar de muchos pequeños dispersos. Menos fragmentación, menos actualizaciones de metadatos.

Para archivos que se escriben secuencialmente—archivos de log, importaciones de bases de datos, grabación de vídeo—XFS va un paso más allá con la speculative preallocation. Asigna espacio extra más allá del final del archivo, empezando pequeño (64 KB) y duplicando cada vez hasta alrededor de 8 GB (el tamaño máximo de extent). Si el archivo deja de crecer, un escáner en segundo plano reclama el espacio no utilizado después de 5 minutos.

Todas estas estructuras—árboles de espacio libre, árboles de inodos, mapas de extents, directorios—se actualizan a medida que se crean y modifican archivos. Pero, ¿qué pasa si el sistema se cae en medio de una actualización?

Journaling: write-ahead logging

XFS usa write-ahead logging para protegerse contra caídas: antes de hacer cualquier cambio de metadatos en disco, escribe un registro de lo que está a punto de hacer en un journal. Si el sistema se cae a mitad de operación, el journal se reproduce al montar para terminar o deshacer los cambios incompletos. XFS solo hace journaling de metadatos (no de datos de archivos), lo que mantiene el journal compacto y rápido.

XFS añade dos optimizaciones encima de esto. Primero, las transacciones no van al journal inmediatamente—se acumulan en un área de preparación en memoria llamada Committed Item List (CIL). Cuando el CIL alcanza un umbral de tamaño, todos los cambios acumulados se vuelcan al journal como un único checkpoint. Este batching significa menos escrituras, pero más grandes, en lugar de muchas pequeñas.

Segundo, una vez que los cambios están en el journal todavía necesitan llegar a sus ubicaciones finales en disco. La Active Item List (AIL) se encarga de esto—un hilo en segundo plano la recorre en orden, empujando las entradas más antiguas primero. Solo cuando los metadatos están en su ubicación permanente se puede reclamar el espacio del journal.

El journal protege contra caídas, pero ¿qué pasa con la corrupción que ocurre silenciosamente—un sector defectuoso, un fallo de hardware? XFS tiene respuesta para eso también.

Reverse mapping: ¿quién es el dueño de este bloque?

La mayoría de los sistemas de archivos rastrean la propiedad en una dirección: dado un archivo, puedes encontrar sus bloques. XFS también rastrea la propiedad en la dirección inversa: dado un bloque físico, puedes encontrar qué archivo (o estructura de metadatos) es su dueño.

Esto se hace a través del árbol B+ de reverse mapping (RMAP), uno por AG. Cada entrada registra un rango de bloques físicos y quién los posee—un número de inodo, o un identificador especial para metadatos internos como el journal o las estructuras del AG. Este índice inverso es lo que permite a XFS reparar metadatos corruptos mientras el sistema de archivos sigue online, y reportar errores en términos de archivos reales en lugar de números de bloque.

El reverse mapping también sustenta el soporte de XFS para bloques compartidos, lo que nos lleva al reflink.

XFS soporta reflink—clonación de archivos copy-on-write. Cuando clonas un archivo con cp --reflink=always, XFS apunta los extents del nuevo archivo a los mismos bloques físicos que el original e incrementa un conteo de referencias. No se duplican datos; la copia es casi instantánea independientemente del tamaño del archivo.

Los conteos de referencias se rastrean en un árbol B+ de conteo de referencias, uno por AG. Cuando después escribes en uno de los archivos clonados, XFS detecta que los bloques afectados están compartidos (conteo de referencias mayor que 1), asigna nuevos bloques para los datos modificados, y decrementa el conteo en los bloques antiguos. El otro archivo no se ve afectado en absoluto.

Ya hemos visto todas las piezas individuales. Veámoslas trabajar juntas.

Juntándolo todo: escribir un archivo

Tracemos qué pasa cuando creas y escribes un archivo de 1 MB, para ver cómo todas estas piezas trabajan juntas.

Primero, XFS necesita un inodo. Elige un AG (basándose en la ubicación del directorio padre) y consulta el árbol FINO para encontrar un chunk con un hueco libre. Asigna el inodo, añade una entrada de directorio al árbol B+ del directorio padre, y registra todo en el CIL.

Ahora escribes 1 MB de datos. Los datos van al page cache en memoria, y XFS marca el extent como “delayed”—reserva 256 bloques en el contador global de espacio libre, pero no elige un AG específico ni bloques específicos todavía. Ninguna asignación de disco ocurre en este punto.

Cuando el kernel decide volcar las páginas sucias, el asignador de XFS entra en acción. Elige el AG preferido (donde vive el inodo del archivo) y usa la estrategia near—buscando en ambos árboles BNO y CNT en paralelo para encontrar 256 bloques contiguos cerca de los datos existentes del archivo. Asigna los bloques, actualiza ambos árboles de espacio libre, registra la propiedad en el árbol RMAP, y convierte el extent delayed en un extent real en el inodo del archivo. Todo esto se registra en el CIL.

El CIL entonces agrega esta transacción con otras transacciones recientes y escribe un único checkpoint en el journal. Finalmente, el hilo en segundo plano del AIL empuja los datos y metadatos reales a sus ubicaciones permanentes en disco. Una vez que todo está escrito, el espacio del journal puede reclamarse.

Todas esas estructuras de las que hemos hablado—Allocation Groups, árboles B+, delayed allocation, el CIL, el AIL—trabajan juntas para hacer esto rápido, contiguo y seguro ante caídas.

¿Es tanta maquinaria que merece la pena la complejidad?

Compromisos

XFS se convirtió en el sistema de archivos preferido para computación empresarial y de alto rendimiento, y con razón. Escala a sistemas de archivos verdaderamente masivos y sistemas multi-core. La delayed allocation y la speculative preallocation mantienen los archivos contiguos. El journal es eficiente gracias al batching del CIL. El RMAP permite la auto-reparación. Y todo esto funciona mientras el sistema de archivos permanece online.

Pero hay compromisos. XFS es complejo—alrededor de 200.000 líneas de código del kernel. Sus estructuras en memoria usan más memoria que sistemas de archivos más simples. Tiene más overhead para archivos muy pequeños (menos de 4 KB) donde la maquinaria de árboles B+ es excesiva. Y notablemente, XFS puede crecer pero no reducirse fácilmente—hay soporte experimental limitado para reducirlo, pero en la práctica si necesitas una partición más pequeña probablemente tendrás que hacer copia de seguridad, reformatear y restaurar.

Para uso de escritorio cotidiano, un sistema de archivos más simple puede ser todo lo que necesites. Pero para sistemas de archivos grandes, archivos grandes y cargas de trabajo paralelas—piensa en servidores de bases de datos, almacenamiento multimedia, computación científica—es difícil superar a XFS.

Terminemos con un breve repaso de todo lo que hemos cubierto.

Resumen

XFS divide el disco en Allocation Groups independientes, cada uno con sus propios árboles B+ para espacio libre, inodos, reverse mapping y conteo de referencias. Los inodos usan dos forks—datos y atributos—y los archivos mapean su contenido a disco mediante extents. La delayed allocation y la speculative preallocation mantienen los archivos contiguos, mientras que el CIL y el AIL hacen el journaling eficiente. El reverse mapping y el reflink completan el conjunto con trazabilidad a nivel de bloque y clonación copy-on-write instantánea.

El corazón de todo es la arquitectura de Allocation Groups—al mantener cada AG verdaderamente independiente, XFS elimina los cuellos de botella globales que limitan a la mayoría de los sistemas de archivos bajo cargas de trabajo paralelas. XFS está construido para escalar.

Todos los sistemas de archivos que hemos cubierto hasta ahora—FAT32, ext4, NTFS y XFS—comparten una característica fundamental: modifican los datos en su lugar. Cuando actualizas un bloque, los nuevos datos sobrescriben los datos antiguos en la misma ubicación del disco. En el próximo artículo , exploraremos Btrfs—un sistema de archivos que toma un enfoque radicalmente diferente. Btrfs nunca modifica los datos en su lugar. Cada cambio crea una nueva copia en una nueva ubicación. Este diseño “copy-on-write” suena despilfarrador, pero permite funcionalidades que son imposibles en sistemas de archivos tradicionales: snapshots instantáneos, RAID integrado con auto-reparación, transacciones atómicas e integridad de datos garantizada.


¿Quieres profundizar más? El código de XFS vive en fs/xfs/ en el árbol del kernel de Linux. Los archivos clave incluyen xfs_alloc.c (asignación), xfs_bmap.c (mapeo de extents), xfs_btree.c (árboles B+), xfs_log.c (journaling), y libxfs/ (código compartido kernel/espacio de usuario). ¡Buena lectura!