Book Deep Read · Chaos Engineering · Chapter 19
Chaos Engineering: System Resiliency in Practice · Ch 19 · Chaos Engineering on a Database · 2020
When you move money in a banking app, it leaves one account and shows up in another. The database behind it must never "take the money and forget to deliver it." Big modern databases are rarely one machine; they are dozens or hundreds of machines working together. This chapter is a first-hand report from a company that builds one (PingCAP, maker of TiDB): how they deliberately torture their own database to make sure it still never loses data or miscounts, even when things break.
You'd think: once all the tests are written and passing, the database is safe to ship. But the nastiest bugs in a distributed database live on paths no test ever walks, like one machine losing power halfway through a write at the exact moment the cable between two others flickers. Each failure alone is handled fine; together they break things. With hundreds of machines running every day, that kind of coincidence eventually happens.
The payoff: coincidences that might happen "once in a blue moon" in real life get hit several times a day in testing, so bugs are caught before users meet them. One honest cost: this kind of shaking can only prove "we found a bug," never "there are no bugs left," and building and running a simulator around the clock takes real people and machines.
Database bugs are quiet, so keep causing accidents on purpose while auditing against rules like "the total can never change."
Want to see where faults land in TiDB's architecture, how the four injection layers work, why the bank test catches consistency bugs, and where the Schrodinger platform and Chaos Mesh came from? → Switch to Deep
You'd think test cases keep a database correct; in fact the deadliest bugs in a distributed database hide on paths where several faults happen to overlap, and they are quiet: no crash, just a wrong answer. This chapter is the PingCAP team's field report on TiDB: inject faults systematically at four layers (application, CPU/memory, network, filesystem), judge correctness against invariants such as "the sum of all balances is conserved," and hand the whole loop to an automated platform that runs day and night. That platform's know-how later became the open-source Chaos Mesh.
Chapter 19, in Part V ("Evolution"), written by Liu Tang and Hao Weng of PingCAP. It follows Ch18 on the human side of systems and precedes Ch20 on security chaos engineering. It is one of the book's few complete practice reports from a database vendor, and maps onto any team building or running distributed storage (NewSQL, distributed key-value stores, message queues).
TiDB is a MySQL-compatible distributed database; production clusters often run tens to hundreds of nodes and carry heavy transactional load. For such a system, "never lose data, never miscount" is non-negotiable. A web service down for five minutes can apologize; a database that drops one transfer may force a business to halt and reconcile.
Three things make it hard. First, failure is normal: slow disks, crashed processes, broken links and jumping clocks happen daily at scale. Second, failures overlap: each single failure's handling path usually has a unit test, but combinations like "a network partition in the middle of a leader change" are hard to write down in advance. Third, database bugs are quiet: no alarm, just a wrong result that may spread before anyone notices. The chapter asks how to turn "correct under failure" from a promise into a fact verified every day.
To shake a system you first need its skeleton. TiDB splits into three parts:
This picture defines the experiment checklist: kill one TiKV replica, and can Raft elect a new leader without losing committed writes? If the PD leader dies, do timestamps stay monotonic? If the link between the SQL layer and storage breaks, do transactions roll back or hang?
The chapter groups injection by the layer it hits, from most precise to most realistic:
① Application: failpoints. "Fault switches" planted in code. Normally they do nothing; switched on in a test, they make a given line return an error, sleep, or panic. PingCAP wrote pingcap/failpoint for Go and fail-rs for Rust, modeled on FreeBSD's failpoints. Their value is precision: a razor-thin window like "a Raft snapshot fails halfway through sending" can be reproduced reliably.
② CPU and memory. Stress tools hog CPU and eat memory to simulate a noisy neighbor saturating the machine. Under resource starvation a database shows chain reactions such as heartbeat timeouts and frequent leader changes, which you never see on an idle box.
③ Network. Linux traffic control (tc) injects delay, loss and reordering; firewall rules (iptables) cut links between specific nodes to create partitions. This is the layer most worth shaking, because everything subtle about consensus lives in "what if a message is lost or late?"
④ Filesystem. Intercept I/O calls to slow them down or return errors (disk full, corrupt reads), checking whether the storage engine ever treats a half-finished write as success, or keeps running in a damaged state after an error.
Two more run throughout: killing processes / restarting nodes, and clock skew, setting a node's time forward or back. TiDB's transaction timestamps come from PD, so local clocks don't directly order transactions, but logic built on timeouts and leases can still be disturbed by a jumping clock, so it gets shaken too.
Take the canonical experiment: a three-replica Region, with leader node A cut off from B and C. The steady-state hypothesis: the minority side (A) can no longer commit writes; the majority side (B, C) elects a new leader after the election timeout and keeps serving; no committed data is lost; when the network heals, the old leader steps down and catches up. Any violation is a bug: A still believing it is leader and serving stale data (breaking linearizable reads), or both sides accepting writes (split brain).
Injecting faults is half the job; the other half is detection. The chapter uses two kinds of signal:
The beauty of invariants: you don't need to know what the bug looks like, only what must never happen. This is the same spirit as Jepsen, which records the operation history and checks it against the consistency model the system claims.
Running one partition experiment by hand is easy. Running thousands, every day, against every new build, in every combination, is not. PingCAP built an internal platform called Schrodinger (after Schrödinger's cat: until you open the box, you don't know whether the system is alive or dead), running on Kubernetes. Roughly:
The fault-injection capabilities built into Schrodinger were later extracted and open-sourced as Chaos Mesh: you declare, as Kubernetes custom resources (CRDs), "which pods, what fault, for how long," and any system on Kubernetes can use it. That is the chapter's landing point: for a database, chaos engineering is not a one-off drill but a continuous verification pipeline running alongside CI.
The core tension: the more precise the injection, the easier it is to reproduce and pinpoint, but the further it is from real failures; the more realistic, the more it uncovers the unexpected, but the harder it is to reproduce.
Fault-injection methods by layer
| Method | Good at finding | Reproducibility | Cost / limits |
|---|---|---|---|
| In-code failpoints | Races in tiny windows, error-handling branches | High, down to a line | Must be planted in code; only tests points you thought of |
| Kill / restart | Crash recovery, leader election, log replay | Medium | Coarse; misses "slow but not dead" |
| Network (tc / iptables) | Partitions, split brain, timeout and retry logic | Medium | Needs root / network namespaces; combinatorial blow-up |
| Filesystem I/O | Torn writes, full disks, silent corruption | Medium | Needs an interception layer; adds overhead |
| Clock skew | Leases, timeouts, logic tied to local time | Medium-low | Blast radius hard to predict; can break the test tooling itself |
| CPU / memory stress | Heartbeat timeouts, jitter-driven cascades of failover | Low | Results vary a lot by hardware |
Routes to verifying "still correct under failure"
| Route | Example | Strength | Cost |
|---|---|---|---|
| Unit / integration tests + failpoints | Everyday development | Fast, easy to pinpoint | Covers only anticipated paths |
| Black-box history checking | Jepsen | Checks against a consistency model; finds isolation holes | Heavy per analysis; not continuous |
| Continuous chaos platform | Schrodinger / Chaos Mesh | Real binaries, real environment, runs day and night | Cluster and staff cost; hard to reproduce |
| Deterministic simulation | FoundationDB | Any failure fully replayable from a seed | System must be designed for simulation from day one |
Rule of thumb: a distributed database needs at least "invariant checking plus continuous injection" together. Injection without auditing tests availability, not correctness.
In an interview or design review, when asked "how do you prove this store doesn't lose data under failure," saying "we use Raft" is not enough: the algorithm is correct, the implementation may not be. A credible answer lists the invariants to verify (no committed write lost, conserved totals, monotonic reads), names which faults are injected at which layers, and says whether these experiments run automatically before every release. This chapter is a complete template you can copy.
Industry evidence
① The most dangerous bugs in a distributed database hide where several faults overlap, and they are quiet: no crash, just wrong answers.
② Know the architecture: TiDB = stateless SQL tier + Raft-replicated TiKV + PD for scheduling and timestamps; every node and link is an injection point.
③ Four layers of injection: application (failpoints), CPU/memory, network (tc / iptables), filesystem; plus process kills and clock skew.
④ Steady state for a partition: the majority elects a new leader, the minority stops writing, no committed data is lost, no split brain.
⑤ Detection stands on two legs: metrics for availability, invariants (the bank test's conserved total) for correctness.
⑥ Invariants don't require knowing what the bug looks like, only what must never happen.
⑦ Automation is the key: Schrodinger builds, deploys, injects and verifies every day; its know-how became the open-source Chaos Mesh.
⑧ Trade-off: precision aids reproduction, realism finds surprises; continuous chaos, Jepsen and deterministic simulation complement each other.
⑨ Pitfalls: "we use Raft so we're consistent" and "it stayed up so it passed" are both wrong; whether you can reproduce a found bug decides success too.