Skip to content

perf: reduce server idle memory retention (jemalloc decay + pool runtime threads) - #162

Open
huajq wants to merge 2 commits into
kvcache-ai:mainfrom
huajq:perf/jemalloc-decay-fc-pool-runtime
Open

perf: reduce server idle memory retention (jemalloc decay + pool runtime threads)#162
huajq wants to merge 2 commits into
kvcache-ai:mainfrom
huajq:perf/jemalloc-decay-fc-pool-runtime

Conversation

@huajq

@huajq huajq commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

What

Two small changes that reduce the API server's idle/background memory
footprint:

  1. Tune jemalloc through an embedded malloc_conf string
    (dirty_decay_ms:1000,muzzy_decay_ms:1000,background_thread:true).
  2. Pin the Firecracker warm pool's private Tokio runtime to a single
    worker thread.

Why

A production memory audit showed two server-side hygiene issues:

  • After bursts (RocksDB opens, image resolution, template builds),
    server RSS climbs and is slow to fall back: jemalloc's default
    dirty-page decay is 10s (muzzy_decay_ms defaults to 0, i.e.
    immediate), so burst pages linger as retained RSS well after the
    burst. Shortening both to 1s returns them within ~2s.
    Note on mechanism: background purging threads are already active —
    the background_threads cargo feature makes tikv-jemalloc-sys
    bake background_thread:true into the jemalloc build via
    --with-malloc-conf (build.rs). It is included in the string only
    for explicitness; the effective change here is the decay tuning.
  • The Firecracker pool's owned runtime uses the default multi-thread
    builder, parking one worker thread per CPU core for the process
    lifetime, although it only drives occasional I/O-bound block_on
    calls from the maintenance thread.

Related issue

N/A — small performance hygiene change from a direct memory audit; no
tracking issue.

Scope and non-goals

  • In scope: server allocator decay tuning; pool runtime thread count.
  • Out of scope: ublk-daemon memory (it does not use jemalloc), guest
    memory, and any pool watermark/sizing changes.

Design and behavior changes

  • malloc_conf is jemalloc's link-time configuration mechanism and is
    parsed at allocator init, before main(); the TOML config system
    therefore cannot express these settings. Precedence (lowest first):
    build-time config_malloc_conf (already carries
    background_thread:true), link-time malloc_conf symbol (this
    change), /etc/malloc.conf, MALLOC_CONF env var — later sources
    win per key, so deployments keep an env-level override hatch.
  • The pool runtime keeps new_multi_thread() with worker_threads(1),
    so the existing spawn-based fill logic behaves identically on one
    worker; pool refill concurrency is unchanged (driven by
    fill_concurrency, cooperative on the single worker).

Compatibility and operations

  • Public API or generated protocol: N/A — untouched.
  • Configuration or defaults: no config changes; MALLOC_CONF remains
    available as a per-environment override.
  • Snapshot manifest, artifact layout, or storage format: N/A.
  • Upgrade and rollback: revert-safe; no state migration.
  • Host requirements, permissions, ports, or dependencies: none added.

Validation

  • make fmt
  • make clippy
  • make test-unit
  • Relevant Rust integration tests — skipped: no sandbox-lifecycle
    behavior change; changes are allocator tuning and a runtime
    worker-count setting covered by existing unit tests.
  • make -C services test — N/A: services/ untouched.
  • Generated clients/server regenerated — N/A: no generated code.
  • Documentation updated — N/A: no user-facing configuration added;
    MALLOC_CONF is jemalloc's built-in override channel.
  • Benchmarks or performance comparison completed — skipped: change
    affects memory retention timing and idle thread count, not a
    latency/throughput path.

Commands and results:

$ make fmt          # cargo fmt --all -- --check — clean
$ make clippy       # workspace --all-targets --all-features -D warnings — clean
$ make test-unit    # 700 passed, 1 failed (pre-existing, see above)
$ nm target/release/server | grep malloc_conf
000000000397c378 d config_malloc_conf   # build-time baked conf (background_thread:true)
000000000396bc40 D malloc_conf          # this change (link-time symbol)
$ MALLOC_CONF="abort_conf:true" ./target/release/server --setup-only
# exit 0 — embedded config string parses; all options valid

Skipped checks and reasons: integration tests and benchmarks (no
lifecycle or latency-path change); test-unit failure is pre-existing on
the base commit in a root test environment, unrelated to these files.

Risks and reviewer notes

  • jemalloc: shorter decay means more frequent madvise purging after
    bursts; the purging runs on jemalloc's background thread (already
    enabled at build time), so the allocation path is unaffected. Config
    validity verified with an abort_conf:true smoke run.
  • Pool runtime: single worker serializes maintenance block_on work;
    refill stays I/O-bound and off the sandbox critical path.
  • Review focus: src/bin/server.rs (new exported symbol),
    src/sandbox/firecracker/pool.rs (worker_threads).

Checklist

  • The PR contains one coherent change and no unrelated formatting or
    refactoring.
  • New behavior is covered by tests, or I explained why testing is
    impractical.
  • Logs and examples contain no credentials, tokens, or private
    registry information.
  • I did not manually edit generated code without updating its
    source and regenerating it.

@github-actions

Copy link
Copy Markdown
Contributor

OpenCodeReview: Review complete: 0 finding(s) across 2 selected item(s).

huajq added 2 commits August 13, 2026 13:34
Set dirty_decay_ms=1000 (from the 10s default) and
muzzy_decay_ms=1000 (from 0, i.e. immediate purge) so pages freed by
burst allocations (RocksDB opens, image resolution, template builds)
are returned to the OS within ~2s. background_thread:true is already
baked into the jemalloc build by tikv-jemalloc-sys (via the
background_threads cargo feature); it is repeated here only for
explicitness. MALLOC_CONF can still override per environment.
The pool's owned runtime only drives occasional I/O-bound block_on
calls (spawn Firecracker, poll its API socket) from the maintenance
thread. The default multi-thread runtime parks one worker per CPU
core for the process lifetime.
@huajq
huajq force-pushed the perf/jemalloc-decay-fc-pool-runtime branch from 777b940 to 56de617 Compare August 13, 2026 05:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant