Guardas una presentación de 300 MB en tu portátil y, un minuto después, se abre en tu móvil. Luego corriges una errata en una diapositiva y la actualización tarda un segundo, porque solo viaja la parte que cambió. Un servicio de sincronización de archivos hace que parezca fácil, pero por debajo hay una separación cuidadosa entre dos tipos de datos muy distintos, más un protocolo que tolera redes inestables, ediciones offline y dispositivos que despiertan días después.
Este artículo diseña un servicio de sincronización al estilo Dropbox: requisitos y cifras, la separación entre metadatos y contenido, el troceado y los hashes de contenido, el protocolo de subida y commit, el journal de metadatos y los cursores, las notificaciones, los conflictos y la recolección de basura, con un diagrama de arquitectura.
Requisitos
- Los archivos guardados en un dispositivo aparecen en los demás dispositivos del usuario en cuestión de segundos o minutos.
- Solo se suben y descargan los datos que cambian.
- Las ediciones hechas offline se sincronizan cuando el dispositivo se reconecta, y las ediciones concurrentes nunca se sobrescriben en silencio.
- Las carpetas compartidas funcionan entre cuentas.
- Nunca se pierden datos; hay historial de versiones para recuperarlos.
Capacidad
Supón 10 millones de dispositivos activos, cada uno sincronizando 20 cambios al día, con una media de 256 KB de datos nuevos por cambio tras la deduplicación:
| Magnitud | Cálculo | Resultado |
|---|---|---|
| Cambios al día | 10M × 20 | 200 millones |
| Commits de metadatos | 200 000 000 / 86 400 s | ~2300 por segundo |
| Datos de bloques nuevos | 200M × 256 KB | ~51 TB al día |
La tasa de metadatos es modesta; el volumen de datos es grande pero fácil de almacenar. Esa asimetría guía el diseño.
Separa los metadatos del contenido
La decisión más importante es tratar el contenido y los metadatos de los archivos como dos sistemas distintos:
- El contenido se guarda como bloques inmutables, direccionados por el hash de sus bytes. Los bloques son grandes, se escriben una vez y es fácil replicarlos en almacenamiento de objetos.
- Los metadatos registran qué archivo, en qué ruta y en qué versión, se compone de qué bloques. Son pequeños, cambian constantemente y necesitan transacciones y un orden estricto.
Dropbox describió esta separación en su artículo sobre streaming sync: cada archivo se divide en bloques de 4 MB, cada bloque se hashea con SHA-256 y el contenido de un archivo se identifica por la lista de esos hashes, su lista de bloques. La API pública sigue exponiendo la misma idea como un content hash: el SHA-256 de cada bloque de 4 MB y luego el SHA-256 de los hashes de bloque concatenados.
Troceado: fijo o definido por el contenido
Los bloques de tamaño fijo son simples y rápidos de calcular. Su debilidad aparece cuando se insertan bytes cerca del inicio de un archivo: todas las fronteras de bloque posteriores a la inserción se desplazan y todos los hashes de bloque cambian.
El troceado definido por el contenido (content-defined chunking) coloca las fronteras donde un hash rodante sobre el contenido coincide con un patrón, como hacen la suma de verificación rodante de rsync y algoritmos más nuevos como FastCDC. Las fronteras se mueven con el contenido, así que una inserción solo cambia los trozos que la rodean.
| Insertar 10 bytes al inicio de un archivo de 100 MB | Bloques fijos de 4 MB | Por contenido, ~4 MB de media |
|---|---|---|
| Trozos que cambian | Los 25 | Alrededor de uno |
| Datos que subir | ~100 MB | ~4 MB |
| Coste de CPU del troceado | Bajo | Mayor: un hash rodante sobre cada byte |
Para archivos a los que sobre todo se añade al final o que se reemplazan enteros, los bloques fijos bastan. Para archivos que se editan por el medio, como documentos grandes, imágenes de disco o bases de datos, el troceado por contenido ahorra muchísimo más ancho de banda.
El protocolo de subida y commit
- El cliente de escritorio detecta un cambio mediante el observador de archivos del sistema operativo, espera a que el archivo deje de cambiar y luego lo trocea y lo hashea.
- Envía un commit al servicio de metadatos: esta ruta, esta nueva lista de bloques, basada en la versión padre N.
- El servicio de metadatos comprueba qué hashes ya tiene y responde con los que necesita.
- El cliente sube solo esos bloques al servicio de bloques, en paralelo, cada uno verificado por su hash. Una subida interrumpida se reanuda desde los bloques que faltan.
- El cliente reintenta el commit. Ahora existen todos los bloques, así que el servicio de metadatos añade una nueva versión del archivo al journal.
El orden es la propiedad de seguridad: una versión que referencia bloques nunca se confirma hasta que todos están almacenados. Una caída en cualquier punto deja la versión antigua o la nueva, nunca un archivo que apunte a datos inexistentes.
El direccionamiento por contenido también da deduplicación gratis. Un archivo copiado en tres carpetas, o revertido a una versión anterior, no sube nada nuevo. La deduplicación entre cuentas ahorra más espacio, pero puede revelar que otra persona guardó el mismo archivo, así que muchos diseños solo deduplican dentro de una cuenta o de una carpeta compartida.
El journal de metadatos y los cursores
Agrupa los archivos en namespaces, como el árbol de carpetas propio de un usuario o una carpeta compartida. Para cada namespace, el servicio de metadatos mantiene un journal de solo anexado: cada cambio recibe el siguiente número de secuencia, con la ruta, la versión y la lista de bloques.
Cada cliente guarda un cursor por namespace: el último número de secuencia que ha aplicado. Sincronizar hacia abajo se reduce entonces a una sola pregunta: «dame todo lo de este namespace posterior a mi cursor». El journal lo hace barato, ordenado y reanudable. Un dispositivo que estuvo offline una semana hace la misma pregunta y recibe una semana de cambios. Particiona los metadatos por namespace, porque los cambios de un namespace nunca necesitan una transacción con otro.
Notificaciones: avisar a los dispositivos de que sincronicen
Consultar el journal desde cada dispositivo inactivo sería un desperdicio. En su lugar, los clientes de escritorio inactivos mantienen una conexión de long poll con un servicio de notificaciones. Cuando avanza el journal de un namespace, el servicio responde a las peticiones en espera con «hay cambios»; cada cliente lista entonces los cambios posteriores a su cursor y descarga los bloques que no tiene. La API pública de Dropbox ofrece el mismo patrón con list_folder/longpoll y list_folder/continue.
Las apps móviles no pueden mantener conexiones abiertas en segundo plano, así que dependen de las notificaciones push de la plataforma y sincronizan cuando se abre la app.
El lado del cliente
El cliente de escritorio es la mitad del sistema. Mantiene una base de datos local con el último estado sincronizado de cada archivo: ruta, versión, lista de bloques, tamaño y fecha de modificación. Comparar tres estados decide cada acción:
| Archivo local frente al último sincronizado | Remoto frente al último sincronizado | Acción |
|---|---|---|
| Cambiado | Sin cambios | Subir |
| Sin cambios | Cambiado | Descargar |
| Cambiado | Cambiado | Conflicto |
| Sin cambios | Sin cambios | Nada |
Una caché local de bloques evita descargar bloques que el dispositivo ya tiene en otros archivos.
Conflictos
Dos dispositivos editan el mismo archivo estando offline. Más tarde, ambos hacen commit con la versión padre N. El servicio de metadatos acepta el primer commit, que crea la versión N+1, y rechaza el segundo porque su padre ya no es el actual. En lugar de fusionar archivos binarios que no entiende, el cliente que pierde guarda su versión junto al original como copia en conflicto y sincroniza ambas. Decide el usuario.
Esto es concurrencia optimista: cada commit indica la versión en la que se basó. La misma comprobación se encarga de las operaciones de directorio, que en la práctica son más difíciles: un renombrado en un dispositivo y una edición en otro, o un borrado que compite con una edición. Escribe estos casos de forma explícita; es ahí donde los motores de sincronización pierden datos.
Historial de versiones y recolección de basura
Las versiones antiguas conservan sus listas de bloques, así que restaurar un archivo consiste simplemente en hacer commit de una lista de bloques antigua como la versión más reciente. Los bloques solo se convierten en basura cuando ninguna versión dentro del periodo de retención los referencia. Ejecuta la recolección de basura como mark-and-sweep con un periodo de gracia, nunca como un borrado inmediato al desaparecer la última referencia, porque puede que un cliente esté a punto de hacer commit de una lista de bloques que reutiliza un bloque que el servidor cree huérfano.
El diagrama de sincronización de archivos
Contenido del diagrama
- Laptop (writer)
- Phone (reader)
- Sync backend
- API gateway
- Metadata service
- Notification service
- Block service
- Journal per namespace
- journal events
- Blocks by SHA-256 (4 MB)
- 1, 3, 4
- 5. changes available
- 6. list after cursor, get blocks
- 1. commit (parent v7)
- 2. missing hashes
- 3. upload missing blocks
- 5. answer long polls
- 4. append v8
- namespace changed
- wake
- put by hash
Cómo leerlo:
- El portátil de la izquierda hace commit de una lista de bloques en el servicio de metadatos y recibe los hashes que le faltan al servidor.
- Las subidas de bloques van al servicio de bloques y acaban en el almacenamiento de objetos, indexadas por hash.
- El servicio de metadatos añade la nueva versión al journal del namespace en Postgres y publica el cambio.
- El servicio de notificaciones responde al long poll del móvil; el móvil lista los cambios posteriores a su cursor y descarga solo los bloques que faltan.
- El bloque Markdown enumera en orden los seis pasos del protocolo.
Dibujar sistemas de sincronización con claridad
- Dibuja los metadatos y el contenido como caminos separados. Almacenes distintos, escalado distinto, modos de fallo distintos.
- Numera los pasos del protocolo en las aristas. Commit, hashes que faltan, subida, nuevo commit: el orden es el argumento de corrección.
- Muestra el segundo dispositivo. La sincronización trata tanto del lector como del escritor; un diagrama con un solo cliente oculta las notificaciones y los cursores.
- Etiqueta la clave de partición: el namespace para los metadatos, el hash para los bloques.
Para un enfoque general, consulta el ejemplo de diagrama de diseño de sistemas. La parte de push se parece al diseño de un sistema de notificaciones, las subidas troceadas al almacenamiento de objetos también aparecen en el diagrama de arquitectura de streaming de vídeo, y particionar los metadatos sigue el diagrama de sharding de bases de datos.
Constrúyelo en Archboard
- Inicia sesión con Google o GitHub y crea un diagrama.
- Añade dos nodos Dispositivo del pack Red: un portátil y un móvil.
- Añade un Marco del pack Básico para el backend de sincronización, con una Puerta de enlace de API del pack Nube en su borde.
- Dentro del marco, añade tres nodos Instancia de cómputo del pack Nube para los servicios de metadatos, bloques y notificaciones, un nodo Postgres del pack Datos para el journal, un nodo Almacenamiento de objetos del pack Nube para los bloques y un nodo Kafka del pack Datos para los eventos del journal.
- Numera las aristas en el orden del protocolo: commit, hashes que faltan, subida, nuevo commit, notificación, listado y descarga.
- Añade un bloque Markdown del pack Básico con el protocolo y la capacidad.
El ejemplo usa 11 nodos de diagrama, dentro del plan Free.
Preguntas frecuentes
¿Por qué los servicios de sincronización dividen los archivos en bloques? Los bloques permiten al cliente subir solo lo que cambió, reanudar transferencias interrumpidas y deduplicar datos idénticos. El hash de un bloque es su identidad, así que el servidor sabe al instante qué bloques ya tiene.
¿Debo usar troceado de tamaño fijo o definido por el contenido? Los bloques de tamaño fijo son más simples y más baratos de calcular. El troceado por contenido mantiene estables las fronteras de los trozos cuando se insertan bytes en medio de un archivo, lo que ahorra ancho de banda en archivos que se editan in situ.
¿Cómo sabe un cliente qué ha cambiado en el servidor? Guarda un cursor por namespace y pide al servicio de metadatos todas las entradas del journal posteriores a él. Un long poll o una notificación push le indica cuándo preguntar.
¿Cómo se gestionan las ediciones en conflicto? Cada commit indica la versión en la que se basó. Si otro dispositivo hizo commit antes, el commit falla y el cliente conserva su edición como copia en conflicto en lugar de sobrescribir la otra.
¿Funciona la deduplicación entre usuarios? Puede, pero puede revelar que otra cuenta guardó el mismo archivo, y no funciona con cifrado por usuario. Muchos servicios limitan la deduplicación a una cuenta o a una carpeta compartida.