From 03cca745f701bb9c23b848bd9738b23b06896e17 Mon Sep 17 00:00:00 2001 From: jknapp Date: Tue, 2 Jun 2026 16:36:25 -0700 Subject: [PATCH] feat(litespeed): wire up dynamic LSAPI tuning + idle reduction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two correctness fixes and a tuning improvement. CORRECTNESS: 1. Strip the stock 'extProcessor lsphp' from httpd_config.conf before appending ours. Previously the stock block (hard-coded PHP_LSAPI_CHILDREN=10 regardless of container memory) always won because our APPEND fragment didn't include an extProcessor block. detect-memory-litespeed.sh was computing LSAPI_CHILDREN but never plumbing it anywhere — silent dead code. 2. Bump LSPHP_WORKER_ESTIMATE_MB from 96 → 115 per the 2026-06-02 memory-sizing finding (vantagehealth OOM-spawn loop). Each lsphp carries ~115 MB shmem-rss accounted per worker. 115 MB matches the real per-worker baseline. TUNING (idle reduction, the original ask): - LSAPI_MAX_IDLE_CHILDREN=2 (was CHILDREN/2 = 5 default) - LSAPI_MAX_IDLE=60s (was 300s default) - PHP_LSAPI_MAX_REQUESTS=500 (recycle workers, prevents bloat) - memSoftLimit=1024M / memHardLimit=1500M per worker (RLIMIT_AS; catches runaway scripts at the worker level, cgroup still backstops the container) Effective LSAPI_CHILDREN per container: 2 GiB → ~17 (was 10 — brain-jar was saturating) 1 GiB → ~8 512 MiB → ~3 (cap-marginal per the memory note; bump container if site grows) Dropped LSAPI_MEM_SOFT/HARD computation in detect-memory: AVAILABLE/CHILDREN was conflating VSZ with RSS-budget arithmetic and would have killed legitimate workers. The 1024/1500 hard-coded values in the template comfortably fit typical Divi/WooCommerce VSZ (280-365 MB). Co-Authored-By: Claude Opus 4.7 (1M context) --- configs/litespeed/httpd_config.tpl | 50 +++++++++++++++++++++++++++++- scripts/create-vhost-litespeed.sh | 7 +++-- scripts/detect-memory-litespeed.sh | 29 ++++++++++------- scripts/entrypoint-litespeed.sh | 2 +- 4 files changed, 72 insertions(+), 16 deletions(-) diff --git a/configs/litespeed/httpd_config.tpl b/configs/litespeed/httpd_config.tpl index 9e1a3d4..0e3d050 100644 --- a/configs/litespeed/httpd_config.tpl +++ b/configs/litespeed/httpd_config.tpl @@ -7,7 +7,8 @@ ## append pattern too (see setup_docker.sh in litespeedtech/ols-dockerfiles). ## ## Rendered at container start by scripts/create-vhost-litespeed.sh via -## envsubst. Templated vars: $user $domain $vhost_map_aliases. +## envsubst. Templated vars: $user $domain $vhost_map_aliases $PHPVER +## $LSAPI_CHILDREN (computed by detect-memory-litespeed.sh) ## --- our listeners (replace stock Default :8088) --- listener HTTP { @@ -30,6 +31,53 @@ listener HTTPS { map siteVH * } +## --- lsphp extProcessor (overrides the stock one which is hard-coded to +## PHP_LSAPI_CHILDREN=10 regardless of container memory). +## +## Sized dynamically by detect-memory-litespeed.sh based on the cgroup cap: +## 2 GiB container → LSAPI_CHILDREN ≈ 17 (was stuck at 10) +## 1 GiB container → LSAPI_CHILDREN ≈ 8 +## 512 MiB → LSAPI_CHILDREN ≈ 3 +## +## Idle-reduction knobs (the question that motivated this whole block): +## LSAPI_MAX_IDLE_CHILDREN=2 default was CHILDREN/2 (so 10/2=5) +## LSAPI_MAX_IDLE=60 default was 300 (5 min) +## Together: max 2 idle workers kept alive, anything idle >60s gets reaped. +## Trade-off: cold-start of an extra worker after idle reaping costs ~50-100ms +## on the first request to it. Worth it for shadowdao-sized low-traffic sites +## where the difference is "30 MB idle" vs "200 MB idle". +## +## memSoftLimit/memHardLimit: per-worker RLIMIT_AS catches a runaway PHP +## script before it hogs the whole pool's memory. Cgroup is still the host +## backstop (one-customer-per-container), but the per-worker cap protects +## the OTHER workers in the same pool from a bad-actor script. 1024M soft +## comfortably accommodates typical Divi/WooCommerce VSZ (~280-365 MB). +extProcessor lsphp { + type lsapi + address uds://tmp/lshttpd/lsphp.sock + maxConns ${LSAPI_CHILDREN} + env PHP_LSAPI_CHILDREN=${LSAPI_CHILDREN} + env LSAPI_MAX_IDLE_CHILDREN=2 + env LSAPI_MAX_IDLE=60 + env PHP_LSAPI_MAX_REQUESTS=500 + env LSAPI_AVOID_FORK=200M + initTimeout 60 + retryTimeout 0 + persistConn 1 + pcKeepAliveTimeout 30 + respBuffer 0 + autoStart 1 + path /usr/local/lsws/lsphp${PHPVER}/bin/lsphp + backlog 100 + instances 1 + runOnStartUp 1 + priority 0 + memSoftLimit 1024M + memHardLimit 1500M + procSoftLimit 400 + procHardLimit 500 +} + ## --- our vhost via vhTemplate (upstream's working pattern) --- ## The template file is /usr/local/lsws/conf/templates/site.conf — written ## by create-vhost-litespeed.sh at the same time as this fragment. diff --git a/scripts/create-vhost-litespeed.sh b/scripts/create-vhost-litespeed.sh index e67c023..1e25495 100644 --- a/scripts/create-vhost-litespeed.sh +++ b/scripts/create-vhost-litespeed.sh @@ -45,9 +45,12 @@ cp /usr/local/lsws/.conf/httpd_config.conf "$LSWS_CONF/httpd_config.conf" ## Strip the stock blocks we replace. Use awk: easier than sed range-deletes ## to skip a NAMED block of arbitrary length terminated by a top-level `}`. +## extProcessor lsphp is stripped because the stock one hard-codes +## PHP_LSAPI_CHILDREN=10 regardless of container size — our appended +## extProcessor scales it from detect-memory-litespeed.sh. awk ' BEGIN { skip = 0 } - /^listener HTTP \{/ || /^listener HTTPS \{/ || /^vhTemplate docker \{/ { skip = 1; next } + /^listener HTTP \{/ || /^listener HTTPS \{/ || /^vhTemplate docker \{/ || /^extProcessor lsphp\{/ || /^extProcessor lsphp \{/ { skip = 1; next } skip && /^\}/ { skip = 0; next } !skip { print } ' "$LSWS_CONF/httpd_config.conf" > "$LSWS_CONF/httpd_config.conf.new" @@ -58,7 +61,7 @@ SENTINEL="## ---- cac-litespeed append (do not edit below) ----" { echo "" echo "$SENTINEL" - envsubst '${user} ${domain} ${vhost_map_aliases}' < "$TPL_DIR/httpd_config.tpl" + envsubst '${user} ${domain} ${vhost_map_aliases} ${PHPVER} ${LSAPI_CHILDREN}' < "$TPL_DIR/httpd_config.tpl" } >> "$LSWS_CONF/httpd_config.conf" ## --- write our vhost template to /usr/local/lsws/conf/templates/site.conf --- diff --git a/scripts/detect-memory-litespeed.sh b/scripts/detect-memory-litespeed.sh index 3283eb9..f59d5b8 100644 --- a/scripts/detect-memory-litespeed.sh +++ b/scripts/detect-memory-litespeed.sh @@ -47,9 +47,16 @@ if [ "$AVAILABLE_MB" -lt 60 ]; then fi ## ---- LSAPI children (analogous to PHP_FPM_MAX_CHILDREN) ---- -## LSAPI is more memory-efficient than FPM; estimate 96 MB / worker -## (vs 128 MB for FPM after the 2026-06-01 bump). Floor 2, cap 50. -LSPHP_WORKER_ESTIMATE_MB=${LSPHP_WORKER_ESTIMATE_MB:-96} +## Per the 2026-06-02 cac-litespeed memory-sizing finding (vantagehealth +## OOM-spawn loop at 512 MB cap): each lsphp worker carries ~115 MB +## shmem-rss which is RSS-accounted per worker (vs cac-fpm's COW-shared +## fork model). Real-world worker budget ≈ 115 MB shmem + ~50-100 MB anon +## that scales with workload. 115 MB is the safe per-worker estimate for +## the divisor; floor at 2, cap at 50. +## +## Sub-512 MB containers are unsafe for dynamic WP on OLS per the memory +## note — the floor of 2 workers still applies but it'll be cap-marginal. +LSPHP_WORKER_ESTIMATE_MB=${LSPHP_WORKER_ESTIMATE_MB:-115} calc_lsapi_children=$((AVAILABLE_MB / LSPHP_WORKER_ESTIMATE_MB)) if [ "$calc_lsapi_children" -lt 2 ]; then @@ -64,13 +71,11 @@ fi ## else the calculated value. LSAPI_CHILDREN=${LSAPI_CHILDREN:-${FPM_MAX_CHILDREN:-$calc_lsapi_children}} -## extprocessor mem limits — total LSAPI heap should fit AVAILABLE_MB with -## some breathing room. Soft = budget/children, hard = soft * 1.5, both capped -## at 2047 (OLS interprets > 2047 oddly in some 1.x builds). -LSAPI_MEM_SOFT=$((AVAILABLE_MB / LSAPI_CHILDREN)) -if [ "$LSAPI_MEM_SOFT" -lt 64 ]; then LSAPI_MEM_SOFT=64; fi -if [ "$LSAPI_MEM_SOFT" -gt 2047 ]; then LSAPI_MEM_SOFT=2047; fi -LSAPI_MEM_HARD=$((LSAPI_MEM_SOFT * 3 / 2)) -if [ "$LSAPI_MEM_HARD" -gt 2047 ]; then LSAPI_MEM_HARD=2047; fi +## Per-worker mem limits (RLIMIT_AS) live in httpd_config.tpl now as +## hard-coded 1024M soft / 1500M hard — those values comfortably fit +## typical Divi/WooCommerce VSZ (~280-365 MB) while still catching a +## true runaway script. Cgroup remains the real backstop. The earlier +## AVAILABLE/CHILDREN formula was killing legitimate workers because +## it conflated VSZ (RLIMIT_AS) with RSS-budget arithmetic. -export CONTAINER_MEMORY_MB LSAPI_CHILDREN LSAPI_MEM_SOFT LSAPI_MEM_HARD +export CONTAINER_MEMORY_MB LSAPI_CHILDREN diff --git a/scripts/entrypoint-litespeed.sh b/scripts/entrypoint-litespeed.sh index 192ae3d..211e142 100644 --- a/scripts/entrypoint-litespeed.sh +++ b/scripts/entrypoint-litespeed.sh @@ -47,7 +47,7 @@ chmod 1777 /tmp/lshttpd ## ---- memory + lsphp pool sizing ---- # shellcheck source=/dev/null source /scripts/detect-memory-litespeed.sh -echo "Container memory: ${CONTAINER_MEMORY_MB}MB | LSAPI_CHILDREN=${LSAPI_CHILDREN} | memSoft=${LSAPI_MEM_SOFT}M memHard=${LSAPI_MEM_HARD}M | PHPVER=${PHPVER}" +echo "Container memory: ${CONTAINER_MEMORY_MB}MB | LSAPI_CHILDREN=${LSAPI_CHILDREN} | PHPVER=${PHPVER}" ## ---- self-signed cert (idempotent) ---- mkdir -p /usr/local/lsws/conf/cert