Skip to content

ciris-status holds ~1 GB of anonymous heap in 8 glibc arenas — server has 1 arena and 40% less heap on a 2.4x larger corpus #69

Description

@emooreatx

ciris-status sits at 88.8% of its 1.5 GB limit and does not come down across restarts, while ciris-server — same host, same substrate, a corpus 2.4x larger — sits at 59.5% of 2 GB.

This is not the corpus/page-cache problem in CIRISServer#527. It is anonymous heap, and the arena count points at allocator behaviour specific to the status node.

Measured on the live node

108.61.242.236, cirisstatus v0.3.60 (substrate 0.5.198 / edge 20.1.1 / persist 40.0.0), container limit 1.5 GB.

VmRSS      1,151 MB
RssAnon    1,137 MB   <-- 98.6% of RSS is heap
RssFile       13.6 MB <-- almost nothing is file-backed
VmSwap       332 MB
VmPeak     2,402 MB

corpus on disk       584 MB   (ciris_engine.db 374 MB, status.db 146 MB, WAL 64 MB)
of which resident     13.6 MB

The database is essentially not in RSS. Whatever is holding the memory is allocated, not mapped — so tuning cache or shrinking the corpus will not touch it.

The asymmetry that localises it

Same binary substrate, same box, same allocator (libc.so.6), taken at the same moment:

ciris-status ciris-server
threads 8 12
64 MB malloc arenas 8 1
RssAnon 1,137 MB 694 MB
VmSwap 332 MB 813 MB
corpus on disk 584 MB 1,400 MB

ciris-server has more threads and a larger corpus but one arena and 40% less heap. So this is not "the substrate costs 1 GB" — it is something in the status node's allocation pattern.

glibc creates a new arena when a thread contends on an existing one, up to 8 x ncores (16 here; MALLOC_ARENA_MAX is unset on both). Reaching 8 arenas on a 2-core box means sustained concurrent allocation from several threads.

The arenas are real, not reserved

Worth ruling out "virtual address space, never touched":

size=64 MB  rss=62 MB  ( 97% touched)
size=64 MB  rss=63 MB  (100% touched)
size=64 MB  rss=64 MB  (100% touched)
size=64 MB  rss=64 MB  (100% touched)
size=64 MB  rss=63 MB  (100% touched)
size=64 MB  rss=54 MB  ( 85% touched)
size=64 MB  rss=63 MB  (100% touched)
size=64 MB  rss=63 MB  (100% touched)

Every arena is 85-100% resident — 510 MB of genuinely dirtied pages. Alongside them sit large discrete regions that are not arenas:

size=128 MB rss=125 MB
size=128 MB rss=124 MB
size= 93 MB rss= 88 MB
size= 92 MB rss= 92 MB
size= 93 MB rss= 62 MB

Total across all regions >=64 MB: ~990 MB resident.

It is being paged out, not absorbed

Sampled over 60 s:

03:01:25  RssAnon 1089 MB  swap 345 MB
03:01:45  RssAnon 1055 MB  swap 409 MB
03:02:05  RssAnon 1027 MB  swap 436 MB
03:02:25  RssAnon 1028 MB  swap 436 MB

RSS is falling only because the kernel is pushing it to swap; committed total (anon + swap) is flat-to-rising at ~1.46 GB. On a 3.9 GB host also running the canonical, that is a meaningful share, and swap-backed heap is what produces the io-stall pressure seen elsewhere.

Workload context

The status node's dominant activity is the adapter, not the fabric:

102  ciris_status::adapter
 40  ciris_server::scorer
  2  ciris_server::compose

Each round: emitted=3 failed=0 skipped=14 total=17 — it evaluates 17 components and emits 3-4 observations. That is a small amount of output for ~1 GB of resident heap, which is what makes the number look wrong rather than merely large.

What would help, cheapest first

  1. Set MALLOC_ARENA_MAX=2 (or 1) for this container and re-measure. Consolidating 8 arenas is the standard mitigation for glibc arena bloat in threaded services and typically reclaims a large fraction. It is a libc tunable rather than application config, but note it sits in tension with the node's deliberate zero-env boot contract — worth a decision rather than an assumption.
  2. Consider a different allocator (jemalloc / mimalloc). Both return memory to the OS far more readily than glibc under this pattern; the process currently links plain libc.so.6.
  3. Identify the two 128 MB and three ~93 MB regions. They are single large allocations, not arenas, and together are ~490 MB — worth knowing whether they are a cache, a decoded model, or per-round buffers that are not being reused.
  4. Confirm plateau vs leak. I only have a 60 s window; committed total looked flat but I cannot rule out slow growth. If it is a plateau, arena tuning likely resolves it; if it grows, that is a separate defect.

Happy to run MALLOC_ARENA_MAX as a controlled experiment on the live node and report the before/after — it needs one container restart and no code change. Say the word and I will, or leave it if you would rather the fix come from the allocation side.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions