State Machine Replication (SMR)

Distributed Systems advanced Building with AI 16 min read

Overview

State machine replication (SMR) is the technique that makes Raft- or Paxos-backed services behave like a single fault-tolerant computer. A deterministic state machine consumes a replicated log of commands; as long as every replica applies entries in the same order, their state stays identical despite crashes or message delays.

Transactional key-value engines, distributed coordinators, and queue schedulers all use SMR to provide linearizable APIs. If you understand how to apply, snapshot, and recover logs you can reason about failover bounds, durability guarantees, and maintenance operations.

This document covers log structure, apply pipelines, snapshotting, and operational playbooks that keep SMR clusters consistent even during chaos events.

Core Concepts

Key Concept

Determinism requires requests to be fully specified—no hidden clocks or randomness. Any nondeterminism leads to divergent state even if consensus is perfect.

Key Concept

The applied index is separate from the committed index; crashing after commit but before apply is safe as long as entries are replayed on restart.

How It Works

Applying the Log

A background worker tails the consensus log. When commit index advances, it batches entries, decodes them, and invokes the deterministic state transition. After each batch, it persists the new applied index and any write-ahead redo artifacts.

Key Concept

Apply loops must avoid blocking Raft. Use bounded channels so pressure propagates upstream and leaders slow down before saturating follower disks.

Snapshots and Log Compaction

When the log grows beyond a threshold, replicas checkpoint state (e.g., RocksDB backup, serialized B-Tree) plus the last applied index. Older log entries up to that index can be discarded. Followers that fall behind catch up either by streaming remaining log or by installing the snapshot.

Key Concept

Snapshots must be self-describing: include term/index, cluster ID, and checksum so receivers can validate before replacing local state.

Recovery

After a crash, the node replays the snapshot, then re-applies remaining log entries from the persisted applied index + 1. If the log is truncated beyond that point, the node requests a fresh snapshot from peers.

Practical Applications

Implementation Patterns

Code Examples

Go: Apply Loop Skeleton

func (a *Applier) run(ctx context.Context) {
    for {
        select {
        case batch := <-a.commitCh:
            for _, entry := range batch {
                if err := a.fsm.Apply(entry.Data); err != nil {
                    a.logger.Error("apply failed", "index", entry.Index, "err", err)
                    panic(err)
                }
                a.applied = entry.Index
            }
            if err := a.store.SetApplied(a.applied); err != nil {
                a.logger.Fatal("persist applied", err)
            }
        case <-ctx.Done():
            return
        }
    }
}

Zig: Snapshot Metadata Header

const SnapshotHeader = struct {
    magic: u32 = 0x5A4D5301,
    term: u64,
    index: u64,
    checksum: u128,
};

fn writeSnapshotHeader(writer: anytype, term: u64, index: u64, checksum: u128) !void {
    var hdr = SnapshotHeader{ .term = term, .index = index, .checksum = checksum };
    try writer.writeStruct(hdr);
}

Common Pitfalls

Key Concepts Recap

Further Reading