Documentation
¶
Overview ¶
Package phpfpm is a Go library for talking to PHP-FPM.
It covers the four things a caller needs in order to reason about a running PHP-FPM installation: parsing the effective configuration, discovering running masters and their pools, scraping live status over FastCGI, and controlling the master (validate, reload).
It exists as its own module because two consumers need it and neither should depend on the other: fpm-exporter reads, and fpm-tune both reads and writes. The code began life inside fpm-exporter's internal/phpfpm, where it could not be imported.
Two conventions run through the package:
Nothing here owns a logger. Entry points that need one take a *slog.Logger. A library with a package-level logger cannot be embedded twice with different destinations, and it makes tests order-dependent.
Reload, never restart. Reload sends SIGUSR2, which has the master re-read its configuration and cycle workers gracefully. Callers changing pm.* settings should pair it with Validate first — an invalid drop-in that reaches a reload takes the pool down.
Index ¶
- Constants
- Variables
- func InvalidateConfigCache(binary, configPath string)
- func MasterPID(pidFile string) (int, error)
- func ParseAddress(addr string, path string) (scheme, address, scriptPath string, err error)
- func Reload(pid int) error
- func ReloadAndWait(ctx context.Context, target ReloadTarget, settle time.Duration, ...) (int, error)
- func ReloadMaster(pid int, configPath string) error
- func RemoveOpcacheScript()
- func Validate(ctx context.Context, binary, configPath string) error
- func VerifyMaster(pid int) error
- type Discovered
- type EffectiveConfig
- type Info
- type Master
- type Memory
- type OpcacheStatus
- type Pool
- type PoolOutcome
- type PoolProcess
- type ReloadTarget
- type Result
- type Stats
- type Target
- type Unstatused
Constants ¶
const DefaultParseTimeout = 30 * time.Second
DefaultParseTimeout bounds ParseConfig when the caller gives no context.
Generous: `php-fpm -tt` on a host with many pools is not instant, and killing a slow-but-working parse is worse than waiting for it. The point is that there IS a bound.
const DefaultTimeout = 3 * time.Second
DefaultTimeout bounds a FastCGI dial when a Target does not set one.
Variables ¶
var ErrNotAMaster = errors.New("not a php-fpm master process")
ErrNotAMaster reports that a pid is not a php-fpm master process.
var ErrPathMissing = errors.New("path does not exist")
trustedPath reports whether a discovered path is safe to execute or read. ErrPathMissing reports a path that is not there at all, as opposed to one that is there and fails the trust checks.
Functions ¶
func InvalidateConfigCache ¶ added in v0.3.0
func InvalidateConfigCache(binary, configPath string)
InvalidateConfigCache forgets the parsed configuration.
Call it after reloading php-fpm with a changed configuration. Without it, a long-running process keeps reporting the settings it saw at startup: a tool that writes pool settings would never observe its own changes, and would show an operator a "currently configured" value that has not been true for hours.
Passing a binary and config path forgets just that pair; passing empty strings forgets everything.
func MasterPID ¶
MasterPID reads a php-fpm master's pid from its pid file.
Discover finds masters by scanning the process table, which is the right tool when you do not know what is running. When you already know which pool you are reloading, the pid file is the authoritative answer and does not require permission to read another user's /proc entry.
func ParseAddress ¶
func Reload ¶
Reload asks a running master to re-read its configuration.
SIGUSR2 is a graceful reload: the master re-reads the config and cycles its workers, letting each finish the request it is serving. It is deliberately not a restart — a restart drops in-flight requests, and on a host serving many sites that is a visible outage for a change that was meant to be routine.
Callers changing pool settings should Validate first. Reload does not check the configuration it is asking the master to adopt.
func ReloadAndWait ¶
func ReloadAndWait(ctx context.Context, target ReloadTarget, settle time.Duration, log *slog.Logger) (int, error)
ReloadAndWait reloads a master and waits for it to come back, returning the pid it came back as.
A reload that kills the master is the failure this exists to detect: Reload returns as soon as the signal is delivered, which is well before the master has re-read anything.
The returned pid may differ from the one signalled, and that is the whole subtlety. SIGUSR2 makes the master re-exec itself. When php-fpm runs in the foreground — under systemd, or as pid 1 in a container — the pid survives. When it runs DAEMONIZED, which is php-fpm's own default, the re-exec produces a new process and the original exits.
Watching the original pid therefore reported a perfectly successful reload as a dead master. Observed on a stock homebrew php-fpm: the log said "using inherited socket" and "ready to handle connections" under a new pid while the caller rolled the change back and told the operator its master had died.
So the confirmation is about the MASTER, not the number: the original pid surviving is one way to see it, and a successor that owns the same config is the other.
func ReloadMaster ¶ added in v0.8.1
ReloadMaster is Reload for a caller that knows which master it means.
VerifyMaster on its own only establishes that the pid is *a* php-fpm master. That is enough on a host running one, and not enough on a host running several: a master can exit between discovery and the reload, and a pid handed straight back to a different master would be signalled as though it were ours. The config path is in the process title, so checking it costs nothing and makes the answer specific.
func RemoveOpcacheScript ¶
func RemoveOpcacheScript()
RemoveOpcacheScript deletes the generated status script. Safe to call when no script was ever written, and safe to call while scrapes are in flight — the next probe recreates it.
func Validate ¶
Validate checks a configuration with `php-fpm -t` without applying it.
This is the guard that makes writing pool configuration survivable. PHP-FPM re-reads its configuration on reload, and a syntax error or an impossible value does not fail gracefully — the master refuses to come back and every pool it served goes down. Validating first turns that outage into a returned error.
The configPath is the master config, not the drop-in: `-t` walks the include tree, so a broken fragment is caught through its parent.
func VerifyMaster ¶ added in v0.5.0
VerifyMaster confirms that a pid really is a php-fpm master, immediately before it is signalled.
This replaced a `pid <= 1` refusal that was wrong in both directions.
It refused the most common deployment there is. In the official php:8.3-fpm image the master IS pid 1, so `fpm-tune apply` wrote the configuration, declined to reload, reported the master as dead and rolled the whole change back — verified against that image, where every apply failed this way.
And it did not guard against the thing that actually matters. A pid is only a promise about the instant it was read: between discovery and the reload the master can exit and the kernel can hand the number to something else. SIGUSR2 to a process that did not ask for it terminates it by default, so the old check would happily kill an unrelated program while carefully declining to signal init. Container pid namespaces start at 1 and stay small, which is exactly where recycling is likely.
Checking what the process IS covers both, and costs one /proc read.
Types ¶
type Discovered ¶
type Discovered struct {
Name string
ConfigPath string
StatusPath string
Binary string
Socket string
StatusSocket string
// MaxChildren and ProcessManager are the pool's CONFIGURED settings, read
// from the effective configuration during discovery.
//
// Carried because a caller that cannot reach a pool still needs them. A pool
// whose socket refuses — restarting, or briefly overloaded — is not a pool
// that has stopped occupying memory, and a consumer with no idea how large
// it is will hand its allocation to a neighbour and overcommit the host the
// moment it comes back. Discovery has already parsed this; throwing it away
// only to be unable to recover it later is the expensive kind of tidy.
MaxChildren int
ProcessManager string
// Workload is the value of env[FPM_TUNE_WORKLOAD] in the pool's config, when
// it set one — a free-form hint a consumer that sizes pools can read to know
// what the pool does before it has measured it. Empty when unset. This
// package does not interpret it; it only carries what the config declared,
// because the config is the one place a per-pool hint can live and be
// discovered without a second file.
Workload string
// PID is the master process serving this pool.
//
// Carried from the process scan because the pid file is not a reliable
// alternative: the official php:8.3-fpm image ships `pid` commented out, so
// there is no file to read — and a caller that could not identify the master
// would write pool configuration and never reload it.
PID int
}
Discovered is one pool found by scanning the process table.
func Discover ¶
func Discover(log *slog.Logger) ([]Discovered, error)
Discover scans the process table for PHP-FPM masters and returns the pools they serve.
It parses each master's effective configuration, which means executing the discovered binary — see trustedPath for why that is gated on ownership.
log may be nil.
func DiscoverContext ¶ added in v0.10.0
DiscoverContext is Discover with a caller-supplied deadline. It forks php-fpm once per master, so a caller in a scrape loop should bound it.
type EffectiveConfig ¶
func ParseConfig ¶
func ParseConfig(FPMBinaryPath string, FPMConfigPath string) (*EffectiveConfig, error)
ParseConfig runs `php-fpm -tt` and parses its report of the effective configuration. Results are cached per binary+config pair.
The cache never expires on its own, which is right for the common case — the parse forks php-fpm and a scrape loop would otherwise do it every few seconds — but it means a caller that CHANGES the configuration has to say so. See InvalidateConfigCache.
func ParseConfigContext ¶ added in v0.10.0
func ParseConfigContext(ctx context.Context, FPMBinaryPath string, FPMConfigPath string) (*EffectiveConfig, error)
ParseConfigContext is ParseConfig with a caller-supplied deadline.
type Master ¶ added in v0.7.0
Master is a running php-fpm master, identified WITHOUT reading its configuration.
Discover parses each master's effective config, which is what makes it useful — and what makes it useless in the one situation a caller most needs an answer. A master whose config file no longer parses is skipped entirely, so a tool trying to repair exactly that config cannot find the master to repair it for. Observed: a rejected pool fragment left on disk by a run that died, a healthy master still serving from the configuration it loaded before the file appeared, and `fpm-tune apply` reporting "no PHP-FPM pools found" while the fragment sat there waiting for any reload to adopt it.
This carries only what the process table itself provides, so it answers even when the configuration does not.
func DiscoverMasters ¶ added in v0.7.0
DiscoverMasters scans the process table for php-fpm masters.
The binary and config path still go through the trust checks — they are about to be handed to exec — but nothing is executed here and nothing is parsed.
log may be nil.
type OpcacheStatus ¶
type OpcacheStatus struct {
Enabled bool `json:"opcache_enabled"`
MemoryUsage Memory `json:"memory_usage"`
Statistics Stats `json:"opcache_statistics"`
}
func GetOpcacheStatus ¶
func GetOpcacheStatus(ctx context.Context, target Target) (*OpcacheStatus, error)
type Pool ¶
type Pool struct {
Address string `json:"address"`
Path string `json:"path"`
Name string `json:"pool"`
ProcessManager string `json:"process manager"`
StartTime int64 `json:"start time"`
StartSince int64 `json:"start since"`
AcceptedConnections int64 `json:"accepted conn"`
ListenQueue int64 `json:"listen queue"`
MaxListenQueue int64 `json:"max listen queue"`
ListenQueueLength int64 `json:"listen queue len"`
IdleProcesses int64 `json:"idle processes"`
ActiveProcesses int64 `json:"active processes"`
TotalProcesses int64 `json:"total processes"`
MaxActiveProcesses int64 `json:"max active processes"`
MaxChildrenReached int64 `json:"max children reached"`
SlowRequests int64 `json:"slow requests"`
MemoryPeak int64 `json:"memory peak"`
Processes []PoolProcess `json:"processes"`
ProcessesCpu *float64 `json:"processes_cpu"`
ProcessesMemory *float64 `json:"processes_memory"`
Config map[string]string `json:"config,omitempty"`
OpcacheStatus OpcacheStatus `json:"opcache_status,omitempty"`
PhpInfo Info `json:"php_info,omitempty"`
}
type PoolOutcome ¶
type PoolOutcome struct {
// Name is the pool's configured or discovered name, used to label a
// failure. A successful scrape prefers the name PHP-FPM itself reports.
Name string
Socket string
Result *Result
Err error
}
PoolOutcome is what happened to one configured pool during a scrape: either a Result, or the error that prevented one. Failures are values rather than log lines so the collector can emit up=0 for a pool that did not answer -- a pool that silently vanishes from the output is indistinguishable from a pool that was removed from the configuration.
func Scrape ¶
Scrape reads one pool's status page. Its client and response body close at the end of this call rather than at the end of a whole batch.
It takes its own snapshot of the process table to measure worker subtrees; ScrapeAll shares one across pools instead. See scrape.
log may be nil.
func ScrapeAll ¶
ScrapeAll scrapes every target. It returns one outcome per target, in the order given, and an error only when nothing at all could be collected.
It took fpm-exporter's whole *config.Config before this package was extracted. A library takes the work it is asked to do; deciding which pools exist belongs to the caller.
log may be nil.
type PoolProcess ¶
type PoolProcess struct {
PID int `json:"pid"`
State string `json:"state"`
StartTime int64 `json:"start time"`
StartSince int64 `json:"start since"`
Requests int64 `json:"requests"`
RequestDuration int64 `json:"request duration"`
RequestMethod string `json:"request method"`
RequestURI string `json:"request uri"`
ContentLength int64 `json:"content length"`
User string `json:"user"`
Script string `json:"script"`
LastRequestCPU float64 `json:"last request cpu"`
LastRequestMemory float64 `json:"last request memory"`
// CurrentRSS is the worker's resident memory, in bytes.
//
// Not part of PHP-FPM's status output — it is filled in from the operating
// system using PID, because the size of a worker is the number any capacity
// decision turns on and the status page does not carry it. Zero means the
// worker could not be read, usually because it exited between the status
// response and the lookup.
CurrentRSS int64 `json:"current_rss"`
// CurrentPSS is the worker's proportional set size, in bytes: like CurrentRSS,
// but every shared page (the opcache SHM segment, shared libraries, the pages
// still copy-on-write from the master) is divided by the number of processes
// mapping it rather than charged in full to each. Summing PSS across a pool's
// workers yields the memory those workers actually cost the host; summing RSS
// double-counts everything shared. Read from /proc/<pid>/smaps_rollup.
//
// Zero means it could not be read: a kernel without smaps_rollup (pre-4.14), a
// permission short of PTRACE_MODE_READ, or the worker having exited. A consumer
// sizing a pool should prefer CurrentPSS when it is non-zero and fall back to
// CurrentRSS otherwise.
CurrentPSS int64 `json:"current_pss"`
// SubtreeRSS is the resident memory of the worker AND every process it
// spawned — the ffmpeg or imagemagick a request shelled out to, each a
// separate pid the status page and CurrentRSS both miss. It is always at
// least CurrentRSS; the difference is what the children cost. Zero means it
// was not measured (no process snapshot this scrape), which is distinct from
// a measured subtree that equals CurrentRSS because nothing was spawned.
//
// Point-in-time: a child that lived and died between two scrapes is not in
// it, and one whose worker exited first has reparented away from the subtree.
// It is the per-worker view; a cgroup's own high-water mark, where there is a
// cgroup, catches the transients this cannot.
SubtreeRSS int64 `json:"subtree_rss"`
}
type ReloadTarget ¶ added in v0.6.0
type ReloadTarget struct {
// PID is the master to signal.
PID int
// PIDFile and ConfigPath are how the master is found again if it comes back
// under a different pid. Either is sufficient; both are better.
PIDFile string
ConfigPath string
}
ReloadTarget identifies a master well enough to recognise it after a reload.
The pid alone is not enough, because a reload does not always preserve it.
type Stats ¶
type Stats struct {
NumCachedScripts uint64 `json:"num_cached_scripts"`
Hits uint64 `json:"hits"`
Misses uint64 `json:"misses"`
BlacklistMisses uint64 `json:"blacklist_misses"`
OomRestarts uint64 `json:"oom_restarts"`
HashRestarts uint64 `json:"hash_restarts"`
ManualRestarts uint64 `json:"manual_restarts"`
HitRate float64 `json:"opcache_hit_rate"`
}
type Target ¶
type Target struct {
// Name identifies the pool in results when the pool itself could not be
// reached. A successful scrape uses the name PHP-FPM reports; this is the
// fallback so a failing pool is still labelled with something meaningful.
Name string `mapstructure:"name"`
// Socket is the pool's FastCGI address ("unix:///run/php-fpm.sock",
// "tcp://127.0.0.1:9000", or a bare path).
Socket string `mapstructure:"socket"`
// StatusSocket is where the status page is served, when it differs from
// Socket. Empty means Socket.
StatusSocket string `mapstructure:"status_socket"`
StatusPath string `mapstructure:"status_path"`
// ConfigPath and Binary let ParseConfig recover the effective configuration
// for this pool. Both come from the master's command line during Discover.
ConfigPath string `mapstructure:"config_path"`
Binary string `mapstructure:"binary"`
// MaxChildren and ProcessManager are the pool's CONFIGURED settings as
// discovery read them.
//
// They matter most when the pool cannot be reached. A pool whose socket
// refuses is not a pool that has stopped occupying memory, and a caller with
// no idea how large it is will hand its allocation away and overcommit the
// host the moment it comes back.
MaxChildren int `mapstructure:"-"`
ProcessManager string `mapstructure:"-"`
// Workload is env[FPM_TUNE_WORKLOAD] from the pool's config, when it set one.
// Carried, not interpreted — a consumer that sizes pools reads it as a hint
// about what the pool does. Empty when unset.
Workload string `mapstructure:"workload"`
// PID is the master serving this pool, when it is known. Zero means unknown,
// not "no master".
PID int `mapstructure:"-"`
// Timeout bounds the FastCGI dial for this pool. Zero means DefaultTimeout.
Timeout time.Duration `mapstructure:"timeout"`
}
Target describes how to reach one PHP-FPM pool.
It replaces the FPMPoolConfig this package used to take from fpm-exporter's internal config. A library cannot depend on one application's configuration type — the second consumer has a different one, and neither should have to adopt the other's.
The mapstructure tags are kept so a viper-based caller can decode straight into this type rather than maintaining a parallel struct and a copy function.
func TargetFromDiscovered ¶
func TargetFromDiscovered(d Discovered) Target
TargetFromDiscovered builds a scrape target from a discovered pool.
type Unstatused ¶ added in v1.1.0
type Unstatused struct {
Name string
ConfigPath string // the master config the pool is defined under
Binary string // the master's php-fpm binary
PID int // the master serving it
Socket string // the pool's listen socket, where the status page would be served
}
Unstatused is a pool that exists but exposes no pm.status_path, so it cannot be scraped for the live metrics the normal results carry — and is left out of them for exactly that reason.
It is surfaced separately rather than dropped silently: a bare php-fpm ships its default `www` pool with pm.status_path commented out, so a tool that needs the status page finds nothing on a stock install and reports it as "no pools", which sends the operator looking for a master that is running the whole time. Carrying the pool lets a caller say what is actually wrong, and — if it manages the host — turn the status page on rather than asking the operator to hand-edit a file.
func UnstatusedPools ¶ added in v1.1.0
UnstatusedPools returns the pools found on the host that have no pm.status_path, so cannot be scraped and are left out of DiscoverContext's results.
It runs the same scan DiscoverContext does — one fork of php-fpm per master — so it is for the two places that need it rather than a hot loop: reporting honestly why nothing was found, and enabling the status page for the pools that lack it.