The hashicorp/raft library is a Go library to provide consensus via Raft protocol implementation. It is the underlying library behind Hashicorp’s Consul.
I’ve had the opportunity to work with this library a couple projects, namely freno and orchestrator. Here are a few observations on working with this library:
- TL;DR on Raft: a group communication protocol; multiple nodes communicate, elect a leader. A leader leads a consensus (any subgroup of more than half the nodes of the original group, or hopefully all of them). Nodes may leave and rejoin, and will remain consistent with consensus.
- The hashicorp/raft library is an implementation of the Raft protocol. There are other implementations, and different implementations support different features.
- The most basic premise is leader election. This is pretty straightforward to implement; you set up nodes to communicate to each other, and they elect a leader. You may query for the leader identity via Leader(), VerifyLeader(), or observing LeaderCh.
- You have no control over the identity of the leader. You cannot “prefer” one node to be the leader. You cannot grab leadership from an elected leader, and you cannot demote a leader unless by killing it.
- The next premise is gossip, sending messages between the raft nodes. With
hashicorp/raft
, only the leader may send messages to the group. This is done via the Apply() function. - Messages are nothing but blobs. Your app encodes the messages into
[]byte
and ships it via raft. Receiving ends need to decode the bytes into a meaningful message. - You will check the result of Apply(), an ApplyFuture. The call to Error() will wait for consensus.
- Just what is a message consensus? It’s a guarantee that the consensus of nodes has received and registered the message.
- Messages form the raft log.
- Messages are guaranteed to be handled in-order across all nodes.
- The leader is satisfied when the followers receive the messages/log, but it cares not for their interpretation of the log.
- The leader does not collect the output, or return value, of the followers applying of the log.
- Consequently, your followers may not abort the message. They may not cast an opinion. They must adhere to the instruction received from the leader.
hashicorp/raft
uses either an LMDB-based store or BoltDB for persisting your messages. Both are transactional stores.- Messages are expected to be idempotent: a node that, say, happens to restart, will request to join back the consensus (or to form a consensus with some other node). To do that, it will have to reapply historical messages that it may have applied in the past.
- Number of messages (log entries) will grow infinitely. Snapshots are taken so as to truncate the log history. You will implement the snapshot dump & load.
- A snapshot includes the log index up to which it covers.
- Upon startup, your node will look for the most recent snapshot. It will read it, then resume replication from the aforementioned log index.
hashicorp/raft
provides a file-system based snapshot implementation.
One of my use cases is completely satisfied with the existing implementations of BoltDB
and of the filesystem snapshot.
However in another (orchestrator
), my app stores its state in a relational backend. To that effect, I’ve modified the logstore and snapshot store. I’m using either MySQL or sqlite
as backend stores for my app. How does that affect my raft
use?
- My backend RDBMS is the de-facto state of my
orchestrator
app. Anything written to this DB is persisted and durable. - When
orchestrator
applies a raft log/message, it runs some app logic which ends with a write to the backend DB. At that time, the raft log is effectively not required anymore to persist. I care not for the history of logs. - Moreover, I care not for snapshotting. To elaborate, I care not for snapshot data. My backend RDBMS is the snapshot data.
- Since I’m running a RDBMS, I find
BoltDB
to be wasteful, an additional transaction store on top a transaction store I already have. - Likewise, the filesystem snapshots are yet another form of store.
- Log Store (including Stable Store) are easily re-implemented on top of RDBMS. The log is a classic relational entity.
- Snapshot is also implemented on top of RDBMS, however I only care for the snapshot metadata (what log entry is covered by a snapshot) and completely discard storing/loading snapshot state or content.
- With all these in place, I have a single entity that defines:
- What my data looks like
- Where my node fares in the group gossip
- A single RDBMS restore returns a dataset that will catch up with raft log correctly. However my restore window is limited by the number of snapshots I store and their frequency.
Thank you very much for this article, it’s really helpful!