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);
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
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
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
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.
Comentarios (0)
Deja un comentario
Artículos Relacionados
Metí el historial completo de git de Linux en una base de datos — y lo que encontré me pareció arqueología
Qué pasa cuando dejás de tratar tu historial de commits como logs y empezás a tratarlo como datos. Lo hice con repos viejos míos. Lo que encontré fue incómodo, revelador, y me hizo entender cómo programo de verdad.
LittleSnitch para Linux: por qué tardó tanto y qué dice eso del ecosistema
Llevo años desarrollando en Linux y la ausencia de un outbound firewall decente con GUI siempre fue el elefante en el cuarto. No es un review — es una excusa para hablar de por qué ciertas herramientas obvias tardan una década en aparecer en Linux.
Harté de esperar que alguien maintuviera la extensión de HAProxy para VS Code — así que la hice yo
La única extensión de HAProxy para VS Code no la tocaban desde 2019. Todos los días la usaba en el trabajo y en mi homelab, y todos los días me tragaba errores de sintaxis sin ningún feedback. Un día dije basta y construí gmm-haproxy-vscode: LSP propio, autocompletado por sección, validación multi-versión y go-to-definition.