bottomless: finished S3 upload tasks are never reaped, so sqld memory grows by ~10 KB per WAL batch
Valutazione
- Difficoltà
- 2/5
- Tempo stimato
- 1-3 ore
- Idoneità per principianti
- 83/100
Direzione di ricerca
Inizia da bottomless/src/replicator.rs e segui il ciclo di caricamento su S3 in Replicator::with_options; leggi come JoinSet gestisce le attività completate mentre riceve le richieste. L’issue propone di recuperare le attività completate all’interno di quel ciclo. Conferma la modifica riproducendo caricamenti WAL continui e verificando che la memoria non cresca più a ogni batch.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Summary
With bottomless replication enabled, sqld's memory grows linearly with the number of WAL batches it uploads and never comes back down. Every uploaded batch leaves about 10 KB behind. A database with a small but steady write stream (one write per second, default 1 s batch interval) grows by roughly 80–130 MB per day until the process is restarted or OOM-killed.
This may be the same report as #1781. It is separate from the snapshot-upload defects in #2260, which does not change the WAL upload loop.
Cause
In bottomless/src/replicator.rs, Replicator::with_options spawns the S3 upload loop. The loop creates an inner JoinSet and spawns one task per upload request:
let mut join_set = JoinSet::new();
while let Some(req) = frames_inbox.recv().await {
// ...
join_set.spawn(async move { /* put_object, remove_file, update progress */ });
}
while join_set.join_next().await.is_some() {}
Nothing reads the results of finished tasks until frames_inbox closes, which happens only at shutdown. A JoinSet keeps every finished task (its allocation and output) until it is joined, so each uploaded batch stays in memory for the lifetime of the replicator. new_generation reuses the same replicator, so the set is not reset at checkpoints either.
Fix
Reap finished tasks inside the loop:
while let Some(req) = frames_inbox.recv().await {
+ while join_set.try_join_next().is_some() {}
tracing::trace!("Received S3 upload request: {}", req.path);
Reproducer
Local Docker, ghcr.io/tursodatabase/libsql-server:361d082 (same code in d6c75af / v0.24.33), S3-compatible store (RustFS) on the same network.
sqld environment (both containers):
SQLD_ENABLE_BOTTOMLESS_REPLICATION=true
LIBSQL_BOTTOMLESS_ENDPOINT=http://s3:9000
LIBSQL_BOTTOMLESS_BUCKET=sqld-leak
LIBSQL_BOTTOMLESS_DATABASE_ID=<unique per container>
LIBSQL_BOTTOMLESS_BATCH_INTERVAL_SECS=1
SQLD_CHECKPOINT_INTERVAL_S=60
LIBSQL_DISABLE_METRICS=true
Load: 10 INSERT OR REPLACE statements per second per container over HTTP. Memory: anon from the container's memory.stat.
| 45 min, 10 writes/s | anon growth, minute 16 → 38 | per uploaded batch |
|---|---|---|
361d082, batch every 1 s |
29.2 → 43.1 MB | 10.8 KB |
d6c75af, batch every 1 s |
same slope | 11.2 KB |
361d082, batch every 60 s |
flattens | — |
361d082 + fix above, batch every 1 s |
18.2 → 21.8 MB | 2.8 KB (see below) |
| bottomless off | 6.3 → 6.6 MB | — |
Growth tracks the number of uploaded batches, not the number of writes: the same write load with a 60 s batch interval stays nearly flat.
Remaining growth with the fix
With the fix, about 2.8 KB per batch still accumulates. A 15-minute rheaper heap profile (/profile/heap/enable) of the patched build shows the live Rust heap flat at ≈ 0.25 MB and only 0.17 MB of allocations never freed, with no growing call stack. So the remainder is not a Rust-side leak. It is glibc keeping freed memory in per-thread arenas: the same patched build with MALLOC_ARENA_MAX=2 stays flat at 13.5 MB from minute 18 to minute 41 (1,380 batches), while the patched build with default glibc settings rises 17.0 → 21.2 MB over the same window.
Environment
- libsql-server
361d082andd6c75af(official images), linux/arm64 under Docker Desktop; same growth pattern observed on linux/amd64 in production. - Allocator: glibc (system).
- Lingua principale
- C
- Stelle
- 17.3k
- Fork
- 545
- Metriche di merge delle PR
- Nessuna PR unita negli ultimi 30g
Preparare l'ambiente
- Include un Dockerfile o un file Docker Compose
- Nessun modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di tursodatabase/libsql
-
Rust local Row::get_value truncates embedded NUL TEXT and panics on malformed UTF-8 (0.9.30)Forse già presa @Sanjays2402 l’ha presa 11 giorni fa. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100
tursodatabase/libsql#2285 ·
-
libsql-ffi: broken builds under Nix on macOSForse già presa @frectonz l’ha presa 61 giorni fa. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 86/100
tursodatabase/libsql#2275 ·
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 65/100
tursodatabase/libsql#2226 ·
-
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 65/100
tursodatabase/libsql#1925 ·
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 62/100
tursodatabase/libsql#1892 ·
Tutte le issue di tursodatabase/libsql
Issue simili
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
polhenarejos/pico-fido#302 ·
-
开发板墙缺少 ESP32_S31_MOSAICOAperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 67/100
espressif/esp-vision#16 · 1 commento ·
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 74/100
libsdl-org/SDL#16577 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 66/100
I maintainer di solito rispondono entro 1 giorno
-
halt_error adds a trailing newline for non-string input, contrary to the manualForse già presa @Huang-404-Q l’ha presa oggi. Aperta
Difficoltà 1/5 1-3 ore Idoneità per principianti 88/100
I maintainer di solito rispondono entro 2 giorni