Dónde ya lo usas sin saberlo: cachés, registros, paquetes y versiones
Las cachés de compilación, los registros de contenedores, los gestores de paquetes y el control de versiones son todos el mismo diseño, y reconocer el patrón compartido enseña más que estudiarlos por separado.
Todo lo que este nivel ha presentado como una idea de arquitectura distribuida lleva décadas funcionando en las herramientas que se usan a diario, y casi nadie lo identifica como tal porque cada dominio lo ha bautizado con su propio vocabulario. La caché de un sistema de compilación, la etiqueta de una imagen de contenedor, el fichero de bloqueo de un gestor de paquetes y el identificador de una confirmación son cuatro nombres distintos para exactamente la misma estructura: un almacén inmutable direccionado por resumen y un puñado de punteros mutables encima. Esta lección recorre los cuatro casos, no para describir herramientas que ya se conocen, sino para hacer visible el esqueleto compartido. Ver el mismo diseño reaparecer de forma independiente en cuatro comunidades que apenas se leen entre sí es la mejor evidencia disponible de que no se trata de una técnica de moda, sino de la forma que adopta un problema cuando se plantea bien.
- Identificar el almacén inmutable y la capa mutable en cuatro herramientas de uso cotidiano.
- Entender por qué una caché correcta exige que la clave resuma todas las entradas de la que depende.
- Reconocer qué papel juega un fichero de bloqueo y por qué es la capa mutable de un gestor de paquetes.
- Extraer el criterio para saber cuándo el patrón encaja en un problema nuevo y cuándo no.
Cachés de compilación: la clave resume todas las entradas
Un sistema de compilación con caché resuelve una pregunta que es exactamente la de este nivel: dada una tarea, ¿ya se ha ejecutado antes con estas mismas condiciones y se puede reutilizar su salida? La única forma correcta de contestarla es construir una clave que resuma absolutamente todo aquello de lo que la salida depende, y esa clave es un resumen criptográfico sobre el contenido de las entradas.
Merece la pena detallar qué entra en esa clave, porque la lista es más larga de lo que la intuición sugiere y cada elemento omitido es una fuente de resultados erróneos: el contenido de los ficheros de entrada, la línea de órdenes completa, la identidad del compilador y su versión, las variables de entorno que se declaren relevantes, la plataforma de destino y las claves de todas las tareas de las que esta depende. Ese último punto es el que convierte la caché en un árbol de resúmenes: la clave de una tarea incorpora las claves de sus dependencias, y por tanto un cambio en cualquier hoja se propaga hasta la raíz.
flowchart TB
E1[ficheros fuente] --> K[clave de la accion]
E2[orden y argumentos] --> K
E3[version del compilador] --> K
E4[claves de las dependencias] --> K
K --> C{esta en la cache}
C -->|si| R[reutilizar la salida]
C -->|no| X[ejecutar y guardar]
style K fill:#cba6f7,color:#11111b
style R fill:#a6e3a1,color:#11111bEsta es también la razón por la que una caché de este tipo puede compartirse entre máquinas y entre personas sin riesgo, que es la propiedad que la hace valiosa en un equipo. Si la clave cubre todo lo que influye en el resultado, entonces la salida que otro construyó ayer en otra máquina es indistinguible de la que yo construiría ahora, y aceptarla no requiere confiar en esa persona: basta con que la clave coincida y que la salida se verifique. Cuando una caché compartida produce resultados incorrectos, la causa es siempre la misma y nunca es la caché: la clave no cubría alguna entrada real, típicamente una variable de entorno, una marca de tiempo incrustada o una ruta absoluta.
# La clave lo cubre todo, incluidas las dependencias, y por eso se comparte
bazel build //app --remote_cache=grpc://cache.interno:9092
nix build .#app # la ruta de salida contiene el resumen de las entradas
ls /nix/store # cada directorio esta nombrado por su clave completa
Hay un matiz de confianza que conviene no pasar por alto, porque distingue una caché compartida bien construida de una peligrosa. Aceptar una salida ajena solo es seguro si la salida es realmente función de la clave, es decir, si la construcción es determinista; si no lo es, un participante puede inyectar en la caché un artefacto arbitrario bajo una clave legítima y todos los demás lo instalarán sin sospechar nada. Por eso los sistemas que se toman en serio la caché compartida entre organizaciones exigen construcciones reproducibles y comprueban, con ejecuciones repetidas en máquinas distintas, que la misma clave produce siempre los mismos bytes.
Ante un fallo de caché compartida, la pregunta productiva no es cómo invalidarla, sino qué influye en el resultado y no está dentro de la clave. Los sospechosos habituales son las marcas de tiempo que el compilador incrusta en la salida, las rutas absolutas del directorio de trabajo, la información de la máquina que se cuela en los metadatos y la aleatoriedad de las tablas de símbolos. Perseguirlos es exactamente el trabajo de hacer una compilación reproducible, y no es casualidad: una compilación reproducible es una compilación cuya salida es función de su clave.
Registros de contenedores y gestores de paquetes
Un registro de imágenes de contenedor tiene la estructura de este nivel de forma tan literal que sus propias especificaciones la nombran. Cada capa del sistema de ficheros es un objeto identificado por su resumen; un manifiesto es un documento que enumera esos resúmenes y que a su vez tiene el suyo; y una etiqueta legible es un puntero mutable que apunta al resumen de un manifiesto. La deduplicación entre imágenes distintas ocurre sola, porque dos imágenes construidas sobre la misma base comparten literalmente los mismos objetos de capa.
# La etiqueta es mutable; el resumen no. Solo lo segundo es reproducible.
docker pull registro/app:produccion # puede cambiar manana
docker pull registro/app@sha256:9f86d0818... # sera siempre lo mismo
# Averiguar a que apunta hoy la etiqueta
docker buildx imagetools inspect registro/app:produccion
Los gestores de paquetes cuentan la misma historia con otra terminología, y la pieza que la revela es el fichero de bloqueo. Un manifiesto de dependencias declara intervalos de versiones aceptables, que son nombres por ubicación: dicen dónde buscar, no qué se va a encontrar, y lo que se encuentra depende del día. El fichero de bloqueo fija, para cada dependencia, la versión exacta y el resumen del contenido descargado, de modo que una instalación posterior no solo obtiene la misma versión sino que comprueba que los bytes son los mismos.
Ficheros de bloqueo
Guardan resumen por dependencia junto a la versión, y convierten una instalación en una operación verificable y repetible.
Sumas de módulos
Un registro público y solo-añadir de resúmenes por módulo y versión permite detectar que una publicación fue reescrita después.
Rutas de almacén por resumen
Cuando el directorio de instalación lleva el resumen de todas las entradas, dos configuraciones distintas conviven sin pisarse.
Etiquetas y canales
Producción, estable o latest son punteros mutables sobre un almacén inmutable. Son cómodos y no son reproducibles.
# El manifiesto declara el contenido esperado de cada dependencia
grep integrity package-lock.json | head -3
# "integrity": "sha512-9f86d081884c7d..." <- resumen del paquete descargado
# Otros gestores publican el mismo dato en un registro aparte y verificable
cat go.sum | head -2 # modulo, version y resumen, una linea por entrada
La segunda tarjeta señala un refinamiento que merece atención porque cierra un agujero real. Un resumen en un fichero de bloqueo protege frente a que el paquete cambie sin que yo me entere, pero no frente a que el autor lo reescriba y yo instale por primera vez la versión reescrita. La respuesta ha sido añadir un registro público, solo-añadir y verificable, donde el resumen de cada publicación queda anotado la primera vez que alguien la descarga; cualquier reescritura posterior produce una discrepancia visible para todo el mundo. Es la misma estructura de este nivel aplicada a los propios nombres.
El control de versiones, que es el caso más puro
Cualquiera que haya usado un sistema de control de versiones moderno ha estado operando un almacén direccionado por contenido sin necesidad de llamarlo así, y conviene desmontarlo pieza por pieza porque es el ejemplo más limpio que existe. Un contenido de fichero se guarda como un objeto cuyo nombre es el resumen de sus bytes. Un directorio se guarda como un objeto que lista nombres de fichero junto a los resúmenes de su contenido, y el resumen de esa lista es su nombre. Una confirmación es un objeto que contiene el resumen del directorio raíz, los resúmenes de sus confirmaciones padre y los metadatos de autoría, y su nombre es el resumen de todo eso.
# El identificador de una confirmacion es el resumen de su contenido
git cat-file -p HEAD # muestra el arbol raiz y los padres
git cat-file -p HEAD^{tree} # el arbol lista resumenes por entrada
# Lo unico mutable del sistema son las referencias
cat .git/refs/heads/main # un fichero con un resumen dentro
// La construccion es tan literal que se puede reproducir a mano
const objeto = `blob ${bytes.length}\0` + bytes; // cabecera y contenido
const nombre = sha1(objeto); // ese es su identificador
// Y un arbol se nombra por la lista de nombres de sus entradas
const arbol = entradas.map(e => `${e.modo} ${e.nombre}\0${e.resumen}`).join('');
const nombreArbol = sha1(`tree ${arbol.length}\0` + arbol);
De esa construcción se siguen todas las propiedades del nivel, y verlo así explica varias cosas que normalmente se aprenden de memoria. Dos ramas que comparten historia comparten literalmente los objetos, sin duplicación. Un identificador de confirmación cubre toda la historia anterior, porque incluye los resúmenes de los padres, y por eso reescribir una confirmación antigua cambia los identificadores de todas las posteriores. Comparar dos repositorios se hace intercambiando resúmenes, no contenidos. Y traer datos de un desconocido es seguro en cuanto a integridad, porque cada objeto se verifica al llegar. Lo único mutable de todo el sistema son las referencias, que son ficheros con un resumen dentro, y por eso son también el único sitio donde aparecen conflictos, avances forzados y trabajo perdido.
Cuando se entiende que el identificador de una confirmación resume también a sus ancestros, deja de ser un misterio por qué reescribir la historia es tan disruptivo: no se está editando un registro, se está creando una historia paralela cuyos nombres no coinciden con ninguno de los que los demás tienen. Nadie ha perdido nada, pero todos los punteros de todo el mundo apuntan a una realidad que ya no es la vigente, y reconciliar eso es un trabajo manual porque el sistema no tiene forma de saber qué versión debía prevalecer.
Vale la pena señalar además una decisión de implementación que estos cuatro sistemas comparten y que suele confundir a quien mira por dentro: el almacenamiento físico no tiene por qué parecerse al modelo lógico. Un control de versiones guarda los objetos comprimidos y agrupados en ficheros que codifican unos como diferencias respecto a otros, y un registro de artefactos guarda las capas comprimidas y a veces las recompone al vuelo. Nada de eso altera el modelo: los nombres siguen siendo resúmenes del contenido lógico y la verificación sigue siendo la misma. La representación en disco es una optimización local y el nombrado es el contrato, y confundir ambas cosas lleva a creer que el sistema guarda diferencias cuando lo que guarda son objetos completos representados de forma astuta.
Cómo reconocer el patrón y cuándo no encaja
Con cuatro ejemplos delante se puede destilar la receta, que tiene siempre las mismas tres piezas: un almacén de objetos inmutables nombrados por el resumen de su contenido; una forma de componer objetos que referencian a otros por resumen, lo que produce árboles y grafos cuyo nombre raíz cubre todo lo alcanzable; y una capa pequeña de punteros mutables que dice qué raíz es la vigente. Cuando en un problema nuevo se pueden identificar esas tres piezas, casi todo el diseño está hecho.
// La receta, reducida a lo minimo, y siempre son las mismas tres piezas
const almacen = new Map(); // nombre de contenido a bytes
const punteros = new Map(); // nombre estable a nombre de contenido
const raiz = await guardar(almacen, componer(hijos)); // arbol inmutable
punteros.set('documento-42', raiz); // el unico punto mutable
Conviene tener igual de claro cuándo el patrón no encaja, porque aplicarlo a la fuerza produce sistemas peores que la alternativa directa. No encaja cuando el dato cambia con muchísima más frecuencia de la que se lee, porque cada cambio produce un objeto nuevo y la capa mutable se satura. No encaja cuando las consultas son por rango o por propiedades del contenido, porque un resumen destruye toda la localidad y no permite ordenar ni recorrer. No encaja cuando el requisito es borrar de verdad y con garantías, porque la deduplicación y la compartición de estructura convierten el borrado en un problema de alcanzabilidad. Y no aporta gran cosa cuando hay un único escritor, un único lector y un único disco, que es el caso en el que el nombrado por ubicación funciona perfectamente.
Lo que hay que llevarse de esta lección no es el catálogo de herramientas, que envejecerá, sino lo que significa que ese catálogo exista. Cuatro comunidades con culturas distintas, que apenas se citan entre sí y que en muchos casos ni siquiera comparten conferencias, llegaron a la misma estructura resolviendo problemas que en la superficie no se parecen en nada: acelerar una compilación, distribuir imágenes de sistema, instalar bibliotecas de forma repetible y llevar la historia de un texto. Ninguna copió a las otras y todas acabaron con un almacén inmutable de objetos nombrados por su resumen, referencias entre objetos por ese mismo nombre y un puñado minúsculo de punteros mutables encima. Esa convergencia independiente es la señal más fiable de que se ha encontrado la forma natural del problema y no una solución entre varias, del mismo modo que en biología la aparición repetida del ojo en linajes sin parentesco dice algo sobre la física de la luz y no sobre las especies. Y merece la pena entender por qué convergen, porque el porqué es lo transferible: los cuatro problemas comparten una estructura profunda que sus enunciados ocultan. En los cuatro hay una cantidad enorme de datos que, una vez producidos, no tienen ninguna razón para cambiar; en los cuatro hay una cantidad diminuta de información que sí cambia y que consiste en decir cuál de todo aquello es el vigente; y en los cuatro la confusión de ambas cosas en un mismo mecanismo —el mecanismo por defecto, que es un nombre asignado por ubicación— es lo que producía todos los síntomas: cachés incorrectas, despliegues irreproducibles, instalaciones que difieren entre máquinas e historias que se pierden. Por eso la pregunta que conviene incorporar al repertorio, y con la que cierra este nivel, no es dónde se puede meter un resumen criptográfico, sino esta otra, mucho más productiva: en el sistema que tengo delante, qué fracción de los datos cambia realmente, y qué parte de la maquinaria de coordinación existe solo porque no se ha distinguido esa fracción del resto. Casi siempre la fracción que cambia es asombrosamente pequeña, casi siempre la maquinaria trata todo por igual, y casi siempre esa igualación es la fuente de la complejidad que se estaba intentando gestionar por otros medios.
- Localiza en un repositorio real el objeto de una confirmación y sigue a mano la cadena hasta el contenido de un fichero.
- Sustituye una etiqueta de imagen por su resumen en un despliegue y comprueba qué garantías cambian.
- Borra tu fichero de bloqueo, reinstala y compara los resúmenes obtenidos con los que había antes.
- Provoca un fallo de caché de compilación introduciendo una marca de tiempo en la salida y localiza qué falta en la clave.
- Elige un componente de tu sistema y escribe cuál sería su almacén inmutable, su forma de componer y su puntero mutable.
- Argumenta en contra: encuentra en tu propio sistema un caso donde este patrón sería peor que la solución directa, y di por qué.