Spawned children
Spawned children
A worker that runs exec('ffmpeg …'), resizes an image or renders a PDF starts
a separate process with its own memory, which the worker's RSS does not include
and the status page knows nothing about. The budget is charged for it all the
same. This page is how that memory is measured, why it is also declared, and
what it does to the plan.
Two numbers per worker
Every scrape reads each worker twice: its own RSS, and its subtree RSS, the worker plus every process descended from it. The difference is what the children cost. A plain web worker has none; a worker mid-transcode has a 600 MiB one. The subtree comes from one snapshot of the process table per round, so a host with forty pools pays for the walk once.
A snapshot misses what lives and dies between two scrapes. A scrape lands every 30 seconds and an ffmpeg may live for two, so short children are under-counted, and they are the memory that spikes.
The cgroup high-water mark
Where the master runs under a cgroup, the kernel keeps a high-water mark of
everything the cgroup has used, children included, continuously rather than
sampled. fpm-tune reads memory.peak (kernel 5.19), memory.max_usage_in_bytes
on cgroup v1, or the largest memory.current it has seen, and reports it in
the recommendation file and on /metrics:
; cgroup used 209.9MiB now, 460.2MiB at its peak (workers AND everything they spawned, the number the OOM killer enforces against)
It is the number the OOM killer enforces against, and the one that catches the child a sample missed. Sizing does not use it; compare it with the budget when a plan looks optimistic. On a host without a cgroup the subtree measurement stands alone.
Declaring the workload
Measurement covers the steady state. It does nothing for the first run: a freshly started media pool has spawned nothing yet and would be sized as if it never will, until the first transcode. So a pool can say what it is in its own configuration:
env[FPM_TUNE_WORKLOAD] = subprocess-heavy
or the host can set a default for pools that declare nothing:
fpm-tune plan --workload subprocess-heavy
| Class | Aliases | Assumption | Reserved per worker before measurement |
|---|---|---|---|
web |
api, simple |
workers spawn nothing (the default) | none |
bursty |
a 256 MiB child on a quarter of the workers at once | 64 MiB | |
subprocess-heavy |
subprocess, media, children |
a 512 MiB child on every worker | 512 MiB |
Names are case-insensitive, and the pool's marker wins over the host default. A
marker that matches none of these falls back to the host default and the plan
warns, naming the pool and the value. The declaration is a floor: once children
have been measured at more than it, the measurement takes over, and a pool
marked web that is caught spawning is reserved for anyway.
How it changes the plan
The child memory is folded into each worker's cost, and the allocator divides
the budget by that cost as it does for a worker's own memory. A
subprocess-heavy pool gets fewer workers; it never makes the plan fail, and
the allocator's guarantee of never committing more than the budget covers the
children too.
The measured figure is amortised. It is the high-water mark of a scrape's total child memory divided by the pool's worker count, taking the larger of the workers in that scrape and the pool's peak concurrency. A pool where two of eight workers were each running a 600 MiB ffmpeg records 150 MiB per worker, so multiplied back by eight it reserves the 1.2 GiB that was there, and a scrape that catches a two-worker ondemand pool mid-transcode does not record a whole child as the per-worker cost. The figure only climbs, until the baseline is reset.
What you will see
The plan's WHY column shows the child part as its own term:
www-forge dynamic 10 41 1.7GiB peak 33 workers busy; raised to 41, measured 41.9MiB/worker + 37.6KiB children
The recommendation file carries the total, and the worst single worker-plus-children seen:
; reserved for spawned children: 376.5KiB (folded into each worker's cost, sized to the workers planned)
; plus ~37.6KiB of children per worker (folded into the sizing; worst single worker+children seen 168.7MiB)
On /metrics: fpm_tune_pool_subtree_rss_bytes{pool} is the worst single
worker's whole footprint, fpm_tune_pool_child_rss_bytes{pool} the child
memory folded into each worker, fpm_tune_cgroup_memory_bytes{state="current"|"peak"}
the cgroup's own usage, and fpm_tune_budget_bytes{state="reserved_children"}
what the plan committed to children in total. A pool whose child bytes climb
while its worker bytes sit flat is the one to give a declaration.
The CPU those children burn is counted on the other axis: php-fpm's per-request CPU share includes every child the request waited for, so a transcode shows as a share above 100% in the CPU table even when the memory sample missed it.