Колхозная реализация выбора лидера на Go и Consul

2 октября 2017

При разработке веб-приложений и всяких там бэкендов иногда возникает необходимость запустить кусок кода ровно на одном сервере, а не на всех. Типичный случай — это когда вам нужно запустить фоновую миграцию схемы базы данных. Однако бывают и другие сценарии, некоторые из которых упоминались в заметке Пример использования акторов-одиночек в Akka. Задача выбора одного «главного» сервера из N довольно просто решается при помощи подхода под названием leader lease, речь о котором и пойдет далее.

Для реализации подхода нужно иметь какое-нибудь key-value хранилище, поддерживающее compare and swap. Кроме того, leader lease предполагает, что время на серверах относительно синхронизировано, например, при помощи NTP. В качестве KV-хранилища я решил использовать Consul, но с тем же успехом можно использовать etcd, или любую NoSQL базу данных, поддерживающую CAS. Помимо прочих, к таким базам данных относятся Cassandra и Couchbase.

Идея проста до безобразия. В KV-хранилище заводится ключ, по которому пишется что-то вроде «нода X является лидером до времени Y», где Y вычисляется как текущее время + какой-то интервал T. Будучи лидером, нода X раз в T/2 или T/3 единиц времени обновляет запись, тем самым продлевая свою роль лидера. Если нода падает или не может достучаться до KV-хранилища, спустя время T ее место займет нода, которая первая обнаружит, что роль лидера освободилась. CAS нужен для предотвращения состояния гонки, если две ноды одновременно пытаются стать лидером.

Библиотека на языке Go, реализующая соответствующий подход, заняла у меня около 270 строк. Код библиотеки вы можете найти на GitHub. Соответственно, реализация на каком-нибудь Python будет раза в два короче.

Не вижу смысла приводить код библиотеки целиком, так как он довольно тривиален. Ограничусь только примером ее использования:

package main

import (
    "flag"
    "github.com/afiskon/go-elector"
    "log"
    "time"
)

func main() {
    selfIdPtr := flag.String("uniqid", "", "Unique id of this node")
    flag.Parse()

    consulUrl := "http://localhost:8500/v1/kv/test/leader_election"
    electorInst, err := elector.Create(*selfIdPtr, consulUrl,
                                       15*time.Second)
    if err != nil {
        log.Panicf("Unable to create the elector: %s", err.Error())
    }

    electorInst.RegisterCallback(func(oldLeaderId string,
                                      newLeaderId string) {
        log.Printf("Leader changed: '%s' -> '%s'\n",
                   oldLeaderId, newLeaderId)
    })

    for {
        leaderId := electorInst.GetCurrentLeader()
        log.Printf("Current leader: '%s'\n", leaderId)
        time.Sleep(5 * time.Second)
    }
}

Так как я редко пишу на Go, при написании библиотеки мне крайне помогли советы Алексея Палажченко, за что я ему весьма благодарен. Помимо прочего, Алексей совершенно справедливо заметил, что использование колбэка в публичном API в мире Go является редким явлением, и более идиоматичным подходом было бы использовать каналы. Однако я уже несколько недель не возвращался к этому коду, поэтому мне лень что-то в нем править. И, сказать по правде, лично мне как-то все равно, что использовать — канал или колбэк.

В общем, я надеюсь, что приведенная информация была для кого-то полезна. Как обычно, буду рад вашим вопросам и дополнениям.

Метки: .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.