DEV Community

Cover image for TigerFS: un filesystem adentro de PostgreSQL (y por qué esta obsesión colectiva me parece un síntoma)
Juan Torchia
Juan Torchia

Posted on • Originally published at juanchi.dev

TigerFS: un filesystem adentro de PostgreSQL (y por qué esta obsesión colectiva me parece un síntoma)

POSIX define 17 llamadas al sistema para manejar archivos. PostgreSQL implementa las 17 adentro de tablas relacionales. Cuando vi eso en el README de TigerFS tuve que cerrar la laptop, respirar, y volver a abrirla.

No porque sea útil. Es claramente un experimento. Sino porque alguien se tomó el trabajo de mapear open(), read(), write(), mkdir(), unlink() — todo — sobre filas y columnas de Postgres. Y lo hizo funcionar.

El año pasado metí el historial de git de Linux en una base de datos y lo llamé arqueología. Ahora alguien hizo lo inverso: tomó algo que existe antes que los sistemas gestores de bases de datos modernos — el concepto mismo de filesystem — y lo metió adentro de uno. Hay algo en esta obsesión colectiva de meter todo adentro de todo que me parece el síntoma de algo más grande.

Lo instalé. Lo rompí dos veces. Y creo que entiendo por qué existe.

TigerFS y la idea detrás del filesystem sobre Postgres

TigerFS es un filesystem en espacio de usuario (FUSE) que usa PostgreSQL como backend de almacenamiento. Eso significa que cuando escribís un archivo, no va a disk directamente — va a una tabla. Cuando creás un directorio, insertás una fila. Cuando borrás un archivo, ejecutás un DELETE.

El schema es elegante en su brutalidad:

-- Tabla principal de inodos
CREATE TABLE inodes (
  inode_id    BIGSERIAL PRIMARY KEY,
  parent_id   BIGINT REFERENCES inodes(inode_id),
  name        TEXT NOT NULL,
  type        CHAR(1) NOT NULL, -- 'f' archivo, 'd' directorio, 'l' symlink
  size        BIGINT DEFAULT 0,
  mode        INTEGER DEFAULT 493, -- 0755 en octal
  uid         INTEGER DEFAULT 0,
  gid         INTEGER DEFAULT 0,
  atime       TIMESTAMPTZ DEFAULT NOW(),
  mtime       TIMESTAMPTZ DEFAULT NOW(),
  ctime       TIMESTAMPTZ DEFAULT NOW()
);

-- Los datos reales van acá, particionados en bloques
CREATE TABLE blocks (
  inode_id    BIGINT REFERENCES inodes(inode_id) ON DELETE CASCADE,
  block_num   INTEGER NOT NULL,
  data        BYTEA NOT NULL, -- contenido binario real
  PRIMARY KEY (inode_id, block_num)
);

-- Índice crítico — sin esto es inutilizable
CREATE INDEX idx_inodes_parent_name ON inodes(parent_id, name);
Enter fullscreen mode Exit fullscreen mode

Cada operación del filesystem se traduce a SQL. Una lectura de archivo es un SELECT data FROM blocks WHERE inode_id = ? ORDER BY block_num. Una escritura es un INSERT ON CONFLICT UPDATE. Un ls es un SELECT name FROM inodes WHERE parent_id = ?.

FUSE hace el puente entre las syscalls del kernel y estas operaciones. Tu programa escribe un archivo, el kernel llama a FUSE, FUSE llama a TigerFS, TigerFS habla con Postgres.

Instalación, primer contacto, y cómo lo rompí

Empecé con Docker porque no soy masoquista (o no tan masoquista):

# Levantamos Postgres primero
docker run -d \
  --name tigerfs-postgres \
  -e POSTGRES_PASSWORD=tigerfs \
  -e POSTGRES_DB=tigerfs \
  -p 5432:5432 \
  postgres:16

# Esperamos que levante de verdad
sleep 3

# Instalamos las dependencias de FUSE en el host
sudo apt-get install -y fuse libfuse-dev

# Clonamos TigerFS
git clone https://github.com/[repo]/tigerfs
cd tigerfs

# Build
make build

# Creamos el punto de montaje
mkdir -p /tmp/tigerfs-mount

# Montamos
./tigerfs mount \
  --dsn "postgres://postgres:tigerfs@localhost:5432/tigerfs" \
  --mountpoint /tmp/tigerfs-mount
Enter fullscreen mode Exit fullscreen mode

Primer problema: FUSE en modo no-root en Linux moderno necesita que user_allow_other esté habilitado en /etc/fuse.conf. Sin eso, solo el usuario que montó puede acceder. En producción esto importa. En un experimento de fin de semana, lo agregué y seguí.

Primer test real:

# Escribimos algo
echo "hola tigerfs" > /tmp/tigerfs-mount/test.txt

# Verificamos que realmente está en Postgres
psql -h localhost -U postgres tigerfs -c "
  SELECT 
    i.name,
    i.size,
    encode(b.data, 'escape') as contenido
  FROM inodes i
  JOIN blocks b ON i.inode_id = b.inode_id
  WHERE i.name = 'test.txt';
