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
- 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.
- 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.
- 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.
- 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.
ciris-statussits at 88.8% of its 1.5 GB limit and does not come down across restarts, whileciris-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(substrate0.5.198/ edge 20.1.1 / persist 40.0.0), container limit 1.5 GB.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-serverhas 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_MAXis 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":
Every arena is 85-100% resident — 510 MB of genuinely dirtied pages. Alongside them sit large discrete regions that are not arenas:
Total across all regions >=64 MB: ~990 MB resident.
It is being paged out, not absorbed
Sampled over 60 s:
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:
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
MALLOC_ARENA_MAX=2(or1) 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.libc.so.6.Happy to run
MALLOC_ARENA_MAXas 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.