Documentation
¶
Overview ¶
Package plan turns observations and remembered baselines into an allocation.
It is the seam where bootstrap becomes learned. Everything below it is either pure (allocate) or purely mechanical (observe, budget, state); the judgement about whether a pool has been watched long enough to be believed lives here, in one place, so it can be read and argued with.
Index ¶
Constants ¶
const KnownWorkloads = "web, bursty, subprocess-heavy"
KnownWorkloads is the canonical set, for help text and warnings.
Variables ¶
var ( // WorkloadWeb is a pool whose workers spawn nothing: a plain web or API pool // serving requests in PHP. Its whole cost is its own worker memory, which is // what this tool measured before any of this existed. The default. WorkloadWeb = Workload{Name: "web", ChildBytes: 0, ConcurrentFraction: 0} // WorkloadBursty spawns a child now and then — a pool that occasionally // generates a PDF or resizes an upload, not on every request. A few workers // are assumed busy with a child at once. WorkloadBursty = Workload{Name: "bursty", ChildBytes: 256 << 20, ConcurrentFraction: 0.25} // WorkloadSubprocessHeavy shells out on most requests — transcoding, image // processing, PDF rendering — so every worker is assumed to have a child. // This is the class that under-provisions catastrophically without an // up-front reservation. WorkloadSubprocessHeavy = Workload{Name: "subprocess-heavy", ChildBytes: 512 << 20, ConcurrentFraction: 1.0} )
The named workloads. The numbers are deliberately round bootstrap guesses, not measurements — they are a floor a real observation is expected to replace, and their only job is to keep a freshly started subprocess-heavy pool from being sized as if its ffmpeg were free.
var DefaultProfile = Profile{
Name: "default",
WorkerBytes: 48 << 20,
ReserveFraction: 0.15,
ReserveFloorBytes: 256 << 20,
}
DefaultProfile is used when no other is given.
ReserveFraction 0.15 targets ~85% of the budget for workers — a stable utilisation that leaves headroom for the OS, page cache and per-request spikes without stranding memory. It applies to php-fpm's budget: on a bare VM that is already what the good-neighbour cap left after other services (budget.WithNeighbors), and the per-worker cost the allocator divides it by folds in the memory a worker's spawned children use. Raise it with --reserve for a more cautious host, or set a fixed amount.
Functions ¶
func LearnFrom ¶
LearnFrom folds a round of observations into the store.
It deliberately does NOT record the ceiling counters — see RecordCounters.
func RecordCounters ¶
RecordCounters stores the ceiling counters for the NEXT round to compare against. It must be called AFTER Build.
Splitting this out of LearnFrom is the whole point. Both ran before the plan was built, so LastMaxChildrenReached was overwritten with the current reading and hitCeiling then compared that reading against itself: the delta was always zero and the counter signal never fired once, in any round, since it was written. Saturation detection rested entirely on the listen queue without anyone noticing there was supposed to be a second signal.
Types ¶
type Input ¶
type Input struct {
Limits budget.Limits
Views []observe.PoolView
State *state.State
Profile Profile
// Workload is the default assumption about what pools do — whether their
// workers spawn subprocesses — used to reserve for children before any has
// been observed. A pool overrides it with a marker in its own config
// (PoolView.Workload). The zero value reserves nothing, which is WorkloadWeb
// and the behaviour before workloads existed.
Workload Workload
// CgroupUsage is what the master's cgroup has actually used, where there is
// a cgroup. HasCgroupUsage is false on a bare VM or dedicated host without
// one — there the per-worker subtree measurement stands alone. Reporting
// only: it is carried to Result for the metrics and the recommendation, not
// used to size anything (yet).
CgroupUsage budget.CgroupUsage
HasCgroupUsage bool
// ReserveBytes overrides the profile's reserve when non-zero.
ReserveBytes int64
// ReserveFraction overrides the profile's reserve fraction when non-zero, so an
// operator can set the target utilisation (e.g. 0.20 to keep 20% back, 80% for
// workers) without a fixed byte amount. Ignored when ReserveBytes is set.
ReserveFraction float64
StateOptions state.Options
AllocateOptions allocate.Options
// At is the round's clock, shared with LearnFrom.
//
// Build reads and refreshes the remembered concurrency peak, and it used to
// stamp that with its own time.Now() while the same round's learning used an
// explicit timestamp — two clocks in one round, in a package whose decay is
// measured in time. Zero means now.
At time.Time
}
Input is everything needed to produce a plan.
type ModeAdvice ¶
type ModeAdvice struct {
Pool string
From string // the mode the pool is in now
To string // the mode its measured shape points toward
Why string // one line, in plain terms, safe to print after the plan
}
ModeAdvice is a suggestion that a pool's process-manager mode (pm = static|dynamic|ondemand) may fit its workload better as something else.
It is advisory and nothing more. fpm-tune sizes pm.max_children inside whatever mode the operator chose; it never writes pm itself, because the right mode depends on latency targets and memory preferences this tool does not measure. The suggestion is here to start that conversation, not to end it.
type PoolDistribution ¶
type PoolDistribution struct {
Name string
P50, P95, P99 int64
// WorstSeen is the largest worker in the DISTRIBUTION, not PoolState's
// HighWaterBytes — that field is a sizing input, fed only by scrapes the
// sizing path accepts, so a pool could report a dozen readings and a
// largest-worker-ever of zero.
WorstSeen int64
Samples int64
// SubtreeHighWater is the largest a single worker's whole footprint — itself
// plus everything it spawned — was ever seen. ChildPerWorker is the child
// memory sizing adds to each worker: the high-water of a scrape's total child
// memory divided by its workers, so it already reflects how many workers ran a
// child at once. Both are zero until a child was observed.
SubtreeHighWater int64
ChildPerWorker int64
}
PoolDistribution is what one pool's workers have measured.
type Profile ¶
type Profile struct {
Name string
// WorkerBytes is the assumed cost of one worker. The default is sized for a
// PHP application with opcache enabled: the runtime plus per-request data,
// with the compiled code in shared memory rather than in each worker.
WorkerBytes int64
// ReserveFraction and ReserveFloorBytes decide how much of the host is held
// back from workers, for the OS, the web server and opcache's shared
// segment. The floor matters on small hosts, where a fraction of very little
// is not enough to run anything.
ReserveFraction float64
ReserveFloorBytes int64
}
Profile is the starting estimate for a pool with no history.
It is a guess, and it is labelled as one everywhere it surfaces. Its job is to produce a survivable configuration on the first run and then get out of the way — every pool that keeps running replaces it with measurement.
type Result ¶
type Result struct {
Plan allocate.Plan
Budget budget.Limits
// Reserve is what was held back from workers for the system, and why.
Reserve int64
ReserveReason string
// ChildReserve is the memory this plan committed to the processes pool
// workers spawn — the sum over pools of each pool's per-worker child cost
// times the workers it was given. It is not held back as a separate reserve;
// it is folded into each worker's cost, so the allocator's own budget
// invariant already covers it. Zero on a host whose pools spawn nothing.
// Reporting only.
ChildReserve int64
ChildReserveReason string
// Bootstrapped names the pools sized from a profile rather than from
// measurement, so the output can say which numbers are guesses.
Bootstrapped []string
// Unreachable names pools that could not be scraped. Their memory is left
// allocated to them: a pool that is merely restarting must not have its
// budget handed to its neighbours.
Unreachable []string
// Distribution is what each pool's workers were actually MEASURED at, as
// opposed to the single number the budget is divided by.
//
// Reporting only. It exists for the person deciding by hand — a pool whose
// median worker is 60MiB and whose p99 is 400MiB wants a different decision
// from one that sits flat at 90MiB, and the sizing number cannot tell them
// apart because it is not trying to.
Distribution []PoolDistribution
// Ambiguous names pool names that appear more than once in this round,
// because two masters each have one. Nothing keyed on the name alone can
// represent them — not the metrics labels, not the plan table — so both say
// so rather than showing whichever came last.
Ambiguous []string
// WorstCaseBytes is what this plan costs if every pool fills its ceiling
// with the most expensive worker ever seen from it. Advisory: sizing to it
// would pin the host to its worst minute.
WorstCaseBytes int64
// CgroupUsage is what the master's cgroup has actually used — every process
// in it, children included — and HasCgroupUsage says whether there was a
// cgroup to read. It is the ground truth the OOM killer enforces against, and
// the one number that catches a child a per-worker sample missed. Reporting
// only. Absent on a bare VM or dedicated host, where subtree RSS stands alone.
CgroupUsage budget.CgroupUsage
HasCgroupUsage bool
// Views is what was observed, kept so the rendered plan can show each pool's
// current setting beside the proposed one. A plan that shows only the new
// number gives an operator nothing to judge it against.
Views []observe.PoolView
// Advice is mode suggestions: pools whose measured shape points toward a
// different pm mode than the one they run. Advisory only — fpm-tune sizes
// within the chosen mode and never writes pm itself. Usually empty.
Advice []ModeAdvice
}
Result is a plan plus the reasoning that produced it.
func (Result) Render ¶
Render writes the plan for a human.
The rationale column is not decoration. This tool proposes changes to a running server's configuration, and an operator who cannot see why a pool is being cut from thirty workers to eight has no basis for letting it happen. Every number here is followed by the evidence for it.
type Workload ¶
type Workload struct {
Name string
// ChildBytes is the assumed peak resident memory of what ONE worker spawns.
// Zero for a pool that shells out to nothing, which is most web and API
// pools.
ChildBytes int64
// ConcurrentFraction is how many of a pool's workers are assumed to be
// running a child at once, as a fraction: all for subprocess-heavy, a quarter
// for bursty, none for web. ChildBytes times this fraction is the per-worker
// child cost the plan adds before it has measured anything — amortised, so a
// bursty pool is not sized as if every worker had a child.
ConcurrentFraction float64
}
A workload is the shape of what a pool's workers do — specifically, whether they spawn other processes, and how many at once.
It exists to solve the cold-start problem. A pool's own worker memory is measured within a few scrapes, but whether a request shells out to an ffmpeg is invisible until one happens to run while a scrape lands — and on a subprocess-heavy host, under-provisioning until then is exactly the window an OOM arrives in. Declaring the workload lets the plan account for children from the first run, before anything has been observed; measurement then refines it.
The class encodes a concurrency assumption that measurement is slow to recover: whether every worker might be transcoding at once (subprocess-heavy), or only a few now and then (bursty), or none (web).
func WorkloadByName ¶
WorkloadByName resolves a marker to its class, falling back to the supplied default for an empty or unrecognised name. ok reports whether the name was recognised, so a caller can warn about a typo rather than silently treating "medai" as the default — which, for a per-pool marker that reserves memory, is the difference between a warning and a silent OOM.