docs(compose): the app CPU cap cannot separate compression from the request path

The comment read "CPU ceiling for the two image workers + ffmpeg poster
extraction", which describes a separation Docker cannot make: `compression.rs`
runs that work in `tokio::task::spawn_blocking` — same process, same cgroup as
every Axum handler — and `cpus`/`cpu_shares` are per-container.

What actually happens is worth knowing when sizing this box: `cpu.max` is
`120000 100000`, so two CPU-pegged blocking workers exhaust the 120 ms quota
after ~60 ms of each 100 ms period and the kernel freezes the WHOLE cgroup —
uploads, feed and SSE included — for the rest of it. Over a 100-photo burst
that is ~210 s during which every request can eat up to 40 ms of throttle.

The value stays at 1.2: an app that can take both cores starves Postgres, and
every request path goes through Postgres. A slightly stalled request beats a
starved database. The comment now says which knob actually shortens the
backlog (COMPRESSION_WORKER_CONCURRENCY) rather than implying this one does.
This commit is contained in:
fabi
2026-08-12 23:11:57 +02:00
parent ac04e27e34
commit e6aeaa0a8b

View File

@@ -178,9 +178,22 @@ services:
# Bounds a runaway ffmpeg transcode (large uploads, 2 workers) so it can't # Bounds a runaway ffmpeg transcode (large uploads, 2 workers) so it can't
# OOM the single box and take down Postgres. # OOM the single box and take down Postgres.
memory: 1G memory: 1G
# CPU ceiling for the two image workers + ffmpeg poster extraction. Bounded below # CPU ceiling for the WHOLE app container. Bounded below 2.0 so it can never take both
# 2.0 so the app can never take both cores on its own. # cores on its own, which is what protects Postgres.
# COMPRESSION_WORKER_CONCURRENCY=2 is the memory bound; this is the CPU one. # COMPRESSION_WORKER_CONCURRENCY=2 is the memory bound; this is the CPU one.
#
# It does NOT cap "the two image workers + ffmpeg" separately from the request path, as
# this used to claim. `compression.rs` runs that work in `tokio::task::spawn_blocking` —
# same process, same cgroup as every Axum handler — and `cpus`/`cpu_shares` are
# per-container, so nothing here can tell them apart. Concretely: `cpu.max` is
# `120000 100000`, so two CPU-pegged blocking workers exhaust the 120 ms quota after
# ~60 ms of each 100 ms period and the kernel then freezes the ENTIRE cgroup — uploads,
# feed and SSE included — for the remainder. Across a 100-photo burst (~210 s of
# draining) every request in that window can eat up to 40 ms of throttle stall.
#
# Kept anyway: an app that can take both cores starves Postgres, and every request path
# goes through Postgres. A slightly stalled request beats a starved database. If the
# backlog needs to drain faster, the knob is COMPRESSION_WORKER_CONCURRENCY, not this.
cpus: '1.2' cpus: '1.2'
# Half the default weight, and this is the ceiling's other half: the cap alone leaves # Half the default weight, and this is the ceiling's other half: the cap alone leaves
# 0.8 vCPU for db + frontend + caddy, which frontend and caddy can consume between them. # 0.8 vCPU for db + frontend + caddy, which frontend and caddy can consume between them.