"

-- Resultado:
--   name   | size |    contenido
-- ---------+------+------------------
--  test.txt|   14 | hola tigerfs\012
Enter fullscreen mode Exit fullscreen mode

Ahí está. Un archivo de texto adentro de una base de datos relacional. El \012 es el newline. Todo correcto.

Cómo lo rompí la primera vez: intenté copiar un archivo binario grande. Un ejecutable de 50MB. TigerFS por default usa bloques de 4KB, lo que significa 12.800 INSERT para un solo archivo. Postgres no se quejó. Pero el tiempo de escritura fue de 40 segundos. Para un archivo de 50MB. En ese momento entendí que estamos muy lejos de ext4.

Cómo lo rompí la segunda vez: dejé una transacción abierta en otra sesión de psql mientras escribía desde FUSE. Deadlock. El filesystem se colgó. Tuve que desmontar a mano con fusermount -u /tmp/tigerfs-mount y reiniciar.

Ambas roturas son esperables. Son las roturas correctas para un experimento.

Los errores comunes y los gotchas que nadie te cuenta

Gotcha 1: FUSE y Docker no son amigos por defecto

Si corrés TigerFS adentro de un container, necesitás --privileged o por lo menos --device /dev/fuse --cap-add SYS_ADMIN. Sin eso, FUSE no puede montar nada.

# Esto falla silenciosamente sin el flag correcto
docker run --device /dev/fuse --cap-add SYS_ADMIN tigerfs-image
Enter fullscreen mode Exit fullscreen mode

Gotcha 2: el tamaño de bloque importa muchísimo

Con bloques de 4KB escribir archivos grandes es una pesadilla de latencia. Con bloques de 1MB mejorás dramáticamente el throughput pero desperdiciás espacio en archivos chicos. No hay bala de plata acá, es el mismo tradeoff de cualquier filesystem real.

Gotcha 3: los índices son todo

Si no tenés el índice compuesto en (parent_id, name), un ls en un directorio con 1000 archivos hace un seq scan completo de la tabla de inodos. Lo aprendí de la peor manera. El mismo principio que siempre: el 73% de los problemas de performance en Postgres son de índices, no de hardware.

Gotcha 4: las transacciones y la atomicidad

Esto es donde se pone interesante. A diferencia de un filesystem tradicional, TigerFS puede envolver operaciones en transacciones reales. Escribís 10 archivos, falla en el 7, hacés rollback, y es como si no hubiera pasado nada. Eso ext4 no te lo da.

Gotcha 5: mtime y atime son gratis

En filesystems normales, actualizar atime en cada lectura es caro (implica una escritura a disco). En TigerFS es una simple actualización de campo en Postgres, que puede optimizarse o deshabilitarse con un flag. Detalle menor, pero muestra que el modelo relacional trae ventajas inesperadas.

Por qué esto existe: el síntoma más grande

Hay una tendencia en la que pienso bastante. La llamo "abstracción como exploración".

No se trata de hacer algo útil. Se trata de entender qué pasa cuando rompés las capas asumidas. Un filesystem existe en un nivel de abstracción. Una base de datos existe en otro. Normalmente no los mezclás. TigerFS pregunta: ¿y si lo mezclamos?

El año pasado yo hice lo mismo en la otra dirección con el historial de git de Linux. Metí datos que normalmente vivirían en un repo de git dentro de Postgres para poder hacerles queries SQL. Misma energía. Diferente dirección.

Veo el mismo patrón en MegaTrain intentando entrenar LLMs de 100B en una sola GPU: alguien preguntando qué pasa si ignoramos la restricción asumida. En Project Glasswing analizando qué hay adentro del código que la IA genera: cuestionando qué asumimos que es seguro.

Estos proyectos no son para producción. Son experimentos mentales ejecutables. Y los experimentos mentales ejecutables son cómo aprendemos de verdad.

Después de la migración de Vercel a Railway que mencioné antes — un fin de semana que me enseñó más sobre infraestructura real que meses de tutoriales — entiendo por qué la gente hace estas cosas. A veces necesitás romper el modelo mental para ver sus bordes.

TigerFS te muestra los bordes del filesystem. Te dice: mirá, un filesystem es básicamente un árbol de metadatos más bloques de datos. Eso es todo. Postgres puede representar eso. La pregunta no es si puede, sino qué ganás y qué perdés.

Lo que perdés: performance (dramáticamente), compatibilidad con herramientas del sistema, simplicidad operacional.

Lo que ganás: transacciones reales, queries sobre metadatos, replicación built-in, backups consistentes con pg_dump, acceso SQL directo a los datos. Si tenés un caso de uso donde esas ventajas superan las desventajas — y existen, especialmente en sistemas embebidos o en entornos donde ya tenés Postgres y necesitás storage estructurado — TigerFS o algo inspirado en él tiene sentido.

