Skip to content

Commit 627248a

Browse files
committed
README: motivation preface, five mermaid charts, pkg made co-equal
Section 0 argues the case - the -m bet and the per-worker -M multiplier - with a sizing-dilemma chart. The grow path, the GROW-BLOCKED machine (replacing the ASCII art), the measured arm-K cycle and the pkg burst multiplier each get a rendered diagram, and the quickstart now points at pkg_auto_scaling_profile.
1 parent b3a73b0 commit 627248a

2 files changed

Lines changed: 262 additions & 26 deletions

File tree

‎README.md‎

Lines changed: 131 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,56 @@ With no cap configured, **nothing changes**: the arena is fixed at
2525
never below the shrink floor, never above the ceiling
2626
```
2727

28+
## 0. Why an elastic arena
29+
30+
Every OpenSIPS deployment ships with two numbers somebody guessed:
31+
`-m`, the shared-memory arena all calls, dialogs and caches live in,
32+
and `-M`, the private arena **each worker process** gets. Both are
33+
fixed at boot. Both are a bet.
34+
35+
**Bet low and you lose at the worst possible moment.** Shared memory
36+
exhausts in the middle of your best traffic hour: allocations fail,
37+
calls drop, and the only fix is a restart with a bigger number —
38+
downtime, during the incident, on every node it touches.
39+
40+
**Bet high and you pay for it every quiet hour.** The arena is pinned
41+
physical memory; on the hugetlb tier it is carved out of the host at
42+
boot and nothing else can ever use it. A production load balancer we
43+
measured peaks at **11.9 MB** of shm over 15 hours of full traffic —
44+
inside a 128 MB fixed arena. And `-M` multiplies: 16 MB across one
45+
gateway's 54 workers is **864 MB** of committed RAM backing arenas that
46+
mostly hold under 3 MB each, because with a fixed `-M` you size *every*
47+
worker for the *busiest* worker's worst minute.
48+
49+
And the bet cannot be won, because the right number is a moving
50+
target — it shifts with traffic mix, dialog lifetimes, the module set,
51+
the season:
52+
53+
```mermaid
54+
xychart-beta
55+
title "One day, one node: fixed arena vs what the traffic needs (MB)"
56+
x-axis ["00h","03h","06h","09h","12h","15h","18h","21h","24h"]
57+
y-axis "MB" 0 --> 560
58+
line [512,512,512,512,512,512,512,512,512]
59+
line [64,32,32,96,224,336,368,160,64]
60+
line [41,17,15,83,201,318,344,138,49]
61+
```
62+
63+
*Top, flat: a fixed `-m 512` sized for the storm, paid around the
64+
clock. Middle, stepped: what v3 keeps committed — granule steps up
65+
under load, shrink lagging the peak by the quiet window. Bottom: live
66+
demand. (Illustrative shapes; a measured cycle is charted in
67+
Section 8.)*
68+
69+
v3 replaces both bets with a **range**: `-m 64:512 -M 16:32`. Start at
70+
the size you can defend, reserve the ceiling (address space — free),
71+
commit physical pages only as demand arrives, hand back what goes
72+
quiet. Growth stops at three independent limits — your policy, the
73+
backing tier, the host's real free RAM (pkg charged × the worker
74+
count). Every decision is a counter you can scrape, the one state that
75+
deserves a page is a latched event, and a dry-run mode narrates what it
76+
*would* have done before you let it do anything.
77+
2878
Every mechanism below was chosen **by measurement first** — including
2979
two designs that looked right on paper and were proven memory-corrupting
3080
before a line of allocator code was written. The log lines, MI output
@@ -33,6 +83,7 @@ mockups.
3383

3484
**Contents**
3585

86+
0. [Why an elastic arena](#0-why-an-elastic-arena)
3687
1. [Quickstart](#1-quickstart)
3788
2. [Concepts and architecture](#2-concepts-and-architecture)
3889
3. [Why the cap is on the command line](#3-why-the-cap-is-on-the-command-line)
@@ -85,6 +136,9 @@ auto_scaling_profile = MEM_SHM
85136
shm_auto_scaling_profile = MEM_SHM
86137
```
87138

