The configuration part of this course covered what forks is, that its
default is 5, and the argument that it is functioning as an accidental
blast-radius limit which must be replaced before it is removed. All of that
still holds and none of it is repeated here.
This lesson answers the performance question that comes next: how high
can it usefully go, and what decides that?
The short version is that “more forks is faster” is true over a narrow
range and false outside it, and the range is smaller than most people
assume.
The curve, measured
24 hosts, gathering facts, on a controller with 12 cores and 46 GiB of RAM.
Every host uses a local connection, so this measures controller-side work
with the network removed — which isolates the ceiling this lesson is about.
Read-only / Safewall clock against fork count, two passes— ansible-core 2.21.3. Fact gathering is real CPU and process work on the controller, which is what makes it a fair proxy for a fork-bound workload.
$ for f in 1 2 4 8 12 16 24 32; do /usr/bin/time -f '%e s' ansible-playbook -i facts.ini facts.yml -f $f; done
pass 1 pass 2
forks=1 14.49 s
forks=2 7.44 s
forks=4 4.54 s forks=4 4.61 s
forks=8 2.37 s forks=8 2.92 s
forks=12 2.84 s
forks=16 2.83 s forks=16 2.28 s
forks=24 2.60 s
forks=32 2.46 s forks=32 2.60 s
Read the shape rather than the individual numbers.
One to eight: near-linear. 14.49 to 7.44 to 4.54 to around 2.6. Each
doubling roughly halves the time. This is the region where “more forks is
faster” is simply true.
Eight to thirty-two: flat. Every value from 8 upward lands between 2.28
and 2.92 seconds, and the two passes disagree about which fork count is
fastest. The run-to-run spread is about half a second — larger than the
entire difference between forks: 8 and forks: 32.
The honest conclusion from this data is not “16 is optimal”. It is: past
roughly the core count, there is no measurable gain, and anyone reporting
one from a single run is reporting noise.
What each fork costs
A fork is a real operating-system process, and the accounting is concrete.
Per host being worked on, the controller pays for:
A Python process with the Ansible libraries loaded. Tens of megabytes
of resident memory before your task does anything.
An SSH client process and connection, plus a persistent control
socket when ControlPersist is in use — which it is by default, since
ssh_args defaults to -C -o ControlMaster=auto -o ControlPersist=60s.
File descriptors for that connection, its control path, and the pipes
between the worker and the parent.
Memory to hold the module payload and the result. The result is the
variable one: a task returning a large stdout holds all of it in the
worker and again in the parent when it is collected.
Multiply by forks and the arithmetic stops being abstract. At forks: 200, that is 200 Python processes, 200 SSH clients, and 200 result
buffers, concurrently, on one machine.
Three ceilings, not one
The controller is the ceiling people think about. It is rarely the first
one you hit in production.
1. The controller
CPU, memory and file descriptors, as above. The signature is a controller
that is visibly busy — load average above core count, memory pressure,
swap — while the managed nodes are idle.
2. The rate at which your fleet accepts connections
Every fork opens an SSH connection. Two hundred forks means two hundred
near-simultaneous connection attempts, and the things in the path have
opinions about that:
sshd on each target enforces MaxStartups, which by default begins
randomly dropping unauthenticated connections past a threshold.
A bastion or jump host multiplexing your whole fleet is one machine
receiving all of them.
Firewalls and load balancers with connection-rate limits treat a burst of
SSH from one source as exactly what it looks like.
The signature here is intermittent, random unreachable hosts — a
different set each run — with a healthy controller and healthy targets.
3. The managed nodes and what they share
This is the one that defeats the whole premise.
Twenty hosts running apt-get update in parallel against one repository
mirror do not finish twenty times faster. They finish about as fast as
the mirror can serve twenty concurrent clients, which may be slower than
serving five.
The same applies to a licence server, an internal artefact store, a
configuration API, a shared NFS mount, and a database that every host is
about to migrate against. And on each individual host, a package manager
holds an exclusive lock — so a play that runs two package tasks against the
same host gains nothing from parallelism it was never going to have.
The signature is that the run does not speed up at all, and errors start
arriving from the shared dependency rather than from Ansible: HTTP 429,
mirror timeouts, Could not get lock.
Finding your own number
Read-only / Safethe fork-ceiling experiment— Run against a non-production group. --check keeps it read-only but changes the profile; use it to learn the shape and confirm the chosen value with a real staging run.
for f in 5 10 20 40 80; do
for pass in 1 2 3; do
printf 'forks=%-4s pass=%s ' "$f" "$pass"
/usr/bin/time -f '%e s' ansible-playbook -i inventory/staging site.yml \
--check -f "$f" >/dev/null
done
done
Three passes per value, because of the noise demonstrated above. While it
runs, watch the controller in another shell — the point where load average
climbs past core count, or vmstat shows swap activity, is more
informative than the wall-clock number.
Then apply the rule this course keeps returning to:
Knowledge check
Knowledge check · 4 questions
Q1. On a 12-core controller, the same 24-host play was timed twice at each fork count: 14.5s at 1, 7.4s at 2, 4.5s at 4, and between 2.3s and 2.9s at every value from 8 to 32. What is the defensible conclusion?
Q2. A run takes 4 minutes at forks: 20 and 11 minutes at forks: 100, with a different handful of hosts reporting UNREACHABLE each time. Where should you look first?
Q3. Which of these can stop a run from getting faster as forks increases, even when the controller has capacity to spare? Select all that apply.
Q4. Because forks is a ceiling rather than a target, raising it is a pure performance change with no safety implications.
Passing score: 75%. Answers are checked in this browser.