Skip to content

Commit f2a4cfc

Browse files
committed
docs: measure FRANKENPHP_RESET_KERNEL instead of guessing at it
Changes - Record the three-way measurement on `/admin` in the Caddyfile, README and changelog - Say what cloning the kernel actually does, and stop implying the reset erases worker mode's benefit Why The comment claimed the reset costs "a boot per request", which is true — `AbstractKernel::__clone()` nulls the container and clears `booted`, so the next `handle()` runs `initializeBundles()` and instantiates the compiled container again. But it was asserted rather than measured, and the conclusion drawn from it was wrong. Measured in prod, 40 seconds at 20 concurrent on `/admin`: 1319 requests per second and a 9.0 ms median with no worker, 1494 and 4.3 ms with one, 1395 and 6.1 ms with one plus the reset. The reset keeps roughly half the gain and still beats no worker on both numbers, because the PHP runtime, OPcache and autoloader stay warm across requests even when the kernel does not. That makes it a reasonable first worker-mode configuration to deploy rather than only something to compare against. Also notes that a boot is not a recompile, which the old wording invited readers to assume.
1 parent cb49602 commit f2a4cfc

4 files changed

Lines changed: 51 additions & 25 deletions

File tree

.docker/Caddyfile

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -102,13 +102,17 @@
102102
# Uncommenting the worker line is all worker mode needs: FrankenPHP sets
103103
# FRANKENPHP_WORKER=1, and symfony/runtime has picked its own
104104
# FrankenPhpWorkerRunner off that since 7.4 — no PHP package, no
105-
# APP_RUNTIME override. Symfony 8.1 adds FRANKENPHP_RESET_KERNEL=1, which
106-
# clones the kernel between requests to mitigate state leaks at the cost
107-
# of a boot per request.
105+
# APP_RUNTIME override.
108106
#
109-
# Off until the stateful services are dealt with: LeantimeService caches
110-
# the Leantime user directory per instance, and the package/module
111-
# factories clear their dedup buffers outside a finally.
107+
# Symfony 8.1 adds FRANKENPHP_RESET_KERNEL=1, which clones the kernel
108+
# after each request. That does mean a kernel boot per request —
109+
# AbstractKernel::__clone() nulls the container and clears `booted`, so
110+
# the next handle() runs initializeBundles() and instantiates the
111+
# compiled container again — but it keeps the PHP runtime, OPcache and
112+
# autoloader warm, so it is not the same as no worker at all. Measured on
113+
# /admin in prod: 1319 rps and a 9.0 ms median without a worker, 1494 and
114+
# 4.3 ms with one, 1395 and 6.1 ms with one plus the reset. Roughly half
115+
# the gain, and immune to state leaking between requests.
112116
php_server {
113117
#worker /app/public/index.php
114118
}