139+
`pkg_auto_scaling_profile` takes the same grammar and does the same
140+
for every worker's private arena — Section 11.
141+
88142
Watch it work:
89143

90144
```bash
@@ -289,26 +343,54 @@ differentially — with one floor, a 16 MB pkg delta was refused while
289343
WARNING:core:hg_grow_ram_refused: pkg: refusing to grow by 16 MB: 144 MB effective (x9 processes) would leave the host under the 14589 MB floor (MemAvailable 14562 MB). Freeing host memory or lowering the floor lifts this.
290344
```
291345

346+
The whole grow path, trigger to publish:
347+
348+
```mermaid
349+
flowchart TD
350+
A["allocation would fail
351+
(chunk carve / large tier / region)"] -- "grow-and-retry" --> G
352+
P["proactive tick:
353+
profile up-window met"] --> G
354+
G["grow request, whole 16 MB granules"] --> L1{"admin ceiling:
355+
profile up-target, else the cap"}
356+
L1 -- "at ceiling: NOTICE, refused++" --> R["refusal counters
357+
(hg_shm_grow_refused / pkg twin)"]
358+
L1 -- ok --> L2{"tier: does the backing
359+
(hugetlb pool) cover the delta?"}
360+
L2 -- no --> R
361+
L2 -- ok --> L3{"host RAM floor:
362+
MemAvailable - delta >= floor
363+
(pkg: delta x worker count)"}
364+
L3 -- under --> R
365+
L3 -- ok --> C["mlock() the new granules:
366+
commit, verify tier per delta"]
367+
C --> W["publish pages to the buddy grid
368+
grows++, headroom NOTICE, cooldown armed"]
369+
R -- "resource refusals only" --> B["GROW-BLOCKED machinery
370+
(Section 7)"]
371+
```
372+
292373
---
293374

294375
## 7. GROW-BLOCKED — the alertable state
295376

296377
A **resource** refusal (host RAM, mlock limit — not an admin ceiling)
297378
should page someone *only if it means something*. The state machine:
298379