Pensá en sistemas de gestión documental. O en pipelines de datos donde el filesystem es una capa de coordinación entre procesos. O en testing, donde querés un filesystem que podés inspeccionar con SQL después de que tu test corra. De repente el experimento empieza a tener aplicaciones reales.

FAQ: filesystem sobre Postgres, FUSE y TigerFS

¿TigerFS es apto para producción?

No, al menos no en su estado actual. Los tiempos de escritura para archivos grandes son órdenes de magnitud más lentos que un filesystem nativo. Está diseñado como experimento y prueba de concepto. Dicho eso, los principios detrás — filesystems respaldados por bases de datos — existen en producción en sistemas como Amazon S3 (que internamente usa modelos similares) y varios sistemas de almacenamiento distribuido.

¿Cómo funciona FUSE exactamente?

FUSE (Filesystem in Userspace) es un módulo del kernel Linux que te permite implementar un filesystem en espacio de usuario, sin tocar código del kernel. Cuando una aplicación llama a open("/tmp/tigerfs-mount/archivo.txt"), el kernel ve que ese path está montado con FUSE y delega la llamada a tu programa en userspace. Tu programa responde, el kernel devuelve el resultado a la aplicación. La magia es que la aplicación no sabe que está hablando con Postgres — cree que está hablando con un filesystem normal.

¿Qué ventaja real tiene guardar archivos en Postgres sobre guardarlos en disco?

Dependiendo del caso de uso: transacciones ACID (podés escribir 100 archivos y hacer rollback si algo falla), queries sobre metadatos con SQL (encontrá todos los archivos modificados en las últimas 24 horas con un simple SELECT), replicación automática si ya tenés Postgres replicado, y backups consistentes con pg_dump. Para la mayoría de los casos, el filesystem nativo gana por goleada. Pero para casos específicos — especialmente coordinación entre procesos o auditoría — la base de datos gana.

¿Por qué los experimentos como TigerFS importan si no se usan en producción?

Porque son los mejores maestros de los fundamentos. Implementar un filesystem te obliga a entender qué es un inodo, por qué existen los bloques, cómo funciona el árbol de directorios. Implementarlo sobre Postgres te obliga a entender qué hace Postgres bien y qué hace mal. No aprendés eso leyendo documentación — lo aprendés rompiendo cosas. El mismo principio aplica a no confiar ciegamente en el código que genera la IA: necesitás entender las capas de abajo para saber qué está pasando.

¿Qué diferencia hay entre TigerFS y guardar archivos como BLOBs en Postgres directamente?

Buena pregunta. Guardar BLOBs en Postgres es una práctica conocida (y a veces válida). TigerFS va más lejos: implementa la semántica completa de un filesystem — permisos, timestamps, directorios anidados, symlinks, operaciones atómicas. No es solo storage de archivos, es un filesystem completo con su árbol de metadatos, su sistema de bloques, y su integración con el VFS del kernel vía FUSE. La diferencia es como comparar guardar HTML en una columna TEXT versus implementar un servidor web completo.

¿Podría usarse algo así en testing o CI?

Esta es la aplicación que más me parece legítima. Imaginá un test que escribe archivos en un filesystem TigerFS, corre, y después podés hacer SELECT * FROM inodes WHERE mtime > NOW() - INTERVAL '10 seconds' para ver exactamente qué archivos tocó tu programa. O podés hacer rollback del filesystem completo entre tests con ROLLBACK. Eso no es trivial con un filesystem normal y requeriría algo como overlayfs o tmpfs con lógica custom. Con TigerFS lo obtenés gratis.

Cerrando: la obsesión que vale la pena tener

No voy a usar TigerFS en producción. No lo recomendaría para nada que importe. Pero lo voy a seguir teniendo instalado porque cada vez que me trabo pensando en un problema de storage o de metadatos, puedo abrir psql, hacer queries sobre el filesystem, y ver la estructura desde un ángulo completamente diferente.

Hay algo que los años me enseñaron — desde los tiempos del cyber café diagnosticando cortes a las 11pm hasta tirar un servidor de producción con rm -rf en mi primera semana — las capas de abstracción son acuerdos, no verdades. Un filesystem es un acuerdo. Una base de datos es un acuerdo. Cuando rompés esos acuerdos de manera controlada, en un experimento, en un fin de semana, sin consecuencias reales, aprendés dónde están los bordes.

TigerFS es ese ejercicio. Y me parece que vale absolutamente la pena hacerlo.

Si te interesa el ángulo de meter datos en lugares donde "no deberían" ir, el post sobre el historial de git de Linux en una base de datos es el complemento natural de este. Y si te preocupa la dependencia en herramientas externas — que es el costo real de experimentos que se convierten en producción — el post sobre Anthropic y el vendor lock-in en APIs de IA tiene el mismo ADN.

Rompé cosas. En ambientes controlados. Con pg_dump antes.


Este artículo fue publicado originalmente en juanchi.dev

Top comments (0)