08 · Chaos Engineering Basics¶
Every module so far has designed for failure — redundancy, health checks, backups, retries. Chaos engineering is how you find out whether those designs actually work, by deliberately causing failures in a controlled way before an uncontrolled one happens on its own.
Why deliberately break things¶
Untested failure-handling code is one of the most common sources of surprise outages: the failover logic that's never actually triggered, the backup that's never actually restored (module 03), the retry policy that turns out to retry non-idempotent writes and duplicates orders. Chaos engineering treats "does the failure-handling actually work" as a falsifiable, testable claim rather than an assumption baked into the architecture diagram.
The core loop:
- Define a steady state — a measurable signal that the system is working normally (e.g. error rate < 1%, p99 latency < 300ms, checkout success rate stable).
- Form a hypothesis — "if we kill one app instance, the steady state holds, because the load balancer's health checks reroute traffic within N seconds."
- Inject a real failure, in a controlled and reversible way.
- Observe — did the steady state hold? If not, where exactly did the assumption break?
- Fix the gap, then re-run the same experiment to confirm the fix actually closed it.
Start small and safe: game days before automated chaos¶
Before running unattended chaos experiments in production, run a game day: a scheduled, announced exercise where the team deliberately breaks something in staging (or a controlled slice of prod) and watches what happens together. This builds the muscle — both the tooling and the team's confidence reading dashboards and reacting — before increasing the blast radius or removing the human from the loop.
Game day checklist:
[ ] Steady-state metric identified and dashboard open
[ ] Hypothesis written down before starting
[ ] Rollback/abort procedure known and tested
[ ] Stakeholders notified of the window
[ ] Blast radius scoped (one instance? one AZ? never "all of prod" on day one)
[ ] Someone assigned to watch alerts, someone assigned to inject the failure
Failure injection techniques, from simple to advanced¶
Process/instance kill — the most basic experiment, directly testing module 01's redundancy and health checks:
# pick one backend instance out of the pool and kill its app process
ssh app-03 'sudo systemctl stop app'
# watch: does the LB stop routing to it within max_fails/fail_timeout?
# does overall error rate stay near zero?
sudo systemctl start app # restore
Network partition / latency injection — simulate a slow or unreachable dependency, testing module 07's timeout/circuit-breaker configuration:
# add 500ms of latency to all outbound traffic on this host (Linux tc/netem)
sudo tc qdisc add dev eth0 root netem delay 500ms
# simulate total packet loss to one specific IP (e.g. the payments service)
sudo tc qdisc add dev eth0 root netem loss 100%
# always remove the rule when done — this is real network impairment
sudo tc qdisc del dev eth0 root
Resource exhaustion — does the app degrade gracefully or crash the whole host when CPU/memory/disk are under pressure?
# stress-ng: consume CPU and memory for a bounded time, then stop automatically
sudo apt install -y stress-ng
stress-ng --cpu 4 --vm 2 --vm-bytes 1G --timeout 120s
# fill disk to test "what happens when /var is full" (do this on a scratch/test host!)
fallocate -l 9G /tmp/fill_disk.img
# ... observe logging, app behavior, alerting ...
rm /tmp/fill_disk.img # release immediately after observing
Dependency failure — stop a database or cache the app depends on and watch how it degrades:
docker stop redis-cache
# does the app fail every request, or fall back to a slower path (DB query
# instead of cache), as designed? Confirm against the actual design intent.
docker start redis-cache
Tooling for repeatable chaos experiments¶
Ad-hoc tc/stress-ng/systemctl stop commands work for a first game
day; for repeatable, scheduled experiments with automatic abort conditions,
purpose-built tools add safety rails:
# Conceptual Chaos Mesh experiment (Kubernetes): kill one pod, auto-revert after 60s
apiVersion: chaos-mesh.org/v1alpha1
kind: PodChaos
metadata:
name: kill-one-app-pod
spec:
action: pod-kill
mode: one
selector:
namespaces: [production]
labelSelectors:
app: orders
duration: "60s"
# Conceptual AWS FIS (Fault Injection Simulator) experiment with a built-in stop condition
description: "Kill 25% of app instances, abort if error rate exceeds 5%"
targets:
instances:
resourceTags: { app: orders }
selectionMode: PERCENT(25)
actions:
- actionId: aws:ec2:stop-instances
stopConditions:
- source: aws:cloudwatch:alarm
value: high-error-rate-alarm
The stopConditions/auto-revert pattern is the key safety feature that
separates "chaos engineering" from "recklessly breaking production": the
experiment monitors the same steady-state signal it's testing against, and
aborts itself the moment things look genuinely bad, rather than relying on
a human to notice and intervene in time.
Blast radius discipline¶
Always experiment against the smallest scope that still tests the hypothesis, and expand only after confidence builds:
1. One instance, in staging
2. One instance, in production, off-peak, with someone watching
3. One AZ/shard, in production, off-peak
4. Scheduled, unattended, small blast radius, automatic abort on steady-state breach
Never start at step 4. Chaos engineering earns broader scope over time by demonstrating the safety mechanisms (fast, correct auto-revert) actually work at a small scale first.
How It Actually Works¶
Why tc qdisc netem can simulate latency/loss without touching the
application at all. tc (traffic control) programs the Linux kernel's
network queuing discipline layer — the code path every outbound packet
passes through between the socket buffer and the NIC driver, below any
application or even the TCP stack's retransmission logic. netem inserts
itself as that queuing discipline and delays, drops, reorders, or
duplicates packets according to its parameters before they're handed to
the network device. Because this operates below the socket layer, it
faithfully reproduces what real network degradation looks like to the
application (timeouts firing, TCP retransmits triggering, connections
resetting) rather than an approximation from mocking a client library —
which is exactly why it's trusted to validate module 07's timeout/circuit
breaker configuration instead of a unit test with a fake slow response.
Why the auto-revert/stop-condition mechanism is the actual safety
boundary, not the blast-radius scoping alone. Chaos Mesh's duration:
"60s" and AWS FIS's stopConditions both work the same way structurally:
the platform doesn't just fire the fault and walk away — it holds a
persistent handle to what it changed (the pod-kill controller's target
selection, or the CloudWatch alarm subscription) and runs a supervisory
loop that either counts down to the duration and reverses the action, or
polls the named alarm and reverses immediately if it fires. This is a
categorically different guarantee than "someone remembers to run the
cleanup command" — a human-run tc qdisc del step can be skipped under
incident pressure or if the terminal session drops, but a stop condition
tied to the platform's own reconciliation loop reverts even if every human
watching the experiment walks away.
Why resource exhaustion experiments reveal failures that process-kill
experiments can't. Killing a process tests the binary up/down health
check path — the exact thing max_fails/liveness probes are built for.
stress-ng --vm-bytes 1G or filling /var instead tests the much harder
degraded-but-technically-running case: a process that responds to health
checks (because the check itself is cheap) while every real request times
out or the OOM killer is about to select it based on cgroup memory
pressure and oom_score_adj. This is the gap most health-check designs
miss — "process responds to /healthz" and "process can actually serve a
real request under current resource pressure" are different claims, and
only resource-exhaustion chaos tests the second one.
Exercise¶
- Pick one HA mechanism you've built in an earlier module (e.g. the
keepalived VIP failover from module 01, or the nginx
max_failspassive health check from Level 2). Write a one-paragraph hypothesis predicting exactly what will happen when you break it. - Run a controlled game day: define the steady-state metric you'll watch, inject the failure (kill the process/interface), and record what actually happened versus your hypothesis.
- If reality didn't match the hypothesis, identify the specific gap (wrong timeout value? health check too lenient? no health check at all?) and fix it.
- Re-run the same experiment after the fix and confirm the steady state now holds — this "fix, then re-verify with the same experiment" step is what turns chaos engineering into an actual improvement loop rather than a one-off finding.