299-
```
300-
resource refusal
301-
idle ──────────────────────────► armed
302-
▲ │
303-
│ refusal recurs after a │ quiet for a full
304-
│ GC pass ─or─ a full │ sweep interval
305-
│ sweep interval with ▼
306-
│ refusals still accruing disarmed (back to idle)
307-
│ │
308-
grow succeeds, ▼
309-
or demand falls LATCHED ── gauge=1, one WARN, event raised
310-
below the floor's │ (re-raised every 5 min while held)
311-
recovery mark ◄─────────┘
380+
```mermaid
381+
stateDiagram-v2
382+
[*] --> idle
383+
idle --> armed: resource refusal
384+
armed --> idle: quiet for a full sweep interval
385+
armed --> LATCHED: refusal recurs after a GC pass
386+
armed --> LATCHED: full sweep interval, refusals still accruing
387+
LATCHED --> idle: a grow succeeds
388+
LATCHED --> idle: demand falls below the floor recovery mark
389+
note right of LATCHED
390+
gauge hg_shm_grow_blocked = 1
391+
one WARN + E_CORE_SHM_GROW_BLOCKED
392+
re-raised every 5 min while held
393+
end note
312394
```
313395

314396
Two latch routes exist because of a measurement: the original
@@ -419,6 +501,21 @@ Floor: never below `-m`/`-M`'s initial size — unless a profile says
419501
otherwise (Section 9): with a profile attached, the profile is the ask
420502
and `-m` is just the starting size.
421503

504+
A full cycle, measured on a live process (rig arm K, Section 17): grow
505+
under MI-driven load, cooldown, the shrink train once the quiet window
506+
passes, regrowth on the next pulse — committed walking 32→96→16→80 MB
507+
while the reservation never moves and the hugetlb pool gets every
508+
released page back:
509+
510+
```mermaid
511+
xychart-beta
512+
title "Measured: committed MB through one load cycle (reservation fixed)"
513+
x-axis ["idle", "hold", "cooldown", "quiet + shrink train", "second hold"]
514+
y-axis "committed MB" 0 --> 110
515+
bar [32, 96, 96, 16, 80]
516+
line [32, 96, 96, 16, 80]
517+
```
518+
422519
---
423520

424521
## 9. The profile — configuration reference
@@ -518,6 +615,27 @@ tune thresholds against the advice lines, flip to 0.
518615
**pool reservations** (each child's arena reserves its own cap at map
519616
time).
520617

618+
What the multiplier means on a real 54-worker gateway — to survive one
619+
worker's burst with a fixed `-M` you must hand the burst size to all
620+
54; with a range, the floor is the norm and the burst is one worker's
621+
temporary excursion:
622+
623+
```mermaid
624+
xychart-beta
625+
title "pkg, 12 of 54 workers: fixed -M sized for the burst vs v3 (MB)"
626+
x-axis ["w1","w2","w3","w4","w5","w6","w7","w8","w9","w10","w11","w12"]
627+
y-axis "MB per worker" 0 --> 36
628+
bar [32,32,32,32,32,32,32,32,32,32,32,32]
629+
bar [16,16,16,32,16,16,16,16,16,16,16,16]
630+
```
631+
632+
*First bars: fixed `-M 32` because worker 4 once needed 32 —
633+
1,728 MB across 54 workers, around the clock. Second bars: v3 with
634+
`-M 16:32` — 53 workers hold the 16 MB floor, the burst worker grows a
635+
granule and returns it after the quiet window; peak 880 MB. When the
636+
profile's down-target sits below `-M`, the floor drops further still.
637+
(Illustrative; the five-worker independence proof is below.)*
638+
521639
Proven: five workers each grew their own arena 8→24→40 MB
522640
independently, on per-delta-verified THP backing, with every stamped
523641
word intact.

‎mem/README.hg_malloc_v3.md‎

Lines changed: 131 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,56 @@ With no cap configured, **nothing changes**: the arena is fixed at
1919
never below the shrink floor, never above the ceiling
2020
```
2121

22+
## 0. Why an elastic arena
23+
24+
Every OpenSIPS deployment ships with two numbers somebody guessed:
25+
`-m`, the shared-memory arena all calls, dialogs and caches live in,
26+
and `-M`, the private arena **each worker process** gets. Both are
27+
fixed at boot. Both are a bet.
28+
29+
**Bet low and you lose at the worst possible moment.** Shared memory
30+
exhausts in the middle of your best traffic hour: allocations fail,
31+
calls drop, and the only fix is a restart with a bigger number —
32+
downtime, during the incident, on every node it touches.
33+
34+
**Bet high and you pay for it every quiet hour.** The arena is pinned
35+
physical memory; on the hugetlb tier it is carved out of the host at
36+
boot and nothing else can ever use it. A production load balancer we
37+
measured peaks at **11.9 MB** of shm over 15 hours of full traffic —
38+
inside a 128 MB fixed arena. And `-M` multiplies: 16 MB across one
39+
gateway's 54 workers is **864 MB** of committed RAM backing arenas that
40+
mostly hold under 3 MB each, because with a fixed `-M` you size *every*
41+
worker for the *busiest* worker's worst minute.
42+
43+
And the bet cannot be won, because the right number is a moving
44+
target — it shifts with traffic mix, dialog lifetimes, the module set,
45+
the season:
46+
47+
```mermaid
48+
xychart-beta
49+
title "One day, one node: fixed arena vs what the traffic needs (MB)"
50+
x-axis ["00h","03h","06h","09h","12h","15h","18h","21h","24h"]
51+
y-axis "MB" 0 --> 560
52+
line [512,512,512,512,512,512,512,512,512]
53+
line [64,32,32,96,224,336,368,160,64]
54+
line [41,17,15,83,201,318,344,138,49]
55+
```
56+
57+
*Top, flat: a fixed `-m 512` sized for the storm, paid around the
58+
clock. Middle, stepped: what v3 keeps committed — granule steps up
59+
under load, shrink lagging the peak by the quiet window. Bottom: live
60+
demand. (Illustrative shapes; a measured cycle is charted in
61+
Section 8.)*
62+
63+
v3 replaces both bets with a **range**: `-m 64:512 -M 16:32`. Start at
64+
the size you can defend, reserve the ceiling (address space — free),
65+
commit physical pages only as demand arrives, hand back what goes
66+
quiet. Growth stops at three independent limits — your policy, the
67+
backing tier, the host's real free RAM (pkg charged × the worker
68+
count). Every decision is a counter you can scrape, the one state that
69+
deserves a page is a latched event, and a dry-run mode narrates what it
70+
*would* have done before you let it do anything.
71+
2272
Every mechanism below was chosen **by measurement first** — including
2373
two designs that looked right on paper and were proven memory-corrupting
2474
before a line of allocator code was written. The log lines, MI output
@@ -27,6 +77,7 @@ mockups.
2777

