Load control & auto-pause
_load_control is BOA's per-host auto-pause safety net: a self-healing watchdog that
measures the system load average every ~5 seconds and, when load stays dangerously high,
throttles crawlers, pauses the web stack, and kills runaway processes — then restores
everything automatically once load drops. It lives in second.sh (deployed at
/var/xdrago/second.sh, source aegir/tools/system/second.sh) and is the load-shedding
counterpart to the security-facing Abuse Guard, which is a different
member of the same /var/xdrago/monitor/ machinery.
Its single design goal: a misbehaving site, a crawler storm, or a stuck Drush job should degrade gracefully — shed load and recover — rather than take the whole box down so hard that even SSH stalls.
Where it runs
second.sh is launched once per minute by the root crontab
(aegir/tools/system/cron/crontabs/root) but self-loops 10 times with a sleep 5
between passes, so the load test fires roughly every 5 seconds across the whole minute
rather than once a minute.
cron: * * * * * bash /var/xdrago/second.sh
│
▼
re-entrancy guard (lock.inc shared lock; legacy `pgrep -fc` fallback)
│
▼
for _iteration in 1..10: ← the 10× / 5 s loop
├─ _load_control ← EVERY pass (cheap load sampling)
├─ heavy fan-out ← only every Nth pass (see Cadence)
│ _proc_control + hackcheck/hackftp/escapecheck
└─ sleep 5
The re-entrancy guard is the shared _manage_single_lock pattern: it sources lock.inc and
takes the shared single-instance lock if present, otherwise falls back to a legacy
pgrep -fc count and exits if more than 2 copies of the script are already running (logged
to /var/log/boa/too.many.log).
_load_controlruns on EVERY pass of the loop, on every box, regardless of box class. The cadence throttle only reduces how often the heavy fan-out (_proc_control, the hack/escape scanners — see process guards) runs on small / idle / CI hosts. It never slows load sampling. Auto-pause stays fully responsive — a ~5 s reaction window — even on a throttledCIorSLOWbox.A standalone load-policy CLI,
loadguard, is installed at/opt/local/bin/loadguard(symlinked into/usr/local/bin) with defaults matching the fleet and the verbsenforce | ok-tasks | ok-spiders | run-tasks— but it is not wired into any shipped crontab. It is manual-only;second.shremains the live enforcement path. Do not treatloadguardas active enforcement.
The load metric
_get_load reads /proc/loadavg, takes the 1-minute and 5-minute load averages, and
normalises each to a per-CPU percentage:
_O_LOAD = (loadavg_1min / nproc) * 100 # 1-minute, per-CPU %
_F_LOAD = (loadavg_5min / nproc) * 100 # 5-minute, per-CPU %
Normalising by nproc is what makes one set of thresholds valid across a 2-core VM and a
64-core bare-metal host: a per-CPU load of 100% means the run queue equals the core count
regardless of how many cores there are. A box sitting at _O_LOAD=410% is running its
1-minute queue at ~4.1× its core count.
Both the 1-minute and 5-minute figures are tested. The 1-minute value reacts to a sudden burst; the 5-minute value catches a sustained grind that a fast burst-and-release would miss.
The four thresholds
Four per-CPU load ratios define the escalation ladder. Each is a ratio relative to a
single CPU; _load_control multiplies it by 100 to get the percentage threshold compared
against _O_LOAD / _F_LOAD.
| Ratio variable | Default | × 100 = threshold | Tier | Action |
|---|---|---|---|---|
_CPU_SPIDER_RATIO |
2.1 |
210% | SPIDER | Block crawlers (web stays up) |
_CPU_TASK_RATIO |
3.1 |
310% | TASK | Skip backend tasks (web stays up) |
_CPU_MAX_RATIO |
6.1 |
610% | MAX | Pause nginx + PHP-FPM |
_CPU_CRIT_RATIO |
8.1 |
810% | CRIT | Kill long procs, then pause web |
All four are overridable in /root/.barracuda.cnf. They are sanitised on load
(_sanitize_number strips anything but digits and a decimal point), so a malformed override
falls back to the built-in default rather than breaking the arithmetic.
The defaults are deliberately well above 100%. A BOA box is expected to run its cores
hot under normal traffic; auto-pause is a last resort for genuinely pathological load, not a
load balancer. The drastic MAX/CRIT tiers carry extra headroom because the linear
ratio × nproc model is right for CPU-runnable load but mis-scales I/O-wait load, which
does not grow with core count. On a 2-core box, SPIDER trips when the 1-minute load average
reaches ~4.2, MAX at ~12.2, CRIT at ~16.2 — by which point the box is effectively
unresponsive and shedding load is strictly better than letting it thrash.
The
TASKratio (3.1) is the threshold above which backend tasks are skipped.second.shdoes not read_CPU_TASK_RATIOat all — the variable lives in/root/.barracuda.cnfand is consumed by the queue scripts themselves (runner.sh,owl.sh,usage.sh,manage_solr_config.sh,purge_binlogs.sh— see process guards). The three tiers_load_controlitself acts on are SPIDER, MAX and CRIT.
The escalation ladder
_load_control is a single if/elif chain evaluated highest-tier first, so only one action
fires per pass. Crucially, every tier is re-verified after a sleep 9 cooldown before
it acts: a one-off spike that has already passed by the time the cooldown elapses is
ignored. Only sustained load triggers a response.
measure _O_LOAD (1m) and _F_LOAD (5m), per-CPU %
│
▼
CRIT — _O_LOAD or _F_LOAD > 810%?
│ yes ─► sleep 9, re-check ─► still high:
│ _terminate_processes (killall -9 php drush.php wget curl)
│ then _hold_services (stop nginx + php-fpm)
│ (kill/pause skipped if a backup is running AND load is
│ I/O-wait-bound — see below; the marker is still written)
│ touch /run/critical_load.pid
│ no
▼
MAX — _O_LOAD or _F_LOAD > 610%?
│ yes ─► sleep 9, re-check ─► still high:
│ _hold_services (stop nginx + php-fpm)
│ (pause skipped if a backup is running AND load is
│ I/O-wait-bound — see below; the marker is still written)
│ touch /run/max_load.pid
│ no
▼
SPIDER — _O_LOAD or _F_LOAD > 210% and ≤ 610%?
│ yes ─► sleep 9, re-check ─► still high:
│ _nginx_high_load_on (enable nginx_high_load.conf, block crawlers)
│ touch /run/spider_load.pid
│ _clear_pause_latch (resume hysteresis — see NORMAL)
│ no
▼
NORMAL — touch /run/normal_load.pid
_clear_pause_latch: drop /run/max_load.pid + /run/critical_load.pid only
once BOTH loads are below the resume threshold (MAX × _RESUME_FRACTION)
if spider protection on and both loads ≤ 210%: _nginx_high_load_off
SPIDER — block crawlers
When per-CPU load sits above 210% but at or below 610% (checked on both the 1-minute and
5-minute figures), _nginx_high_load_on renames /data/conf/nginx_high_load_off.conf to
/data/conf/nginx_high_load.conf and reloads nginx. That file is glob-included by
aegir/conf/nginx/nginx_compact_include.conf (include /data/conf/nginx_high_load.c*;), so
swapping the suffix toggles crawler blocking without rewriting any vhost. The web stack
stays fully up for real users; only spiders are shed. This is the gentlest acting tier and
the only one that does not set _skip_proc_control, so the heavy fan-out
(_proc_control and the hack/escape scanners) still runs on its normal cadence. The MAX and
CRIT tiers set _skip_proc_control=true, which suppresses only _proc_control for that pass
(the hack/escape scanners are unaffected).
MAX — pause the web stack
Above 610%, _hold_services stops the entire web tier: service nginx stop, then
force-quit on every installed php<NN>-fpm init script, then a belt-and-braces
killall php-fpm and killall nginx. The box stops serving requests entirely so the run
queue can drain. An ALERT-level incident is logged and (subject to policy below) emailed.
CRIT — terminate runaways, then pause
Above 810%, _terminate_processes runs first: killall -9 php drush.php wget curl.
This targets the usual culprits of a runaway — a stuck PHP request, a Drush job in a loop, a
wget/curl pulling something huge — before _hold_services pauses the web tier. Killing the
runaway first is what lets the box actually recover instead of immediately re-spiking after
the pause.
Backup exemption — MAX/CRIT skip only disk-bound backup load
Both the MAX and the CRIT branches consult two gates after the re-check:
_backup_in_progress and _load_is_iowait_bound. Only when a backup is running and
the load is measurably I/O-wait-bound do they log the high load and take no action — neither
pausing the web tier nor killing processes. A backup's high load is expected, largely
I/O-wait (D-state from dump/duplicity disk churn) and self-limiting, so pausing
nginx/PHP-FPM would cut service for no benefit on a disk-bound job.
_load_is_iowait_bound samples /proc/stat twice, 0.2 s apart, and requires the system
iowait share of the delta to reach at least _LOAD_IOWAIT_MIN% (default 10, sanitised by
_sanitize_number, overridable in /root/.barracuda.cnf). The consequences:
- A CPU-bound runaway coincident with a backup window still gets acted on. CPU-bound load has low iowait, so pause/kill fire normally — the exemption never shields a genuine runaway just because a backup happens to be running.
- Fails closed. Any
/procread error or degenerate delta yields 0% iowait — below the threshold — so the drastic tiers still act on a failed measurement. - The skip is logged to
/var/log/boa/high.load.incident.logwith the measured iowait%, e.g.Max load N% (1-minute) during a running backup, I/O-wait-bound (iowait X% >= 10%) - NOT pausing web.
The backup gate matches tightly: the BOA backup orchestrators by
path (backboa, duobackboa, multiback, mysql_backup.sh, mysql_cluster_backup.sh),
the dump engines mydumper/duplicity by exact name, or the /run/boa_sql_cluster_backup.pid
marker — so a genuine non-backup overload is never mistaken for a backup. (The SPIDER tier has
no such exemption; crawler shedding stays active during a backup.)
NORMAL — recovery, with resume hysteresis
When load is at or below the spider threshold on both the 1-minute and 5-minute figures,
the box is marked healthy and, if spider protection is currently on, _nginx_high_load_off
renames the file back and reloads. Recovery is automatic — there is no manual "un-pause"
step. (_hold_services stops services rather than disabling them, so the service
watchdogs bring nginx and PHP-FPM back; the spider config is the only piece _load_control
explicitly reverts.)
Web resume is latched, not instant. The service watchdogs restart web only when both
/run/max_load.pid and /run/critical_load.pid are absent, and _clear_pause_latch —
called from the SPIDER and NORMAL branches — removes those markers only once load has
fallen below the resume threshold = MAX threshold × _RESUME_FRACTION (default 0.8,
so 488% with the default 6.1 MAX ratio) on both the 1-minute and 5-minute averages.
Pause trips when either average exceeds a threshold; resume needs both below the lower
bar. That asymmetry is a deliberate hysteresis gap: without it a box hovering at the MAX
threshold would flap pause/resume. The per-pass report header prints the computed
Web Resume Threshold alongside the three tier thresholds.
_RESUME_FRACTION is sanitised and clamped to the open interval (0,1) — anything outside
resets to 0.8, so a bad override can neither pin the latch closed (a fraction of 0 would
keep web paused forever) nor disable the hysteresis (≥ 1). The latch also fails toward
resume: an empty or unusable threshold clears the markers rather than leaving web stuck
paused.
State files in /run
Each pass writes its tier marker under /run and clears the stale ones, so the current
load posture is readable from the marker(s) present. Several service watchdogs read these
to decide whether to stand down.
| State file | Meaning |
|---|---|
/run/normal_load.pid |
Load within normal parameters |
/run/spider_load.pid |
Spider protection active (crawlers blocked) |
/run/max_load.pid |
Web stack paused (MAX); held by resume hysteresis |
/run/critical_load.pid |
Critical: processes killed + web paused; held by resume hysteresis |
/run/boa_second_auto_healing.pid |
A pause/heal action is in progress |
The two pause markers are the hysteresis latch: a SPIDER or NORMAL pass removes
max_load.pid / critical_load.pid only once both load averages are below the resume
threshold (see NORMAL — recovery), so during the
recovery window a spider or normal marker can coexist with a still-held pause marker.
boa_second_auto_healing.pid is a short-lived guard set by _hold_services for the
duration of a pause and removed when it finishes. While it exists, the MAX/CRIT branches skip
re-issuing _hold_services, so two overlapping passes can't stack service stops on top of
each other.
To read the live posture:
ls -1 /run/*_load.pid 2>/dev/null
tail -n 20 /var/log/boa/high.load.incident.log
Process priority — _B_NICE
second.sh renices itself (and the _proc_control pass) to _B_NICE. The value is
sanitised to an integer and clamped to the valid nice range -20..19; anything
out-of-range or non-numeric falls back to 0. Set _B_NICE in /root/.barracuda.cnf to
bias the whole monitor pass softer or harder against site traffic. The clamp matters because
the renice runs unconditionally on a script that fans out helpers — an unbounded value would
either be rejected by the kernel or starve the very watchdog meant to be relieving load.
Incident reporting
When a pause or kill fires, _incident_email_report appends to the incident log and may
email an alert. Two gates control whether mail is actually sent.
Policy gate — _INCIDENT_REPORT. Normalised to upper-case and matched against three
current levels, with legacy spellings mapped in:
| Value | Behaviour |
|---|---|
OFF |
Total silence — never email |
CRIT |
Email only when the event level is ALERT (the effective default) |
ALL |
Email every event — very noisy, debugging only |
Legacy values are remapped: NO → OFF, YES → CRIT, MINI → CRIT. Anything
unrecognised also falls back to CRIT, so a typo fails safe to "critical alerts only"
rather than to silence or to noise.
What "default" means here.
second.sh's in-code default for_INCIDENT_REPORTisCRIT. The shipped/root/.barracuda.cnfdoes not leave it unset — it sets_INCIDENT_REPORT=MINI, andMINInormalises toCRITforsecond.sh. Either way the effective behaviour out of the box isCRIT: you receive onlyALERT-level mail from the load-control path. (The sameMINIsetting means something stricter on the per-service watchdogs — see auto-healing — so do not assume one_INCIDENT_REPORTvalue behaves identically across every monitor.)
This CRIT ladder is specific to second.sh. The per-service auto-healing watchdogs read
the same _INCIDENT_REPORT variable but apply it with different per-script defaults and a
stricter send gate (most mail only on ALL); see
Service auto-healing watchdogs. The two tables describe different
scripts, not conflicting rules.
Source note. The current
casemaps bothYESandMINItoCRIT. The explanatory comment block just above it still describes the older intent (YES→MINI, withMINIas a distinct "most important alerts" level); there is no separateMINIruntime level any more — the code collapses it intoCRIT. Trust thecase, not the comment.
The pause/kill actions call _incident_email_report at level ALERT, so they are delivered
under the default CRIT policy. The spider on/off events are coded at INFO and their email
calls are commented out — they are logged but never mailed, to avoid alert spam from routine
crawler shedding.
Uptime grace gate — _check_uptime_grace_period. Sourced from lock.inc, this is the
very first thing _incident_email_report checks. It returns failure (suppressing the email
and forcing _INCIDENT_REPORT=OFF) when any of these hold:
- system uptime is under 15 minutes;
- the Hostmaster alias
/var/aegir/.drush/hm.alias.drushrc.phpis missing, orcsf/lfdaren't up yet; - an install / upgrade is mid-flight (
/run/octopus_install_run.pid,/run/boa_run.pid,/run/boa_wait.pid).
This is what stops a fresh boot or an in-progress BOA upgrade — both of which legitimately spike load — from flooding the mailbox with auto-pause alerts before the box has settled.
Destination — _MY_EMAIL (default notify@omega8.cc). Mail goes there via s-nail; if
_MY_EMAIL is empty, _incident_email_report returns without sending. The body is the
last 200 lines of the incident log at /var/log/boa/high.load.incident.log, so the
current incident is not buried under history; the full log stays on disk and rotates weekly
(see auto-healing). The log records every action (Web Server Paused,
PHP/Wget/cURL terminated, spider protection enabled/disabled) with a timestamp and the
offending load percentage regardless of whether mail was sent.
Tuning summary
| Variable | Default | Effect |
|---|---|---|
_CPU_SPIDER_RATIO |
2.1 |
Per-CPU ratio to start blocking crawlers (×100 = %) |
_CPU_TASK_RATIO |
3.1 |
Per-CPU ratio above which backend tasks are skipped (read by the queue scripts, not second.sh) |
_CPU_MAX_RATIO |
6.1 |
Per-CPU ratio to pause nginx + PHP-FPM |
_CPU_CRIT_RATIO |
8.1 |
Per-CPU ratio to kill long procs then pause web |
_LOAD_IOWAIT_MIN |
10 |
Minimum system iowait% for the backup pause-skip to treat high load as disk-bound |
_RESUME_FRACTION |
0.8 |
Web-resume hysteresis: pause latch clears below MAX × this on both averages (clamped to (0,1)) |
_B_NICE |
0 |
Renice for the monitor pass (clamped -20..19) |
_INCIDENT_REPORT |
CRIT (shipped as MINI → CRIT) |
Email policy for second.sh: OFF / CRIT / ALL |
_MY_EMAIL |
notify@omega8.cc |
Incident-alert recipient; empty = no mail |
All live in /root/.barracuda.cnf. Raise the ratios on a box that is expected to run hot;
lower _CPU_SPIDER_RATIO on a box where crawler load is the main threat. Do not lower the
ratios near or below 100% (1.0) — the box would auto-pause under ordinary traffic.
Related
- Cron cadence & idle-load throttle — the 10×/5 s loop, box
classification (
CI/SLOW/NORMAL), and the heavy fan-out throttle that load sampling is deliberately exempt from. - Process guards & auth scanners —
_proc_controland the task-queue runner gated by_skip_proc_controland_CPU_TASK_RATIO. - Service auto-healing watchdogs — the watchdogs that read the
/run/*_load.pidmarkers this page writes, and the per-service_INCIDENT_REPORTgate. - Abuse Guard — the security-facing monitor in the same
/var/xdrago/monitor/family; it sheds malicious request load where_load_controlsheds aggregate system load. - Reference appendix — every override variable named above.