El problema fundamental que D1 aborda es la gestión y escalabilidad de un número masivo de bases de datos pequeñas, en contraste con el modelo tradicional de pocas bases de datos grandes. Este cambio es impulsado por plataformas de desarrollo asistido por IA y entornos multi-tenant, donde cada usuario o entorno requiere su propia instancia de base de datos. La tesis central es que, para escalar a millones de bases de datos, el plano de control debe ser desacoplado del plano de datos, priorizando la ejecución de consultas en el edge y relegando las operaciones de gestión a componentes centralizados pero asíncronos y eventualmente consistentes.
La arquitectura de D1 se apoya en principios de sistemas distribuidos como la idempotencia, la tolerancia a fallos, la consistencia eventual y la evitación de cuellos de botella centralizados en la ruta crítica. Esto es particularmente relevante en el contexto de SQLite, una base de datos embebida que, por su naturaleza, no está diseñada para ser distribuida. D1 resuelve esto encapsulando cada instancia de SQLite dentro de un Durable Object de Cloudflare, lo que permite su distribución global y persistencia.
Históricamente, la gestión de múltiples bases de datos por tenant ha sido un desafío de infraestructura significativo, a menudo requiriendo soluciones complejas de sharding o bases de datos multi-tenant con esquemas complejos. D1 simplifica esto al ofrecer una instancia de SQLite dedicada por tenant, delegando la complejidad de la distribución y la persistencia a la plataforma de Cloudflare.
Arquitectura del Sistema
La arquitectura de D1 se compone de varios elementos clave. Cada base de datos D1 es una instancia de SQLite encapsulada dentro de un Cloudflare Durable Object (DO), lo que facilita la creación de millones de ellas y su distribución global. Las solicitudes de API (creación, eliminación, listado) pasan por el Edge API Worker y utilizan Hyperdrive para interactuar con un clúster de PostgreSQL centralizado, que actúa como la fuente de verdad para los metadatos de las bases de datos. Hyperdrive optimiza el acceso a PostgreSQL con pooling de conexiones, enrutamiento y caching, utilizando réplicas de lectura siempre que sea posible.
El plano de datos, que maneja las consultas SQL, está diseñado para ser lo más rápido posible y ejecutarse en el edge. Las consultas se dirigen desde el Edge API Worker o directamente desde Workers de usuario (a través de D1 Bindings) al D1 Eyeball Worker. Este Eyeball Worker actúa como un proxy que reenvía la solicitud al Durable Object subyacente que aloja la base de datos SQLite. Para el enrutamiento, el Edge API Worker consulta Workers KV, un almacén de clave-valor distribuido globalmente, para obtener el mapeo entre el ID de la base de datos D1 y el ID del Durable Object. Workers KV actúa como una capa de caché duradera en el edge, evitando que las consultas necesiten acceder a PostgreSQL en la ruta caliente.
Las operaciones de creación de bases de datos implican la generación de un UUID de D1 y un Durable Object ID único, la inicialización del DO, la persistencia de metadatos en PostgreSQL y la escritura del mapeo en Workers KV. Este proceso incluye un bucle de reintentos con nuevos IDs para garantizar la idempotencia y la recuperación de fallos. Los Durable Objects envían 'heartbeats' periódicos al plano de control para sincronizar su estado (activo/eliminado) y límites de cuenta, lo que permite la auto-corrección del sistema y la eventual consistencia de Workers KV. Para la aplicación de límites de uso (almacenamiento, filas), D1 utiliza un sistema asíncrono basado en colas (Kafka-like) para procesar actualizaciones de uso de los DOs, agregando datos en PostgreSQL (usando CTEs para evitar contención de escritura) y ClickHouse para analíticas y facturación. Para la aplicación de límites en tiempo real, se utiliza Quicksilver (Workers KV Instant) para replicar rápidamente el estado de limitación a los Durable Objects en el edge.
Flujo de Creación de Base de Datos D1
- 1 Usuario Envía comando `wrangler d1 create`
- 2 Edge API Worker Recibe solicitud, verifica límites de cuenta (Postgres Read Replica via Hyper...
- 3 Edge API Worker (Core) Reenvía solicitud a DC cercano a Postgres para múltiples round-trips
- 4 Edge API Worker (Edge) Reenvía ejecución a DC cercano a la solicitud original para inicializar DO
- 5 Edge API Worker Genera UUID de DB y Durable Object ID, invoca `init()` en el nuevo Durable Ob...
- 6 Durable Object Almacena metadatos en su almacenamiento duradero
- 7 Edge API Worker Persiste metadatos de DB en Postgres Primary via Hyperdrive (punto de control)
- 8 Durable Object Actualiza Workers KV con metadatos de DB (asíncrono, best-effort)
Flujo de Ejecución de Consulta D1 (HTTP API)
- 1 Usuario Envía solicitud de consulta SQL
- 2 Edge API Worker Autenticación, autorización, rate limiting. Carga mapeo DB UUID -> DO ID de W...
- 3 Edge API Worker (Fallback) Si no está en KV, lee de Hyperdrive (cache local, cache cerca de Postgres, Po...
- 4 Edge API Worker Invoca D1 Eyeball Worker via Service Binding con IDs relevantes
- 5 D1 Eyeball Worker Validación, reenvía consulta a D1 Durable Object
- 6 D1 Durable Object Ejecuta consulta SQL en SQLite, devuelve resultado
- 7 D1 Eyeball Worker Proxy de respuesta
- 8 Edge API Worker Devuelve resultado al usuario
| Capa | Tecnología | Justificación |
|---|---|---|
| storage | SQLite | Base de datos embebida para cada instancia de D1, encapsulada en Durable Objects. |
| storage | PostgreSQL | Base de datos central para metadatos del plano de control (estado de DBs, mapeos, límites de cuenta). Uso de réplicas de lectura para operaciones no críticas. |
| cache | Workers KV | Almacén de clave-valor distribuido globalmente para metadatos de enrutamiento de DBs en el edge, desacoplando el plano de datos de PostgreSQL. vs Hyperdrive Cache (no es duradero) |
| networking | Hyperdrive | Proxy de base de datos para PostgreSQL, proporcionando pooling de conexiones, enrutamiento optimizado y caching en el edge y cerca del clúster central. Caching configurable con TTLs, uso de `dynamic_ttl` para invalidación controlada. |
| compute | Cloudflare Durable Objects | Unidad de computación y estado duradero que encapsula cada instancia de SQLite, permitiendo su distribución y persistencia. Uso de `newUniqueId()` para IDs únicos, `init()` RPC para inicialización. |
| compute | Cloudflare Workers | Plataforma serverless para la lógica del plano de control (Edge API Worker) y el proxy de consultas (Eyeball Worker), ejecutándose en el edge. Service Bindings para comunicación entre Workers. |
| messaging | Kafka-like Queue | Sistema de colas interno para procesar asíncronamente las actualizaciones de uso de almacenamiento de los Durable Objects, suavizando la carga de escritura en PostgreSQL. Consumidores limitados para controlar la carga en PostgreSQL. |
| data-processing | ClickHouse | Base de datos analítica para almacenar eventos de uso de almacenamiento y filas, utilizada para facturación y analíticas históricas. vs PostgreSQL (para analíticas) |
| cache | Quicksilver (Workers KV Instant) | Almacén de clave-valor distribuido globalmente con replicación sub-segundo, utilizado para la aplicación de límites de uso de filas en tiempo real. vs Workers KV (mayor latencia de replicación) Claves con fecha de expiración. |
Trade-offs
Ganancias
- ▲ Latencia de consultas
- ▲▲ Escalabilidad del plano de datos
- ▲ Tolerancia a fallos
- ▲ Costo de infraestructura
Costes
- △ Consistencia inmediata (para metadatos y límites)
- △ Complejidad del plano de control
WITH updates AS MATERIALIZED (
DELETE FROM account_database_stats_changes
RETURNING *
),
changes AS MATERIALIZED (
SELECT
account_id,
SUM(storage_change) AS storage
FROM updates
GROUP BY account_id
)
INSERT INTO account_database_stats (account_id, storage)
SELECT
account_id,
storage
FROM changes
ON CONFLICT (account_id) DO UPDATE
SET storage = account_database_stats.storage + EXCLUDED.storage;const parsed = parseQueries(request);
const cachedDB = await withRetries(getCachedDatabase(parsed.dbUuid));
const response = await env.EYEBALL_WORKER.query({
db: cachedDB,
queries: parsed.queries,
});
return response;-- $2 = Math.floor(Date.now() / (X * 1000))
SELECT storage, $2 as dynamic_ttl
FROM account_database_stats WHERE account_id = $1Fundamentos Teóricos
La arquitectura de D1 se alinea con varios principios fundamentales de la computación distribuida y la gestión de bases de datos. La separación del plano de control y el plano de datos es un patrón arquitectónico común en sistemas a gran escala, que busca optimizar la latencia y la disponibilidad de las operaciones críticas (plano de datos) desacoplándolas de las operaciones de gestión menos sensibles a la latencia (plano de control). Este enfoque es una aplicación práctica del principio de 'separación de preocupaciones'.
El uso extensivo de la consistencia eventual, especialmente en la propagación de metadatos a través de Workers KV y en la aplicación de límites de uso, refleja la realidad de los sistemas distribuidos a escala global. Esto se relaciona con el teorema CAP, donde la disponibilidad y la tolerancia a particiones se priorizan sobre la consistencia fuerte para operaciones que no requieren garantías transaccionales inmediatas. Los 'heartbeats' de los Durable Objects para la auto-corrección son un mecanismo para lograr esta consistencia eventual, asegurando que el estado global del sistema converja a la verdad de PostgreSQL con el tiempo.
El patrón de agregación de cambios de uso en PostgreSQL, utilizando Common Table Expressions (CTEs) y la tabla account_database_stats_changes para evitar la contención de escritura en una única fila, es una forma de 'incremental view maintenance' o 'materialized view' simplificada. Aunque no es una vista materializada transaccional de primera clase, demuestra la aplicación de técnicas de optimización de bases de datos para manejar cargas de escritura elevadas en un entorno distribuido, similar a los patrones descritos en papers sobre bases de datos OLAP y OLTP híbridas o sistemas de procesamiento de eventos complejos.