Leader election
Recently all our services were replicated to multiple data centers on Google Cloud Platform. The REST API services scaled well without any code changes. However the batch jobs and their schedulers needed attention to ensure we avoid any kind of race conditions.
Leader election is ubiquitous in today’s world of distributed computing. Knowingly or unknowingly we use a multitude of tools that make use of leader election to ensure systems run like clockwork.
In this article will explore the idea, and one of its implementations in Go using Google Datastore for persisting data.
What is leader election ?
Before I answer that question, we need to understand how modern web applications and services are deployed. Much of the deployment strategy is decided by non functional requirements of the project. Requirements such as minimum availability and response times are taken into consideration for deciding the amount of redundancy and the location of the deployments. For example, if we want to make a service highly available, we would need to make replicas of it. So even if an instance dies, there would always be other instances available to carry out the same duties.
Making deployments redundant present us with another problem of ensuring that no two instances of the same job are performing the same piece of work at the same time. That of course does not imply that two instances cannot run at the same time, they shouldn’t just get in each others way that will result in the system becoming inconsistent.
Enter leader election.
It is the process of electing one of the replicas as the leader and letting it decide what to do, while the other replicas either remain idle or follow the leader. The decision depends on your use case. The leader may choose to perform a piece of task on its own, or it may decide to split the task into smaller chunks and distribute them to the other replicas.
How can this be implemented ?
There are quite a few tools and techniques available for implementing leader election in your project. The one we explore here is based on an idea presented in this video (double thumbs up 👍🏽👍🏽 to the folks at Microsoft for creating amazing documentation and training material). I quite liked this approach for its simplicity and the fact that it can work with any database system that features atomic operations.
The job
As a contrived example, let us create a job that preaches how cool Go is. Every time the job is scheduled it says something preachy about the language. However, in case we decided to scale up our preachers, we’d like to ensure that we are not getting too preachy. That is to say that at any given point in time only one job gets to preach the message.
Our preacher is simple. It accepts a StringWriter as an argument and it preaches by writing the message to the string writer. Moreover since it is a preacher it likes to repeat the message, for which it accepts an integer indicating how many times the message should be printed.
We create a custom type, Job, which consists of a name and a doable (a function). In the next section we will create a scheduler which accepts a job that gets scheduled periodically.
|
|
The scheduler
At the heart of our application is the scheduler. It is this component that on getting elected successfully schedules the job.
Before implementing the scheduling logic we need to create a data structure that would hold the lease information. In our case we can define a lease as a temporary right to schedule a job. It is important to understand that these leases need to be temporary to avoid a situation where a scheduler becomes the leader by getting the lease but eventually crashes. This would result in a deadlock wherein the previous leader is not available to work, yet other schedulers cannot become the new leader.
A lease can be uniquely identified by its job name, as there can be exactly one scheduler as the leader for a job. Thus, we can use the job name as a name key, and include only the leader name and expiry in the structure.
|
|
While creating a scheduler we need to assign it a name to be able to uniquely identify it in the election process.
|
|
Finally we need to implement the becomeLeader
function.
A scheduler can gain leadership in one of the following three scenarios
- There is no leader yet
- The scheduler is already the leader
- The lease of the previous leader has expired
Reading and writing of lease entities need to be done in a transaction to ensure we avoid any race conditions. To guarantee atomicity of our database operations we make use of the client’s RunInTransaction
function.
|
|
Putting the components together
|
|
You are encouraged to play around with the working example available at https://github.com/utsavgupta/go-leader-election.