RAID and Redundancy

One disk gives you one disk's capacity, one disk's speed, and one disk's chance of ruining your week when it dies. RAID — a Redundant Array of Independent Disks — is the idea that several cheap drives, coordinated by the block layer or a controller, can be made to look like a single volume that is faster, bigger, or more reliable than any one of them. The catch, and the whole subject, is that you cannot maximise all three at once. RAID is a family of explicit trade-offs between capacity, performance, and reliability.

The two primitive tricks are striping (spread data across disks so they work in parallel) and redundancy (keep extra copies or parity so a failure loses nothing). Every RAID level is a different recipe mixing these two — and the interesting engineering lives in the parity schemes, the "small write" problem they create, and the terrifying arithmetic of rebuild time when a disk in a large array finally fails.

The level zoo

Six levels cover almost everything you meet in practice. Read this table as "what am I buying, and what am I paying?" for an array of N disks each of size C.

LevelTechniqueUsable capacityToleratesNotes
RAID 0striping onlyN\cdot C0 failuresfastest, zero safety — one death loses all
RAID 1mirroringC (2 disks)1 failuresimple, halves capacity
RAID 4striping + dedicated parity disk(N-1)\cdot C1 failureparity disk is a write bottleneck
RAID 5striping + distributed parity(N-1)\cdot C1 failurethe workhorse; small-write penalty
RAID 6striping + two parities(N-2)\cdot C2 failuressurvives a failure during rebuild
RAID 10mirror, then stripeN\cdot C/2≥1 (often more)fast + safe, expensive

The elegant trick behind RAID 4/5/6 is parity via XOR. For a stripe of data blocks A, B, C, store P = A \oplus B \oplus C. XOR's magic is that it is its own inverse: if disk B dies, you recover it as B = A \oplus C \oplus P. You get one disk of protection for the price of one disk of capacity — vastly cheaper than mirroring everything. RAID 6 adds a second, independent parity (Reed–Solomon style) so any two disks can fail and still be reconstructed.

Reconstruct a lost disk with XOR

This is the whole reliability story in ten lines. We build a stripe, compute its parity, then "lose" a disk and rebuild its exact bytes from the survivors plus parity. Change which disk fails — it always comes back.

// RAID-5 parity and reconstruction via XOR. Each "block" is one byte here for clarity. const A = 0b1011_0010; const B = 0b0110_1101; const C = 0b1100_0001; const P = A ^ B ^ C; // parity block: P = A ⊕ B ⊕ C const bin = (x: number) => x.toString(2).padStart(8, "0"); console.log(`A = ${bin(A)}`); console.log(`B = ${bin(B)}`); console.log(`C = ${bin(C)}`); console.log(`P = ${bin(P)} (parity = A ⊕ B ⊕ C)`); // Disk holding B dies. Rebuild it from the survivors and parity. const Brebuilt = A ^ C ^ P; // A ⊕ C ⊕ P must equal B console.log(`\nDisk B failed. Rebuild B = A ⊕ C ⊕ P = ${bin(Brebuilt)}`); console.log(`recovered correctly? ${Brebuilt === B}`);

The small-write problem — why RAID 5 hates tiny updates

Parity is cheap to read but expensive to update. Suppose you overwrite a single data block A \to A' in a RAID-5 stripe. Parity must change too — but recomputing it naively means reading every other block in the stripe. The clever shortcut still costs four I/Os, the read-modify-write:

So one logical small write becomes two reads + two writes — a 4× amplification, and the parity disk (RAID 4) or parity blocks (RAID 5) become hot. This is why RAID 5 is superb for large sequential writes (compute parity once for a whole stripe) and painful for small random writes — databases with many tiny updates often choose RAID 10 instead.

A stripe's data and its parity live on different disks, so they cannot be updated in one atomic step. If power fails between writing the new data and writing the new parity, the stripe is now inconsistent — parity no longer matches data — and nothing detects it until a disk later fails and you reconstruct from that stale parity, silently returning wrong bytes. This is the RAID write hole. Hardware controllers plug it with battery-backed cache (finish the write after power returns); software systems like ZFS/btrfs plug it by making the stripe update copy-on-write and checksummed, so a torn stripe is detected and never trusted. It is a lovely example of why "just add parity" is not the end of the reliability story.

The single most expensive misconception in all of storage: "we're on RAID 6, our data is safe." RAID protects against disk hardware failure and nothing else. It faithfully, instantly replicates a rm -rf /, a ransomware encryption, a buggy write, or a controller that scribbles garbage — across every disk in the array. It does not protect against fire, theft, or the building flooding. Mirroring a mistake gives you a very reliable copy of the mistake. RAID buys you availability (keep serving through a dead disk) and sometimes performance; a real backup is a separate, ideally offline and off-site, point-in-time copy. You need both, and they solve different problems. Never let RAID lull you out of backups.

Rebuild time and the case for RAID 6

When a disk dies, the array runs degraded and must rebuild the lost disk onto a spare by reading every surviving disk in full and recomputing the missing blocks. For a modern 16 TB drive at a realistic ~150 MB/s rebuild rate, that is 16{,}000{,}000 / 150 \approx 106{,}000 s ≈ 30 hours — and often far longer under live load. During that whole window a RAID 5 array has zero remaining redundancy: a second disk failure means total data loss.

Worse, rebuild stresses the surviving disks — reading them cover to cover, at exactly the moment they are the same age and model as the one that just died. As drives grew past a few terabytes, the probability of a second failure (or an unrecoverable read error) during a multi-day rebuild stopped being negligible. That is the whole argument for RAID 6: two parities mean the array survives a second failure during the rebuild. For large arrays of large disks, RAID 6 is not paranoia — it is the statistically responsible default.