
SQLite VFS con consultas JOIN en frío de menos de 100 ms desde S3 + compresión y cifrado a nivel de página
turbolite es un VFS de SQLite en Rust que sirve búsquedas puntuales y uniones directamente desde S3 con una latencia en frío inferior a 250 ms.
Este repositorio es un espacio de trabajo de Cargo con dos crates:
turbolite — Biblioteca pura de Rust. VFS de SQLite con compresión a nivel de página, cifrado y escalonamiento a S3.turbolite-ffi — FFI C / extensión cargable + enlaces de idiomas (Python, Node.js, Go).También ofrece compresión a nivel de página (zstd) y cifrado (AES-256) para eficiencia y seguridad en reposo, que pueden usarse por separado de S3.
Experimental. turbolite está en desarrollo activo y contiene errores. Ten cuidado.
El almacenamiento de objetos se está volviendo rápido. S3 Express One Zone ofrece GETs de un solo dígito en milisegundos y Tigris también es extremadamente rápido. La brecha entre el disco local y el almacenamiento en la nube se está reduciendo, y turbolite lo explota.
El diseño y el nombre están inspirados en el enfoque de turbopuffer de arquitectura despiadada en torno a las limitaciones del almacenamiento en la nube. El objetivo inicial del proyecto era superar los arranques en frío de más de 500 ms de Neon. Objetivo cumplido.
Si tienes una base de datos por servidor, usa un volumen. turbolite explora cómo tener cientos o miles de bases de datos (una por inquilino, una por espacio de trabajo, una por dispositivo), no quieres un volumen para cada una, y estás de acuerdo con una única fuente de escritura.
turbolite se distribuye como biblioteca de Rust, una extensión cargable de SQLite (.so/.dylib), y paquetes de idiomas para Python y Node.js, además de dependencias de Github para Go. Cualquier almacenamiento compatible con S3 funciona (AWS S3, Tigris, R2, MinIO, etc.). Es un VFS de SQLite estándar que opera a nivel de página, por lo que la mayoría de las funciones de SQLite deberían funcionar: FTS, R-tree, JSON, modo WAL, etc.
turbolite es parte del ecosistema más amplio de hadb. turbolite independiente es un VFS de almacenamiento con un escritor seguro; si deseas elección de líder HA más replicación continua de WAL, úsalo a través de haqlite-turbolite, que agrega HaQLite y walrust encima. Ese camino HA es aún muy experimental.
Si deseas contribuir a turbolite o encontrar errores, por favor crea una solicitud de extracción o abre un problema.
1 M publicaciones / 100 K usuarios (~1.5 GB almacenados) sin nada en caché, cada byte desde S3. EC2 c5.2xlarge + S3 Express One Zone (misma AZ, ~4 ms de latencia GET). Fly performance-8x + Tigris (~25 ms de latencia GET). Ambos: 8 vCPU dedicados, 16 GB RAM, 7 hilos de trabajo de precarga. Consulta Evaluación comparativa y El backend de almacenamiento importa.
Los puntos de referencia están organizados por nivel de caché (qué está ya en disco local cuando se ejecuta la consulta):
interior es el punto de referencia en frío más realista: las páginas interiores se cargan con avidez al abrir la conexión, por lo que cuando ejecutas tu primera consulta, ya están en caché. Las páginas de índice se precargan agresivamente en segundo plano en el primer acceso y puede que no estén listas aún.
100 K filas, Fly.io performance-2x (vCPU dedicado, NVMe, IAD):
Las búsquedas puntuales tienen la mayor sobrecarga por página (~2x). Todo lo demás se acerca o supera la paridad. La arquitectura de caché libre de bloqueos significa que las lecturas concurrentes nunca bloquean las escrituras.
| Después | Local | S3 (RustFS en la misma región) |
|---|---|---|
| 1 K inserciones | 19 ms | 38 ms |
| Lote de 10 K | 17 ms | 114 ms |
| 1 K actualizaciones | 9 ms | 36 ms |
Las escrituras siempre son a velocidad local. El costo de S3 solo ocurre en el punto de control. Números con RustFS en la misma región de Fly (~2 ms RTT). S3 Express One Zone sería comparable.
pip install turbolite
- [Comandos esenciales](#core-commands)
- [Mejoras](#improvements)
- [Configuración](#configuration)
- [Opciones](#options)
- [Opciones de prioridad y severidad](#priority-and-severity-options)
- [Ejecución de múltiples comandos en secuencia](#multi-command-execution-in-sequence)
- [Uso de IA](#using-ai)
- [Demostración](#demo)
- [Instalación](#installation)
- [Requisitos](#requirements)
- [Licencia](#license)```python
import turbolite
conn = turbolite.connect("my.db", mode="s3",
bucket="my-bucket",
endpoint="https://t3.storage.dev")
conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)")
conn.execute("INSERT INTO users VALUES (1, 'alice', '[email protected]')")
conn.commit()
alice = conn.cursor().execute("SELECT * FROM users").fetchone()
print(alice[1])
>>> "alice"
Vea Instalación para Node, Go, Rust, modo solo local, y usando la extensión cargable .so directamente
turbolite está diseñado para las restricciones de S3 en lugar de las restricciones del sistema de archivos. Cada decisión fluye de este modelo:
turbolite añade capas de introspección e indirección entre SQLite y S3 que agrupan, comprimen, rastrean y obtienen páginas de manera eficiente.
SQLite usa un índice de árbol B y solicita una página a la vez. Sabe que la página N está en el desplazamiento de bytes N * tamaño_de_página. Y esas páginas se distribuyen aleatoriamente en el mapa de páginas para un acceso aleatorio eficiente. Pero en S3, obtener una página por solicitud significaría miles de GETs potencialmente aleatorios por consulta.
Pero las páginas no se crean de la misma manera. SQLite tiene diferentes tipos de páginas. turbolite separa los grupos de páginas por tipo: páginas de árbol B interiores, páginas de hoja de índice y páginas de hoja de datos.
Las páginas interiores se tocan en cada consulta para enrutar las búsquedas a las páginas hoja. turbolite las detecta, las almacena en lotes comprimidos en S3 y las carga con avidez al abrir el VFS. Después de eso, cada recorrido del árbol B es un acierto de caché.
Las páginas de hoja de índice reciben el mismo tratamiento: lotes separados, precarga en segundo plano perezosa, fijadas contra desalojo. Las consultas en frío solo necesitan obtener las páginas de datos.
turbolite aprovecha la introspección del árbol B para entender de qué árbol (una tabla o un índice) forma parte una página, y almacena inteligentemente esas páginas juntas en S3 como grupos de páginas: muchas páginas agrupadas en un solo objeto de S3. Lo suficientemente grandes para saturar el ancho de banda en la precarga, lo suficientemente pequeños para consultas puntuales. Por defecto: 256 páginas por grupo, ~16MB con páginas de 64KB.
Almacenar la misma tabla/índice juntos significa que hacemos la menor cantidad posible de GETs para consultas en frío.
turbolite indirecciona las búsquedas de páginas con un archivo de manifiesto que es la fuente de verdad sobre dónde vive cada página. Reemplaza el implícito offset = página * tamaño de SQLite con punteros explícitos. Las versiones antiguas de grupos de páginas nunca se sobrescriben; el PUT del manifiesto es el punto de confirmación atómico. Las versiones antiguas se convierten en basura, limpiada por gc().
SQLite utiliza páginas de 4KB por defecto para coincidir con el tamaño de página del disco del sistema de archivos. En S3, el tamaño de página del disco es irrelevante. Lo que importa es minimizar el número de solicitudes y maximizar la ramificación del árbol B. La respuesta son páginas grandes: turbolite utiliza páginas de 64KB por defecto. Menos páginas = menos viajes de ida y vuelta a S3 para llegar a una hoja.
Para hacer rápidas las consultas puntuales, turbolite utiliza compresión buscable: cada grupo de páginas se codifica como múltiples tramas zstd (~4 páginas por trama). El manifiesto almacena los desplazamientos de bytes por trama, por lo que en un fallo de caché se obtiene solo el subfragmento de ~256KB con la página necesaria mediante un GET de rango de S3, no todo el grupo.
La precarga tiene dos capas: proactiva (adelantarse al plan de consulta) y reactiva (adaptativa basada en fallos).
El adelantamiento al plan de consulta se ejecuta primero. Antes de que se ejecute una consulta, turbolite intercepta el plan de consulta de SQLite mediante EXPLAIN QUERY PLAN, extrae las tablas e índices exactos que la consulta tocará, y envía todos sus grupos de páginas al pool de precarga antes de que se lea siquiera la primera página. Una unión de cinco tablas que de otro modo desencadenaría cinco ciclos secuenciales de fallo-y-luego-extracción, en su lugar dispara las cinco obtenciones en paralelo al inicio de la consulta. Para consultas SCAN, esto significa que toda la tabla se precarga por adelantado.
Advertencia: SQLite admite una función de callback de traza por conexión. Si otra extensión reclama la ranura primero, el adelantamiento retrocede silenciosamente a la precarga reactiva.
La precarga reactiva maneja lo que el adelantamiento no cubre y actúa como respaldo. En un fallo de caché, ocurren dos cosas simultáneamente:
Los contadores de fallos se rastrean por árbol B, no globalmente. Una consulta de perfil que golpea usuarios (fallo 1) luego publicaciones (fallo 1) rastrea correctamente cada árbol en 1, no 2. Esto evita que una unión de múltiples tablas escale accidentalmente la precarga en cada árbol solo porque toca varios.
Cada fallo consecutivo avanza a través de una programación de precarga que controla qué fracción de grupos del mismo árbol precargar. turbolite selecciona una programación automáticamente según el plan de consulta:
[0.3, 0.3, 0.4]: para consultas SEARCH ... USING INDEX que escanean porciones desconocidas de índices. Agresiva desde el primer fallo porque no sabemos cuánto del índice se escaneará.[0.0, 0.0, 0.0]: para consultas puntuales y búsquedas de índice que golpean 1-2 páginas por árbol. Tres oportunidades gratuitas antes de cualquier precarga. Las programaciones con predominancia de cero superan a las de rampa temprana tanto en S3 Express como en Tigris.Puede ajustar la programación de precarga en el momento de abrir estableciendo prefetch.search / prefetch.lookup en TurboliteConfig — usted conoce la forma esperada de la carga de trabajo, por lo que el VFS no tiene que adivinarlo. Consulte Configuración de precarga.
Ambas programaciones aprovechan la introspección del árbol B: cada grupo precargado garantiza contener páginas del árbol correcto. Un ejemplo: si SQLite solicita una página de la tabla usuarios, luego solicita otra de la misma tabla, turbolite asume que se acerca un escaneo y precarga el resto de la tabla usuarios en segundo plano, y nada más. Sin la introspección del árbol B, accidentalmente obtendría la mitad de la tabla usuarios y la mitad de la tabla publicaciones solo porque los datos viven uno al lado del otro en el disco.
La anticipación de hoja de índice hace lo mismo para SEARCH indexado. Una hoja de índice ya enumera los rowids de la tabla que SQLite está a punto de solicitar — por lo que turbolite los resuelve a través de las páginas interiores en caché y precarga esas tramas de tabla en un lote en lugar de una por una, reduciendo el número de solicitudes.
turbolite tiene su propio caché de páginas en memoria que reemplaza el caché de páginas integrado de SQLite. El paginador de SQLite almacena páginas internamente en caché y nunca vuelve a leer del VFS para páginas en caché. Esto está bien para bases de datos de un solo escritor, pero para réplicas de solo lectura (seguidores HA, lectores que sondean el manifiesto), el caché de SQLite se vuelve obsoleto cuando los datos subyacentes cambian mediante replicación.
El caché de turbolite es consciente del manifiesto: cuando se dispara set_manifest() (nuevos datos de replicación), invalida las páginas afectadas tanto en el caché de disco como en el caché en memoria. Las escrituras también invalidan sus páginas en el caché en memoria. Esto garantiza lecturas frescas después de la replicación o las escrituras.
Arquitectura:``` SQLite (PRAGMA cache_size=0) -> turbolite VFS xRead -> in-memory page cache (64MB default, AtomicPtr, zero-lock reads) -> disk cache (NVMe pread) -> S3 (on miss)
**Configuración:**
- `cache.mem_budget` en `TurboliteConfig` (bytes). Por defecto: 64MB.
- Variable de entorno `TURBOLITE_MEM_CACHE_BUDGET` (p.ej., `128MB`, `1GB`).
- Establécelo en `0` para deshabilitar completamente la caché en memoria.
`turbolite.connect()` (Python/Go/TypeScript) deshabilita automáticamente la caché de páginas de SQLite y utiliza la de turbolite en su lugar. Los consumidores de Rust que usan directamente `Connection::open_with_flags_and_vfs` deben establecer `PRAGMA cache_size=0` para obtener el mismo comportamiento.
### Cifrado y Compresión
#### Compresión
Todos los datos se comprimen con zstd antes del almacenamiento. Los grupos de páginas usan codificación multiframe seekable que comprime independientemente cada frame (~4 páginas, ~256KB), por lo que una búsqueda puntual descomprime solo el frame relevante en lugar de todo el grupo de páginas. Los diccionarios zstd personalizados pueden mejorar aún más las tasas de compresión.
El modo local (no S3) también comprime a nivel de página con zstd. Consulta la CLI para las herramientas de entrenamiento de diccionarios.
#### Cifrado
Si el cifrado está habilitado, turbolite cifra todo: objetos S3, caché local, WAL, metadatos. Los datos de S3 usan AES-256-GCM con nonces aleatorios por frame (autenticado, detección de manipulaciones). Los datos locales usan AES-256-CTR con sobrecarga de tamaño cero. El cifrado ocurre después de la compresión: `plaintext → zstd → encrypt → S3`.
**Rotación de claves:** `rotate_encryption_key(config, new_key)` vuelve a cifrar, agrega o elimina el cifrado en todos los datos S3 sin descomprimir. `Some` a `Some` rota las claves, `Some` a `None` elimina el cifrado, `None` a `Some` lo agrega. A prueba de fallos: los objetos antiguos nunca se sobrescriben, la subida del manifiesto es el punto de confirmación atómico, y un paso de verificación confirma que los nuevos datos son legibles antes de confirmar. Los huérfanos de ejecuciones parciales se limpian con `gc()`.
## Fortalezas y Limitaciones
### Dónde turbolite es rápido
**Las búsquedas puntuales son el punto ideal.** En el nivel de caché `index`, una búsqueda puntual obtiene 1-2 sub-chunks mediante S3 range GET (~100KB cada uno). Las páginas interiores y de índice ya están en caché. En el nivel de caché `none`, añade ~120ms para la reobtención de la interior + primera página de datos. Esto funciona en cualquier tamaño de máquina.
**Escaneos con suficientes núcleos.** El pool de precarga satura el ancho de banda de S3 con programación adaptativa por árbol. Las consultas de búsqueda aumentan la precarga agresivamente desde el primer fallo; las consultas SCAN conscientes del plan precargan en bloque toda la tabla por adelantado. Suficientes hilos pueden sincronizar bases de datos de varios GB en segundos con 2-3 lotes de precarga.
### Dónde turbolite es lento
**Escaneos en máquinas pequeñas.** Con 1 hilo de precarga, un escaneo de 1.46GB toma segundos, no milisegundos. El cuello de botella son los viajes de ida y vuelta a S3: cada salto obtiene grupos en serie. Si tu primera consulta es un escaneo completo en una máquina de 1 vCPU, espera que el inicio sea doloroso.
**Mala sintonización de hilos.** Demasiado pocos hilos de precarga y los escaneos se detienen esperando a S3. Demasiados y el trabajo de SQLite en primer plano comienza a competir con las descargas. El valor predeterminado (`max(num_cpus - 1, 1)`) deja un núcleo para el trabajo en primer plano, pero las cargas de trabajo intensivas en escaneos en bases de datos grandes aún necesitan suficientes CPUs.
**Penalización de primera consulta.** La primera consulta en el nivel de caché `none` paga ~50-200ms por la carga de la página interior más al menos una obtención de datos. Si la consulta necesita una página de índice antes de que finalice la precarga en segundo plano, recurre a un GET de rango en línea.
### Limitaciones actuales
- **Turbolite independiente es de un solo escritor.** Dos máquinas escribiendo directamente en el mismo prefijo corromperán el manifiesto.
- **El modo HA/failover es experimental y se encuentra en `haqlite-turbolite`.** Esa pila combina las concesiones de HaQLite, la segmentación de páginas de turbolite y la replicación continua de WAL de walrust. Es la ruta prevista para despliegues multinodo, no el acceso directo de múltiples escritores a un prefijo de turbolite.
- **El envío de WAL es experimental.** Requiere el flag de feature `wal` + walrust. Consulta [Durabilidad](#durability).
Características de SQLite que **sí** funcionan: FTS, R-tree, JSON, modo WAL, modo de journal DELETE, VACUUM, autovacuum.
## Ajuste
### Parámetros generales
| Parámetro | Qué controla | Por defecto |
|-----------|-------------|---------|
| `prefetch.threads` | Hilos de trabajo para obtenciones S3 paralelas | max(num_cpus - 1, 1) |
| `cache.pages_per_group` | Páginas por objeto S3, más grande = menos PUTs, más bytes por obtención | 256 |
| `cache.gc_enabled` | Eliminar versiones antiguas de grupos de páginas después del checkpoint | true |
| `sync_mode` | Durabilidad del checkpoint: `Durable` (subida a S3 en el checkpoint) o `LocalThenFlush` (aplazar subida) | Durable |
### Programaciones de precarga
El frontrunning del plan de consulta (ver Arquitectura) es el mecanismo principal de precarga. Las programaciones reactivas a continuación actúan como respaldo cuando el frontrunning no está disponible o cuando las consultas acceden a páginas que no estaban en el plan.
| Estrategia | Cuándo | Programación por defecto | Qué sucede |
|----------|------|-----------------|--------------|
| **SCAN** (frontrun) | EQP dice `SCAN table` | Todos los grupos por adelantado | Precarga masiva de toda la tabla antes de la primera lectura. No se necesita programación de saltos. |
| **SEARCH** (reactivo) | EQP dice `SEARCH ... USING INDEX` | `[0.3, 0.3, 0.4]` | Precarga agresiva desde el primer fallo; escanea porciones de índice desconocidas. |
| **Lookup** (reactivo) | Consultas puntuales, sin información de EQP | `[0.0, 0.0, 0.0]` | Tres saltos libres, cero precarga. Las consultas puntuales rara vez se benefician de la precarga. |
Cada elemento es la fracción de grupos hermanos a precargar en el Nth fallo consecutivo de caché por árbol. Cuando los fallos superan la longitud del array, fracción=1.0 (todos los restantes).
**¿Por qué dos programaciones reactivas?** Las consultas SEARCH escanean porciones desconocidas de índices/tablas y necesitan un calentamiento agresivo. Las Lookups alcanzan 1-2 páginas por árbol y apenas necesitan precarga. Los contadores de fallos por árbol aseguran un seguimiento independiente: una consulta de perfil que alcanza usuarios (fallo 1) y luego posts (fallo 1) sigue cada árbol por separado.
### Configurando la precarga
Establece `prefetch.search` y `prefetch.lookup` en `TurboliteConfig` durante la construcción del VFS:```rust
use turbolite::tiered::{TurboliteConfig, PrefetchConfig};
let config = TurboliteConfig {
prefetch: PrefetchConfig {
search: vec![0.4, 0.3, 0.3],
lookup: vec![0.0, 0.0, 0.2],
query_plan: true,
..Default::default()
},
..Default::default()
};
Para el reajuste por consulta sin reabrir la conexión, usa la
función SQL turbolite_config_set (Phase Cirrus c). Cada envío
está limitado al identificador de la conexión solicitante y permanece en vigor hasta
que lo cambies de nuevo:```sql
SELECT turbolite_config_set('prefetch_search', '0.5,0.5,0.0');
SELECT turbolite_config_set('prefetch_lookup', '0.0,0.0,0.0');
SELECT * FROM posts WHERE created_at > ?; -- runs with the new schedule
### Index-leaf lookahead
Cuando una consulta utiliza un índice para encontrar filas en una tabla (`SEARCH ... USING INDEX`), la hoja del índice que SQLite lee ya nombra los rowids de la tabla que está a punto de recuperar. El lookahead analiza esos rowids, los resuelve a sus correspondientes frames de hoja de tabla a través de las páginas interiores en caché, y precarga los frames en un solo lote — de modo que las filas de la tabla llegan juntas en lugar de una ronda de ida y vuelta de S3 a la vez.
Está **activado por defecto** y solo se activa para los `SEARCH` indexados que persiguen una tabla. Los escaneos, las lecturas punto por rowid y las consultas completamente en caliente siguen la ruta normal sin alteraciones, por lo que rara vez hay motivo para desactivarlo. Necesita la precarga del plan de consulta (`plan_aware`, por defecto true).
El único caso para desactivarlo es una carga de trabajo completamente en caliente y sensible a la CPU, donde analizar cada hoja de índice cuesta un poco y no precarga nada porque las páginas ya están en caché:```sql
SELECT turbolite_config_set('lookahead', 'false');
O establece lookahead en TurboliteConfig / la variable de entorno TURBOLITE_LOOKAHEAD al momento de apertura.
Los llamadores Rust pueden invocar la misma ruta mediante turbolite::tiered::settings::set.
Nota: el prefetch es por conexión. Cada nueva conexión comienza con contadores de fallos por árbol en frío. El caché es compartido, por lo que una segunda conexión se beneficia de las páginas cacheadas por la primera.
Los schedules óptimos de prefetch dependen del equilibrio latencia-ancho de banda de tu backend S3. Probamos 10 pares de schedules en 6 consultas tanto en S3 Express (~4ms GET) como en Tigris (~25ms GET):
En S3 Express, off/off (sin prefetch en absoluto) es sorprendentemente competitivo para consultas puntuales porque cada GET de rango de sub-chunk es solo ~4ms. La brecha entre "sin prefetch" y "prefetch óptimo" es pequeña (23% para búsquedas puntuales) porque los GET individuales son baratos. En Tigris, la misma consulta se beneficia mucho más del prefetch (hasta 39% en idx-filter) porque cada viaje de ida y vuelta desperdiciado cuesta 25ms.
El efecto práctico: en backends de alta latencia, empuja los schedules de búsqueda más agresivamente y mantén los schedules de búsqueda puntual con más ceros iniciales. En S3 Express, los valores predeterminados funcionan bien y el ajuste proporciona ganancias menores. El rendimiento de escaneo completo es insensible al schedule en ambos backends porque el frontrunning del plan de consulta realiza un prefetch masivo de toda la tabla de antemano.
Usa tiered-tune (ver abajo) para encontrar schedules óptimos para tu backend y consultas específicos.
tiered-tune se conecta a una base de datos turbolite existente y barre schedules de prefetch contra tus consultas reales. En lugar de adivinar schedules, ejecuta tu carga de trabajo real y deja que la herramienta encuentre el mejor par:```bash
cargo run --release --features cloud,zstd --bin tiered-tune --
--prefix "databases/tenant-123"
--query "SELECT * FROM users WHERE id = ?1"
--query "SELECT p.*, u.name FROM posts p JOIN users u ON p.user_id = u.id WHERE p.id = ?1"
--iterations 10
cargo run --release --features cloud,zstd --bin tiered-tune --
--prefix "databases/tenant-123"
--query "SELECT * FROM orders WHERE user_id = ?1 ORDER BY created_at DESC LIMIT 20"
--search-schedules "0.3,0.3,0.4;0.5,0.5;1.0"
--lookup-schedules "0;0,0,0.1;0,0,0,0.1,0.2"
--iterations 10
El resultado es una tabla de comparación por consulta (como `tiered-bench --matrix`) que muestra p50, p90, recuento de GET y bytes por cada par de planes. La herramienta recomienda un plan e imprime la asignación `TurboliteConfig` para aplicarlo.
## Durabilidad
turbolite es una capa de almacenamiento, no un sistema de replicación. La durabilidad depende de cuándo los datos llegan a S3.
**Después del punto de control (checkpoint)**: grupos de páginas + manifiesto están en S3. S3 proporciona 11 nueves de durabilidad. Estos datos sobreviven a la pérdida de la máquina.
**Entre puntos de control**: las escrituras solo viven en el WAL local, en el disco local. Si la máquina falla antes del siguiente punto de control, esas escrituras se pierden.
La frecuencia del punto de control controla la compensación: puntos de control más frecuentes = ventana de datos en riesgo más pequeña, pero más PUTs a S3. El valor predeterminado es el punto de control automático de SQLite (cada 1000 marcos WAL).
### Modos de punto de control
turbolite admite dos modos de punto de control mediante `sync_mode` en `TurboliteConfig`:
**`SyncMode::Durable`** (predeterminado). El punto de control sube grupos de páginas a S3 mientras mantiene el bloqueo EXCLUSIVO de SQLite. Simple, completamente duradero en cada punto de control. Ninguna escritura o lectura puede continuar hasta que la subida se complete. Adecuado para la mayoría de las cargas de trabajo.
**`SyncMode::LocalThenFlush`**. El punto de control escribe solo en la caché de disco local (~1 ms de retención de bloqueo), luego libera el bloqueo. La persona que llama sube a S3 por separado mediante `flush_to_s3()`, durante lo cual las lecturas y escrituras continúan normalmente. Esto es útil para cargas de trabajo con muchas escrituras donde bloquear a los lectores durante la duración de una subida a S3 es inaceptable.
Entre el punto de control y el vaciado (flush), los datos existen solo en la caché de disco local. Un fallo del proceso no es problema (los datos están en el disco local y los registros de preparación capturan el contenido exacto de las páginas para subirlas). La pérdida de la máquina antes del vaciado significa que esas escrituras se pierden. El desalojo de caché es seguro: turbolite protege automáticamente las páginas pendientes del desalojo.
**Recuperación tras fallo**: Si el proceso falla entre el punto de control y el vaciado, los registros de preparación sobreviven en el disco. En la siguiente llamada `TurboliteVfs::new()`, se recuperan automáticamente y se ponen en cola para la siguiente llamada `flush_to_s3()`. Las lecturas se atienden inmediatamente desde la caché local sin esperar al vaciado.
### Envío de WAL (experimental)
Con la característica `wal` habilitada, turbolite envía marcos WAL a S3 mediante [walrust](https://github.com/russellromney/walrust), cerrando la brecha de durabilidad entre escrituras individuales y puntos de control.```toml
# Cargo.toml
turbolite = { version = "0.5", features = ["cloud", "zstd", "wal"] }
git clone https://github.com/nil0x42/manspider cd manspider pip install pipenv pipenv install pipenv shell
manspider --help
docker build -t manspider . docker run -it --rm --network host manspider --help
docker-compose up
pipx (Recomendado)pipx install git+https://github.com/N0tA1dan/manspider
manspider --help
let config = TurboliteConfig { wal_replication: true, // enable WAL shipping ..Default::default() };
turbolite y walrust se mantienen sincronizados mediante el cursor de reproducción almacenado como `manifest.change_counter`. Las rutas de importación/punto de control siembran ese cursor desde el contador de cambios de archivo de SQLite; la reproducción directa de páginas puede avanzarlo hasta la secuencia de conjunto de cambios confirmados más reciente. En un inicio en frío, turbolite materializa la base de datos a partir de grupos de páginas, luego walrust reproduce segmentos WAL con txid > `change_counter` para recuperar escrituras que ocurrieron después del último punto de control.
**Modelo de durabilidad con envío de WAL**: cada transacción confirmada se envía a S3 como un segmento WAL dentro del intervalo de sincronización (predeterminado 100ms). Si la máquina falla, se pierde como máximo un intervalo de sincronización de escrituras. Después del punto de control, los segmentos WAL con txid <= `change_counter` se recolectan como basura automáticamente.
El envío de WAL es complementario a SyncMode: SyncMode controla cómo los puntos de control llegan a S3, el envío de WAL hace que las escrituras individuales sean duraderas antes del punto de control.
### Modelo de consistencia
Escritor único, lectores de instantáneas. Un proceso escribe; los lectores ven el último manifiesto confirmado cuando abrieron. turbolite no es una base de datos distribuida y no coordina entre múltiples escritores.
## Modo Local (sin S3)
turbolite también funciona como un VFS comprimido/cifrado puramente local:
Compresión: zstd (predeterminado), lz4, snappy, gzip. Con zstd, puedes entrenar e incrustar diccionarios de compresión personalizados y rotarlos automáticamente para una compresión más eficiente. Los tamaños de página más grandes se comprimen mejor. Consulta la CLI para herramientas de entrenamiento.
Cifrado: AES-256-GCM por página.
La operación a nivel de página significa que la mayoría de las características de SQLite aún funcionan: FTS, R-tree, JSON, modo WAL. La mayoría de las otras extensiones de compresión/cifrado de SQLite operan a nivel de archivo o requieren compilaciones personalizadas.
## Instalación
Este repositorio es un espacio de trabajo de Cargo. El crate `turbolite` es la biblioteca pura de Rust en la raíz del espacio de trabajo. Los enlaces de lenguaje y la extensión cargable se encuentran en `turbolite-ffi/`.
**Python**: `pip install turbolite` — ver [turbolite-ffi/packages/python/](https://github.com/russellromney/turbolite/blob/HEAD/turbolite-ffi/packages/python/)```python
import turbolite
# Local compressed (no S3 needed)
conn = turbolite.connect("my.db")
# S3 cloud
conn = turbolite.connect("my.db", mode="s3", bucket="my-bucket", endpoint="https://t3.storage.dev")
# Manual extension loading for full control
import sqlite3
conn = sqlite3.connect(":memory:")
turbolite.load(conn)
conn.close()
conn = sqlite3.connect("file:my.db?vfs=turbolite", uri=True) # local
# For S3, prefer turbolite.connect(..., mode="s3", bucket=..., prefix=...).
# It registers a per-database VFS so multiple S3 volumes can share one process.
Node.js: npm install turbolite — ver turbolite-ffi/packages/node/
Rust:```toml [dependencies] turbolite = "0.5" # local VFS turbolite = { version = "0.5", features = ["cloud"] } # + S3 storage turbolite = { version = "0.5", features = ["encryption"] } # + encryption
**Go** (cgo, enlaza la biblioteca compartida):```bash
make lib-bundled # build libturbolite.{so,dylib}
// #cgo LDFLAGS: -L/path/to/target/release -lturbolite
// #include <stdlib.h>
// extern int turbolite_register_local_file_first(const char* name, const char* db_path, int level);
// extern void* turbolite_open(const char* path, const char* vfs_name);
// extern int turbolite_exec(void* db, const char* sql);
// extern char* turbolite_query_json(void* db, const char* sql);
// extern void turbolite_close(void* db);
import "C"
The recommended turbolite_register_local_file_first(name, db_path, level) está indexada
por la ruta de base de datos visible para el usuario. La función de nivel inferior
turbolite_register_local(name, cache_dir, level) aún se exporta para
integradores que quieran gestionar su propio directorio de caché. Consulte
examples/go/ para ver un ejemplo completo de servidor HTTP.
Construya la extensión cargable para cualquier lenguaje con load_extension de SQLite:```bash
make ext # produces target/release/turbolite.{so,dylib}
Please provide the Markdown content you wish to have translated.```c
sqlite3_enable_load_extension(db, 1);
sqlite3_load_extension(db, "path/to/turbolite", NULL, NULL);
// "turbolite" VFS (local) is always registered
// "turbolite-s3" is a single-volume convenience VFS when TURBOLITE_BUCKET is set
Para la historia de usuario de archivo primero, registre un VFS por base de datos que posea el app.db del llamador:```sql
SELECT turbolite_register_file_first_vfs('app', '/data/app.db');
-- now open /data/app.db via vfs=app; turbolite stores its sidecar
-- metadata at /data/app.db-turbolite/.
Para configurar el VFS `"turbolite"` predeterminado para el modo de archivo primero (file-first) en el momento de carga de la extensión, establece `TURBOLITE_DATABASE_PATH=/data/app.db` en el entorno antes de cargar la extensión. El sidecar es entonces `/data/app.db-turbolite/` y el parámetro de nivel inferior `TURBOLITE_CACHE_DIR` se ignora.
### Node.js```bash
npm install turbolite
```js
const { connect } = require("turbolite");
// File-first: /data/app.db is the local page image. // /data/app.db-turbolite/ holds hidden implementation state. const db = connect("/data/app.db"); db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)"); db.prepare("INSERT INTO users VALUES (?, ?)").run(1, 'alice');
const rows = db.prepare("SELECT id, name FROM users").all(); // [{ id: 1, name: 'alice' }] db.close();
`db` es una base de datos estándar de better-sqlite3. `connect()` registra un VFS de archivo primero por base de datos. Para exportar un archivo SQLite estándar (por ejemplo, para inspeccionarlo con la CLI de `sqlite3`), use la API de respaldo de better-sqlite3: `await db.backup('export.sqlite')`. Consulte la [documentación completa en turbolite-ffi/packages/node/](https://github.com/russellromney/turbolite/blob/HEAD/turbolite-ffi/packages/node/).
### Rust (local, archivo primero)```rust
use turbolite::tiered::{TurboliteVfs, TurboliteConfig};
// `app.db` is the user-visible local page image.
// `app.db-turbolite/` holds hidden implementation state.
let config = TurboliteConfig::for_database_path("/data/app.db");
let vfs = TurboliteVfs::new_local(config)?;
turbolite::tiered::register("turbolite", vfs)?;
let conn = rusqlite::Connection::open_with_flags_and_vfs(
"/data/app.db",
rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE | rusqlite::OpenFlags::SQLITE_OPEN_CREATE,
"turbolite",
)?;
El formulario de nivel inferior le permite seleccionar el directorio de caché directamente:```rust let config = TurboliteConfig { cache_dir: "/path/to/data".into(), // turbolite owns this dir ..Default::default() };
En ese caso, la imagen local es `/path/to/data/data.cache` en lugar de un `app.db` nombrado por el llamador. Los nuevos incrustadores deberían preferir la forma de archivo primero.
### Rust (S3 cloud)```rust
use turbolite::tiered::{TurboliteVfs, TurboliteConfig};
use hadb_storage::StorageBackend;
let config = TurboliteConfig::for_database_path("/data/app.db");
let storage: Arc<dyn StorageBackend> = /* your S3 backend */;
let vfs = TurboliteVfs::with_backend(config, storage, tokio::runtime::Handle::current())?;
turbolite::tiered::register("turbolite", vfs)?;
let conn = rusqlite::Connection::open_with_flags_and_vfs(
"/data/app.db",
rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE | rusqlite::OpenFlags::SQLITE_OPEN_CREATE,
"turbolite",
)?;
app.db es la imagen de página comprimida de turbolite. No se garantiza que pueda abrirse directamente con sqlite3 estándar. Para un archivo SQLite normal (por ejemplo, para la CLI de sqlite3), use la API de respaldo en línea de SQLite o el asistente de exportación específico del binding (conn.iterdump() en Python, db.backup() en Node).
turbolite incluye una CLI para inspeccionar, administrar e interactuar con bases de datos turbolite sin escribir Rust.```bash cargo install turbolite --features cloud,zstd
### Comandos```bash
# Inspect a database manifest
turbolite info --db my.db
turbolite info --db my.db --bucket my-bucket --endpoint https://t3.storage.dev
# Interactive SQLite shell (with turbolite VFS)
turbolite shell --db my.db
turbolite shell --db my.db --bucket my-bucket --read-only
# Download entire database from S3 into local cache
turbolite download --db my.db --bucket my-bucket --threads 8
# Export to plain SQLite (for migration or backup)
turbolite export --db my.db --output plain.db
# Import a plain SQLite file into turbolite S3 format
turbolite import --input plain.db --bucket my-bucket --prefix databases/my-db
Todos los comandos S3 aceptan las banderas --bucket, --prefix, --endpoint y --region, o leen de las variables de entorno TURBOLITE_BUCKET, TURBOLITE_PREFIX, AWS_ENDPOINT_URL y AWS_REGION.
Hay muchos proyectos en el espacio de SQLite sobre red. turbolite toma ideas de todos ellos.
El enfoque más común: colocar un archivo .db sin modificar en S3 o una CDN y emitir solicitudes HTTP Range GET cuando SQLite lee una página.
.dbi opcional que precolecta nodos interiores del B-tree para precarga - la misma idea que los paquetes de páginas interiores de turbolite. Diseñado para componerse con sqlite_zstd_vfs.Todos estos son de solo lectura y obtienen páginas sin comprimir del archivo sin procesar. Una búsqueda puntual transfiere una página sin procesar de 4KB (o 64KB) por solicitud.
Estos tratan el almacenamiento de objetos como la fuente de verdad y replican páginas individuales o conjuntos de cambios, permitiendo réplicas parciales y despliegues sin conexión/periféricos.
orbitinghail/graft): Un motor de almacenamiento transaccional para replicación perezosa, parcial y fuertemente consistente sobre S3. La extensión SQLite libgraft implementa un VFS que lee y escribe páginas de 4KB a través de volúmenes Graft. Usa compresión zstd en marcos y conjuntos de cambios basados en splinter. El primo arquitectónico más cercano a turbolite en el espacio de «replicar páginas, no marcos WAL», con un enfoque en la sincronización periférica multi-escritor en lugar de latencia de lectura en frío.Estos replican escrituras locales a S3 para copia de seguridad o restauración.
wa-sqlite. Mismo modelo de un objeto por página, adaptado para uso WASM/lado del cliente.Todos los benchmarks viven en benchmark/. Consulte benchmark/README.md para escenarios de despliegue (local, Fly.io, EC2).
El binario tiered-bench genera un conjunto de datos de redes sociales (usuarios, publicaciones, me gusta, amistades) y evalúa consultas en cada nivel de caché contra S3.
Un arnés separado benchmark/bench_s3vfs.py ejecuta las mismas consultas contra sqlite-s3vfs para una comparación directa. Se despliega mediante benchmark/fly-s3vfs.toml y utiliza el mismo generador de conjuntos de datos determinista que tiered-bench.```bash
TIERED_TEST_BUCKET=my-bucket AWS_ENDPOINT_URL=https://t3.storage.dev
cargo run --features zstd,cloud --bin tiered-bench --release --
--sizes 100000
cargo run --features zstd,cloud --bin tiered-bench --release --
--sizes 1000000 --prefetch-threads 8 --queries post --modes interior
cargo run --example quick-bench --features encryption --release
Flags clave: `--sizes` (conteos de filas), `--ppg` (páginas por grupo), `--prefetch-threads`, `--prefetch-search` (programación de BÚSQUEDA), `--prefetch-lookup` (programación de búsqueda), `--grouping` (posicional o btree), `--queries` (post/perfil/quién-dio-like/mutuos), `--modes` (ninguno/interior/índice/datos), `--skip-verify` (omitir COUNT(*) en máquinas pequeñas), `--iterations`, `--plan-aware` (habilitar búsqueda anticipada predictiva), `--matrix` (pares de programación de barrido). Programaciones por consulta: `--post-prefetch`/`--post-lookup`, `--profile-prefetch`/`--profile-lookup`, etc. (búsqueda y búsqueda son independientes por consulta).```bash
# Matrix mode: test 10 schedule pairs x 6 queries at cold level
cargo run --features zstd,cloud --bin tiered-bench --release -- \
--sizes 1000000 --import auto --plan-aware --matrix --iterations 10
# Tune schedules for your own database and queries
cargo run --features zstd,cloud --bin tiered-tune --release -- \
--prefix "databases/my-db" \
--query "SELECT * FROM users WHERE id = ?1" --param 42 \
--plan-aware --iterations 10
cargo test --features zstd # local VFS tests cargo test --features zstd,cloud # + S3 integration tests cargo test --features zstd,encryption # + encryption tests
## Notas
turbolite anteriormente se llamaba `sqlite-compress-encrypt-vfs`, también conocido como `sqlces`.
### Detalles del modelo de seguridad
Los datos en S3 usan AES-256-GCM con nonces aleatorios únicos por trama (autenticado, detección de manipulación). Los archivos locales usan AES-256-CTR con nonces deterministas (número de página / desplazamiento de bytes), proporcionando confidencialidad contra atacantes de disco en reposo. Los nonces deterministas de CTR significan que los atacantes con múltiples instantáneas podrían recuperar el XOR de los textos planos en desplazamientos reutilizados, coincidiendo con la compensación de la extensión SEE de SQLite. La caché local es efímera y recreable desde S3.
## Licencia
Apache-2.0
| Consulta | Tipo | Frío (S3 Express) | Frío (Tigris) |
|---|
| Publicación + usuario | búsqueda puntual + unión | 86 ms | 172 ms |
| Perfil | unión multíple (5 JOINs) | 251 ms | 479 ms |
| Quién-dio-like | búsqueda en índice + unión | 206 ms | 302 ms |
| Amigos mutuos | unión multíple búsqueda | 19 ms | 49 ms |
| Filtro indexado | escaneo de índice cubierto | 79 ms | 88 ms |
| Escaneo completo + filtro | escaneo completo de tabla | 476 ms | 532 ms |
| Nivel de caché | Qué está en caché | Qué se obtiene de S3 | Cuándo ocurre |
|---|
| ninguno | nada | todo | Inicio fresco, caché vacía |
| interior | páginas interiores del árbol B | páginas de índice + datos | Primera consulta después de abrir conexión |
| índice | páginas interiores + de índice | solo páginas de datos | Operación normal de turbolite |
| datos | todo | nada | Equivalente a SQLite local |
| Operación | SQLite | turbolite | Sobrecarga |
|---|
| Búsqueda puntual | 145 K/s | 73 K/s | 2.0x |
| Escaneo de rango | 8.8 K/s | 8.3 K/s | paridad |
| Escaneo completo de tabla | 56/s | 60/s | paridad |
| INSERT | 19 K/s | 23 K/s | paridad |
| UPDATE por PK | 40 K/s | 27 K/s | 1.5x |
| INSERT por lotes (en txn) | 685 K/s | 740 K/s | paridad |
| Restricción de S3 | Implicación |
|---|
| Los viajes de ida y vuelta son lentos | Minimizar el número de solicitudes. Escribir por lotes, precargar lecturas de forma agresiva. |
| El ancho de banda es un cuello de botella | Maximizar la utilización del ancho de banda. |
| PUT y GET cobran por operación | Un GET de 64KB cuesta lo mismo que un GET de 16MB. Optimizar el número de solicitudes, no la eficiencia en bytes. |
| Los objetos son inmutables | Nunca actualizar en el lugar. Escribir nuevas versiones, intercambiar un puntero. Sin corrupción por escritura parcial. |
| El almacenamiento es barato | No optimizar para el espacio. Sobredimensionar, mantener versiones antiguas, dejar que la recolección de basura limpie más tarde. |
| Carga de trabajo | Configuración | Por qué |
|---|
| OLTP mixto | Predeterminados | Plan-aware maneja escaneos, el schedule de búsqueda calienta índices, el schedule de búsqueda puntual se mantiene conservador. |
| Carga pesada de puntos (DBs de agentes) | prefetch.lookup: vec![0.0, 0.0, 0.0] | Las búsquedas puntuales casi nunca necesitan prefetch. |
| Análisis pesado de escaneos | prefetch.search: vec![0.5, 0.5], prefetch.query_plan: true | Calentamiento agresivo de búsqueda más prefetch masivo plan-aware. |
| Conservador (serverless con ráfagas) | prefetch.search: vec![0.1, 0.2, 0.3], prefetch.lookup: vec![0.0, 0.0, 0.1] | Ruido mínimo de prefetch. |
| Backend | Latencia GET | Mejor búsqueda puntual | Mejor perfil | Ganancia de ajuste |
|---|
| S3 Express | ~4ms | 74ms (off/off: 96ms) | 188ms (off/off: 212ms) | 5-23% sobre sin prefetch |
| Tigris | ~25ms | 192ms (off/off: 231ms) | 524ms (off/off: 616ms) | 8-34% sobre sin prefetch |
| turbolite | Raw-file range GETs | Litestream VFS | sqlite_web_vfs + zstd_vfs | mvsqlite | Graft | sqlite-s3vfs |
|---|
| Lecturas desde S3 | GETs de rango buscables en grupos de páginas comprimidas | GETs de rango en páginas sin procesar | GETs de rango en archivos LTX | GETs de rango en DB externa comprimida | búsquedas KV en FoundationDB | obtención diferida de páginas de 4KB / conjuntos de cambios | un GetObject por página |
| Escrituras a S3 | punto de control (un PUT por grupo) | no | no | no | sí (MVCC) | sí (replicación asíncrona de conjuntos de cambios) | un PUT por página |
| Compresión | zstd multiframe buscable | ninguna | ninguna | zstd (DB anidada) | codificación delta zstd | zstd en marcos | ninguna |
| Encriptación | AES-256-GCM por página | ninguna | ninguna | ninguna | ninguna | no listada | ninguna |
| Precarga | anticipación + planificación de saltos | ninguna o readahead básica | caché LRU | consolidación adaptativa | búferes de cliente | perezosa / bajo demanda | ninguna |
| Optimización de páginas interiores | detectadas, fijadas, empaquetadas por separado | ninguna | índice de páginas desde trailers LTX | archivo .dbi opcional | ninguna | no listada | ninguna |
| Bytes por búsqueda puntual (caché: índice) | ~100KB (un marco comprimido) | 4-64KB (una página sin procesar) | varía | varía | varía | 4KB (una página) | 4KB (una página) |
| Costo de escritura por 4096 páginas | ~$0.000005 (un PUT) | n/a | n/a | n/a | operaciones de FoundationDB | conjuntos de cambios por lote | ~$0.02 (4096 PUTs) |