Ну, вы можете много слышать о «распределенной системе» или о «плоте». Но вы можете задаться вопросом, как его использовать.

В этом уроке мы обсудим, как работать с базой данных kv, работающей в кластерном режиме, с использованием Golang и библиотеки Raft. Это руководство больше сосредоточено на коде и использовании кластеров raft, чем на обсуждении алгоритмов raft.

Что такое Raft?

Raft — это протокол, с помощью которого кластер узлов может поддерживать реплицированный конечный автомат.

Конечный автомат синхронизируется за счет использования реплицированного журнала. Однако подробности протокола Raft выходят за рамки этого руководства. Дополнительные сведения о Raft см. в разделе В поисках понятного алгоритма консенсуса.

Реализация плота на Golang

Алгоритм Raft ищет понятный алгоритм консенсуса. К сожалению, для большинства библиотек go требовалось глубокое знание их реализации и API.

Библиотека raft, которую мы будем использовать в этом руководстве, была создана в соответствии с принципом понятности raft, и ее единственная цель — обеспечить консенсус с минималистичным, простым, чистым и идиоматичным API.

Etcd Raft — наиболее широко используемая библиотека Raft в производстве. Но он следует минималистичной философии дизайна, реализуя только базовый алгоритм плота, который оставляет пробелы и двусмысленности.

Итак, вместо того, чтобы изобретать велосипед, библиотека shaj13/raft использует etcd raft в качестве ядра.

Вот как вы можете извлечь выгоду из мощности и стабильности etcd raft с понятным API. Действительно, это заставляет вас сосредоточиться на создании отличного программного обеспечения.

Создание нашего проекта

Начнем с создания нашего проекта.

mkdir raft && cd raft && go mod init raft && touch raft.go

Это создаст новый каталог с именем raft и инициализирует проект с go.mod.

Наши первые строки кода

Прежде чем мы напишем какой-либо код, нам нужно написать некоторый обязательный код, чтобы программа работала.

package main
import (
  "log"
)
func main() {
  log.Println("Raft !!")
}

Создание командной строки нашего процесса

Мы удалим строку, выводящую Raft!!, добавим пакет флагов и инициализируем его.

package main 
import "flag"
func main() {
   addr := flag.String("raft", "", "raft server address")
   join := flag.String("join", "", "join cluster address")
   api := flag.String("api", "", "api server address")
   state := flag.String("state_dir", "", "raft state directory (WAL, Snapshots)")
   flag.Parse() 
}

Создание нашей базы данных kv

Мы собираемся реализовать структуру с именем stateMachine определить kv базу данных, которая считывает и применяет значение ключа, а также делает database моментальный снимок и восстанавливает его.

Создание наших конечных точек

Мы собираемся добавить пакет мультиплексирования гориллы и инициализировать маршрутизатор внутри функции main.

router := mux.NewRouter()

Теперь мы собираемся установить конечные точки нашего API, способ, которым мы это настроим, — создать все наши конечные точки в основной функции, каждой конечной точке нужна функция для обработки запроса, и мы определим их ниже основной функции.

router.HandleFunc("/", http.HandlerFunc(save)).Methods("PUT", "POST")
router.HandleFunc("/{key}", http.HandlerFunc(get)).Methods("GET")
router.HandleFunc("/mgmt/nodes", http.HandlerFunc(nodes)).Methods("GET")
router.HandleFunc("/mgmt/nodes/{id}", http.HandlerFunc(removeNode)).Methods("DELETE")

Обработчики маршрутов

Теперь нам просто нужно определить функции, которые будут обрабатывать запросы.
Прежде чем мы начнем, нам нужно объявить две переменные, чтобы разрешить доступ к данным маршрутов.

  • Узел представляет процесс плота
  • FSM представляет собой конечный автомат процесса плота

Создание нашего плотного узла и сервера

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

Теперь ваш файл должен выглядеть примерно так:

Тестирование нашего кода

Строительство плотного кластера

go mod tidy && go build raft.go 

Спуск одноузлового плота

Сначала запустим одночленный кластер raft:

./raft -state_dir=$TMPDIR/1 -raft :8080 -api :9090

Каждый процесс raft поддерживает один экземпляр raft и сервер ключей-значений.

Адрес сервера плота (-raft), каталог состояния (-state_dir) и адрес сервера ключ-значение http (-api) передаются через командную строку.

Затем сохраните значение («medium») для ключа («hello»):

curl -L http://127.0.0.1:9090/ -X PUT -d '{"Key":"hello", "Value":"medium"}'

Наконец, получите сохраненный ключ:

curl -L http://127.0.0.1:9090/hello

Запуск локального кластера

Давайте приведем два дополнительных экземпляра плота.

./raft -state_dir $TMPDIR/2 -raft :8081 -api :9091 -join :8080
./raft -state_dir $TMPDIR/3 -raft :8082 -api :9092 -join :8080

Теперь можно записать пару ключ-значение в любой член кластера и аналогичным образом получить ее из любого члена.

Отказоустойчивость

Чтобы протестировать восстановление кластера, запишите значение «foo» в ключ «foo»:

curl -L http://127.0.0.1:9090/ -X PUT -d '{"Key":"foo", "Value":"foo"}'

Далее остановите узел (9092) и замените значение на «bar», чтобы проверить доступность кластера:

curl -L http://127.0.0.1:9090/ -X PUT -d '{"Key":"foo", "Value":"bar"}'
curl -L http://127.0.0.1:9090/foo

Наконец, верните узел обратно и убедитесь, что он восстанавливается с обновленным значением «bar»:

curl -L http://127.0.0.1:9092/foo

Реконфигурация кластера

Узлы можно добавлять, удалять или обновлять из работающего кластера. Удалим узел с помощью запросов к REST API.

Во-первых, перечислите все узлы плотного кластера и получите идентификатор узла.

curl -L http://127.0.0.1:9090/mgmt/nodes

Затем удалите узел с помощью запроса DELETE:

curl -L http://127.0.0.1:9090/<ID> -X DELETE

Узел отключится, как только кластер обработает этот запрос.

Подробнее о библиотеке raft можно узнать на GitHub и GoDoc. Большое спасибо за чтение. Я надеюсь, что вы нашли эту статью полезной.