CHANGELOG.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6363
`igor-baseline.json` records the 33 existing findings with a reason each, so
6464
the job fails only on new ones; vendor code is out of scope
6565
- Document worker mode and the statelessness it requires in `README.md` and
66-
`claude.md`. It stays off: measured here it gives roughly 20% more requests
67-
per second on `/admin` and half the median latency, but around 40% fewer on
68-
`/health/live`, and the numbers come from a laptop sharing CPU with other
69-
containers
66+
`claude.md`. It stays off. Measured on `/admin` in prod: 1319 requests per
67+
second without a worker, 1494 with one, 1395 with one plus
68+
`FRANKENPHP_RESET_KERNEL=1` — so the reset keeps about half the gain rather
69+
than erasing it. `/health/live` inverts the ranking, and the numbers come
70+
from a laptop sharing CPU with other containers
7071
- [#96](https://github.com/itk-dev/devops_itksites/pull/96)
7172
Show the Service Agreements monthly price as Danish kroner,
7273
`12.500,50 kr.`, on index and detail

README.md

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -219,14 +219,25 @@ Add a count – `worker /app/public/index.php 8` – to override the default, wh
219219
is twice the number of CPU cores. Keep `num_threads` × `memory_limit` below the
220220
memory available to the container.
221221

222-
What it bought here, measured on this project in `prod` with a warm OPcache:
223-
about 20% more requests per second on `/admin` and half the median latency,
224-
against about 40% *fewer* on `/health/live`. The trivial endpoint is worker
225-
mode's worst case – there is no per-request work for the saved kernel boot to be
226-
weighed against, and the runner's `gc_collect_cycles()` on every request is not
227-
free. The numbers come from a laptop sharing CPU with other containers and
228-
running the application over a bind mount, so treat them as a shape rather than
229-
a figure, and measure again on a server before adopting.
222+
What it bought here, measured in `prod` with a warm OPcache, 40 seconds at 20
223+
concurrent on `/admin`:
224+
225+
| | requests/sec | median |
226+
| --- | --- | --- |
227+
| no worker | 1319 | 9.0 ms |
228+
| worker | 1494 | 4.3 ms |
229+
| worker + `FRANKENPHP_RESET_KERNEL=1` | 1395 | 6.1 ms |
230+
231+
On `/health/live` the ranking inverts – roughly 40% *fewer* requests per second
232+
with a worker. That endpoint returns a constant, which is worker mode's worst
233+
case: there is no per-request work for the saved kernel boot to be weighed
234+
against, and the runner's `gc_collect_cycles()` on every request is not free.
235+
Worth knowing, since the health endpoints are the polled ones.
236+
237+
The numbers come from a laptop sharing CPU with other containers and running the
238+
application over a bind mount, so treat them as a shape rather than a figure.
239+
Short runs on that machine varied by more than tenfold; only 40-second runs were
240+
reproducible. Measure again on a server before adopting.
230241

231242
**Services must not carry request state.** Under php-fpm a service instance died
232243
with the request; in a worker it does not, so anything a service remembers leaks
@@ -257,10 +268,18 @@ The audit needs the service map that `IgorPhpBundle` writes during
257268
scope (`ignore_vendors` in `igor.json`): it reported 341 findings there, none of
258269
them ours to fix.
259270

260-
`FRANKENPHP_RESET_KERNEL=1`, on Symfony 8.1 and later, clones the kernel between
261-
requests instead. It hides this class of bug at the cost of a boot per request,
262-
which is most of what worker mode is for – useful to compare against, not to
263-
depend on.
271+
`FRANKENPHP_RESET_KERNEL=1`, on Symfony 8.1 and later, clones the kernel after
272+
each request instead, which makes this class of bug harmless.
273+
`AbstractKernel::__clone()` nulls the container and clears `booted`, so the next
274+
request runs `initializeBundles()` and instantiates the compiled container again
275+
– a kernel boot, though not a recompile. It is not as expensive as it sounds:
276+
the PHP runtime, OPcache and autoloader stay warm, and it kept about half the
277+
worker-mode gain in the table above while still beating no worker on both
278+
throughput and latency.
279+
280+
That makes it a reasonable first configuration to deploy rather than only a
281+
diagnostic – most of the latency win, immune to the leaks the audit below
282+
guards against – with the reset turned off later once there is confidence.
264283

265284
#### Metrics
266285

claude.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,9 +134,11 @@ the job fails only on new ones — fix a genuine finding rather than baselining
134134
it, and regenerate with `composer worker-state-baseline` only after a deliberate
135135
change.
136136

137-
`FRANKENPHP_RESET_KERNEL=1` (Symfony 8.1+) clones the kernel between requests
138-
and papers over all of this, at the cost of a boot per request. Treat it as a
139-
measurement baseline, not a fix.
137+
`FRANKENPHP_RESET_KERNEL=1` (Symfony 8.1+) clones the kernel after each request,
138+
which makes all of this harmless. It costs a kernel boot per request but keeps
139+
the PHP runtime warm, and measured on `/admin` it held about half the worker-mode
140+
gain while still beating no worker — so it is a usable configuration, not just a
141+
baseline. It is not a licence to write stateful services: the audit still runs.
140142

141143
## Quality Checks
142144

0 commit comments

Comments
 (0)