2878
**Contents**
2979

80+
0. [Why an elastic arena](#0-why-an-elastic-arena)
3081
1. [Quickstart](#1-quickstart)
3182
2. [Concepts and architecture](#2-concepts-and-architecture)
3283
3. [Why the cap is on the command line](#3-why-the-cap-is-on-the-command-line)
@@ -79,6 +130,9 @@ auto_scaling_profile = MEM_SHM
79130
shm_auto_scaling_profile = MEM_SHM
80131
```
81132

133+
`pkg_auto_scaling_profile` takes the same grammar and does the same
134+
for every worker's private arena — Section 11.
135+
82136
Watch it work:
83137

84138
```bash
@@ -283,26 +337,54 @@ differentially — with one floor, a 16 MB pkg delta was refused while
283337
WARNING:core:hg_grow_ram_refused: pkg: refusing to grow by 16 MB: 144 MB effective (x9 processes) would leave the host under the 14589 MB floor (MemAvailable 14562 MB). Freeing host memory or lowering the floor lifts this.
284338
```
285339

340+
The whole grow path, trigger to publish:
341+
342+
```mermaid
343+
flowchart TD
344+
A["allocation would fail
345+
(chunk carve / large tier / region)"] -- "grow-and-retry" --> G
346+
P["proactive tick:
347+
profile up-window met"] --> G
348+
G["grow request, whole 16 MB granules"] --> L1{"admin ceiling:
349+
profile up-target, else the cap"}
350+
L1 -- "at ceiling: NOTICE, refused++" --> R["refusal counters
351+
(hg_shm_grow_refused / pkg twin)"]
352+
L1 -- ok --> L2{"tier: does the backing
353+
(hugetlb pool) cover the delta?"}
354+
L2 -- no --> R
355+
L2 -- ok --> L3{"host RAM floor:
356+
MemAvailable - delta >= floor
357+
(pkg: delta x worker count)"}
358+
L3 -- under --> R
359+
L3 -- ok --> C["mlock() the new granules:
360+
commit, verify tier per delta"]
361+
C --> W["publish pages to the buddy grid
362+
grows++, headroom NOTICE, cooldown armed"]
363+
R -- "resource refusals only" --> B["GROW-BLOCKED machinery
364+
(Section 7)"]
365+
```
366+
286367
---
287368

288369
## 7. GROW-BLOCKED — the alertable state
289370

290371
A **resource** refusal (host RAM, mlock limit — not an admin ceiling)
291372
should page someone *only if it means something*. The state machine:
292373

293-
```
294-
resource refusal
295-
idle ──────────────────────────► armed
296-
▲ │
297-
│ refusal recurs after a │ quiet for a full
298-
│ GC pass ─or─ a full │ sweep interval
299-
│ sweep interval with ▼
300-
│ refusals still accruing disarmed (back to idle)
301-
│ │
302-
grow succeeds, ▼
303-
or demand falls LATCHED ── gauge=1, one WARN, event raised
304-
below the floor's │ (re-raised every 5 min while held)
305-
recovery mark ◄─────────┘
374+
```mermaid
375+
stateDiagram-v2
376+
[*] --> idle
377+
idle --> armed: resource refusal
378+
armed --> idle: quiet for a full sweep interval
379+
armed --> LATCHED: refusal recurs after a GC pass
380+
armed --> LATCHED: full sweep interval, refusals still accruing
381+
LATCHED --> idle: a grow succeeds
382+
LATCHED --> idle: demand falls below the floor recovery mark
383+
note right of LATCHED
384+
gauge hg_shm_grow_blocked = 1
385+
one WARN + E_CORE_SHM_GROW_BLOCKED
386+
re-raised every 5 min while held
387+
end note
306388
```
307389

308390
Two latch routes exist because of a measurement: the original
@@ -413,6 +495,21 @@ Floor: never below `-m`/`-M`'s initial size — unless a profile says
413495
otherwise (Section 9): with a profile attached, the profile is the ask
414496
and `-m` is just the starting size.
415497

498+
A full cycle, measured on a live process (rig arm K, Section 17): grow
499+
under MI-driven load, cooldown, the shrink train once the quiet window
500+
passes, regrowth on the next pulse — committed walking 32→96→16→80 MB
501+
while the reservation never moves and the hugetlb pool gets every
502+
released page back:
503+
504+
```mermaid
505+
xychart-beta
506+
title "Measured: committed MB through one load cycle (reservation fixed)"
507+
x-axis ["idle", "hold", "cooldown", "quiet + shrink train", "second hold"]
508+
y-axis "committed MB" 0 --> 110
509+
bar [32, 96, 96, 16, 80]
510+
line [32, 96, 96, 16, 80]
511+
```
512+
416513
---
417514

418515
## 9. The profile — configuration reference
@@ -512,6 +609,27 @@ tune thresholds against the advice lines, flip to 0.
512609
**pool reservations** (each child's arena reserves its own cap at map
513610
time).
514611

612+
What the multiplier means on a real 54-worker gateway — to survive one
613+
worker's burst with a fixed `-M` you must hand the burst size to all
614+
54; with a range, the floor is the norm and the burst is one worker's
615+
temporary excursion:
616+
617+
```mermaid
618+
xychart-beta
619+
title "pkg, 12 of 54 workers: fixed -M sized for the burst vs v3 (MB)"
620+
x-axis ["w1","w2","w3","w4","w5","w6","w7","w8","w9","w10","w11","w12"]
621+
y-axis "MB per worker" 0 --> 36
622+
bar [32,32,32,32,32,32,32,32,32,32,32,32]
623+
bar [16,16,16,32,16,16,16,16,16,16,16,16]
624+
```
625+
626+
*First bars: fixed `-M 32` because worker 4 once needed 32 —
627+
1,728 MB across 54 workers, around the clock. Second bars: v3 with
628+
`-M 16:32` — 53 workers hold the 16 MB floor, the burst worker grows a
629+
granule and returns it after the quiet window; peak 880 MB. When the
630+
profile's down-target sits below `-M`, the floor drops further still.
631+
(Illustrative; the five-worker independence proof is below.)*
632+
515633
Proven: five workers each grew their own arena 8→24→40 MB
516634
independently, on per-delta-verified THP backing, with every stamped
517635
word intact.

0 commit comments

Comments
 (0)