Replicated State Machine Background
A service running on one machine has an obvious weakness:
One machine fails → service stops
The natural solution is to replicate service data:
┌─ Replica 1
Service ├─ Replica 2
└─ Replica 3
But merely copying data is insufficient.
If the replicas:
- contain different state;
- process requests in different orders;
- disagree about which requests completed;
- produce different outputs after failover;
then switching to a backup can expose incorrect behavior.
The real goal is:
Replicate the service so that all replicas behave like one logical machine, even when an individual machine fails.
This is the purpose of a replicated state machine, or RSM.
Fault Tolerance and Availability
Fault tolerance means the service continues behaving correctly despite failures.
The desired user experience is often failure-agnostic:
- the service remains available;
- completed operations are not lost;
- the user does not see state move backward;
- failover does not produce contradictory results;
- the performance disruption is limited.
Suppose a client successfully received OK for the following request:
deposit(account, $100)
If the primary then crashes, the new primary must not behave as though the deposit never occurred. The previously visible result is now part of the service’s externally observed history.
Failure Models
A failure model specifies what faulty components are allowed to do.
Fail-stop failure
When a node stops, every other node knows with certainty that it failed
This is the simplest model.
In reality, perfectly detecting failure is difficult because failure may cause due to a lot of things:
- the node actually failed
- merely slow
- network partition
Crash failure
When a node stops, other nodes may not know whether it crashed
This creates uncertainty:
Crashed vs slow vs network partitioned
This is the uncertainty behind FLP and many leader-election problem
Omission failure
A node may fail to send or receive some messages
For example:
- it processes a client request but loses the response;
- it sends an update to one backup but not another;
- it receives only part of the replication stream.
Byzantine failure
A faulty node can behave arbitrarily
It may:
- send different messages to different replicas;
- invent operations;
- corrupt its state;
- lie about what it committed;
- impersonate a correct execution;
- collude with other faulty nodes.
How woud you tolerate a failed node?
- Replicas
- I have some backups of my data, so will not lose data
- Failure-agnostic
- Ideally, do not want users to realize there was a failure at all
How would you make failures agnostic?
- Make replicas always the same, thus the set of replicas appear to be a logical single machine
Replicated State Machine (Deterministic Automation)
Today when people talk about fault tolerance in distributed systems, they mean RSM techniques to keep system state safe under failures.
Determinism is essential for RSM
A deterministic state machine satisfies:
same state + same input -> same next state and output
A state machine’s outputs must be determined by its request sequence, rather than timing or unrelated external activity.
Each replica (state machine) receives the same set of operations and executes them in the same order
- How do we maintain the same set of operation for every replica?
- How can we ensure each replica executes them in the same order?
- A bunch of techniques exist; we start with a simple one: Primary Backup
Primary Backup
A replica group contains (N) replicas, which at any moment:
- exactly one is the primary;
- the others are backups.
Primary responsibilities:
- receives client requests;
- chooses their total order;
- assigns log positions;
- replicates the log;
- usually sends the client response.
Backup responsibilities:
- receives operations from the primary;
- stores them in the same log positions;
- executes committed operations in order, or saves them for later execution;
- takes over if the primary fails.
Normal Primary-Backup Operations
- Primary gets operations
- Primary orders ops as a sequence of totally ordered items into log
- Replicates log to backups
- Backup executes opts or just inserts into log
- Primary gets ack from backup and then reply to client
Multicore execution can break determinism
The observation is:
- a primary may process requests concurrently on multiple cores;
- a backup may replay the log sequentially.
Suppose primary executes two operations that modify the same state:
A: x = x + 1
B: x = x * 2
The primary might physically interleave them in one way while the backup executes them in another.
Solution
Therefore, the primary must not merely log when requests arrived. It must log the logical order in which their effects committed:
log[10] = A
log[11] = B
The backup then reproduces that logical order.
Replication must capture the output of concurrency control, such as:
- the chosen serialization order;
- committed values;
- lock or transaction outcomes;
- nondeterministic decisions.
What if some operations are non-deterministic
Some operations do not produce results determined solely by state and input. For example:
random()timeofday()
Solution
The solution is to replicate the decision. For example,
instead of letting every replica call the clock independently:
expire_session(timeofday())
the primary chooses a time and logs:
expire_session(chosen_time=14:03:17)
The backups replay the primary’s chosen value.
A good general rule is:
Perform nondeterminism once, convert its result into deterministic replicated input, and make every replica apply that result.
Failover Duplicate execution problem
Suppose the request is:
withdraw(account, $100)
The request commits, but the reply is lost.
The client retries:
withdraw(account, $100)
If the new primary treats it as a new request, the account loses $200.
Solution: Request Identifiers
Clients attach a unique identifier or monotonically increasing sequence number:
client = C7
request number = 42
operation = withdraw($100)
Replicas store a client table:
C7:
highest completed request = 42
cached reply = OK
When request 42 arrives again, the new primary:
- recognizes it as a duplicate;
- does not execute the withdrawal again;
- resends the cached response.
This provides at-most-once execution.
Primary Failure Handling
Configurations, views, and epochs
A configuration identifies:
- the current primary;
- the backups;
- often a monotonically increasing configuration number.
It may be represented as:
configuration 8:
primary = P
backups = {B1, B2}
After failover:
configuration 9:
primary = B1
backups = {B2, P}
Other protocols call a configuration an:
- epoch;
- term;
- view.
The increasing number distinguishes messages from old and new leadership periods.
Atomic test-and-set
There is a shared storage containing the primary role.
A candidate executes an atomic test-and-set:
if primary_role is unclaimed:
claim primary_role
Atomicity ensures that if multiple backups race, only one succeeds.