← Все проекты

Кейс · Go-бэкенд

Плот: KV на Raft

Учебная реализация Raft с нуля на чистой stdlib: выборы лидера, репликация лога, коммит по кворуму, лог и term/vote на диске с fsync, снапшоты с компакцией, линеаризуемые чтения (read-index), редирект фолловера на лидера. Тестируется детерминированным симулятором сети и времени с проверкой линеаризуемости; дашборд умеет убивать лидера и рвать сеть (демо).

  • Go
  • Raft
  • Distributed systems
  • Consensus
  • stdlib only
  • HTTP/JSON
  • SSE
  • WAL + fsync
  • Snapshots
  • Linearizability
  • Deterministic simulation testing
  • Vanilla JS
  • Docker
  • GitHub Actions

01 · Как выглядит

Скриншоты

Скриншот проекта «Плот: KV на Raft» на экране 1280 пикселей
Десктоп, 1280 px
Скриншот проекта «Плот: KV на Raft» на экране телефона, 390 пикселей
Телефон, 390 px

02 · О проекте

Что это и зачем

«Плот» — кластер из трёх узлов, которые договариваются о порядке записей по алгоритму Raft. Алгоритм написан с нуля на чистой stdlib Go, без raft-библиотек и внешних зависимостей, по статье Ongaro и Ousterhout «In Search of an Understandable Consensus Algorithm».

Рядом дашборд на ванильном JS: роли узлов, term, commit и хвост лога в реальном времени, а в демо-режиме можно убить лидера или оборвать сеть и посмотреть, как кластер выбирает нового. Это учебная реализация: она не прошла Jepsen и не предназначена для продакшена.

Ограничения

Состав кластера фиксирован: ровно три узла, без динамического членства и joint consensus. Нет pre-vote и check-quorum: отрезанный фолловер раздувает term, а отрезанный лидер считает себя лидером, пока не увидит больший term (линеаризуемые чтения он при этом не отдаёт).

Нет клиентских сессий и exactly-once. Ответ 504 означает «исход неизвестен»: команда могла закоммититься позже. Повтор PUT безопасен, потому что он идемпотентен.

Проверка собственная, не Jepsen. В симуляторе идеальный диск, снапшот уходит одним сообщением и целиком держится в памяти, TLS и аутентификации нет, каждый PUT это отдельный fsync.

Одна ручная мутация тестами не ловится: коммит записей прошлых term (фигура 8 из статьи) не покрыт целенаправленным сценарием. Тесты с -race проходят только в CI (локально нет gcc), Docker-образ не собирался.

Что внутри

  • Выборы со случайным таймаутом, голос только кандидату с не отстающим логом; term и votedFor пишутся на диск до рассылки запросов
  • Репликация лога с подсказкой ConflictIndex: лидер откатывается сразу к началу конфликтующего term, а не на шаг
  • Коммит по кворуму только для записей текущего term (раздел 5.4.2 статьи); новый лидер сразу кладёт в лог no-op
  • Персистентность: WAL с CRC32 и fsync, state.json через временный файл и rename, битый хвост лога обрезается при загрузке
  • Снапшоты и компакция «поколениями» файлов: крэш на любом шаге оставляет целое старое или целое новое состояние; отставшему фолловеру уходит snapshot
  • Линеаризуемые чтения через read-index без часов и лизов; чтение с ?stale=1 локальное и может отставать
  • Ядро raft.Node без горутин и часов: время приходит через Tick, сообщения через Step, поэтому тот же код работает и в рантайме, и в симуляторе
  • Детерминированный симулятор сети и времени (потери, задержки, обрывы, крэши) с проверкой инвариантов и линеаризуемости истории (Wing и Gong)
  • Дашборд на SSE: роль, term, commit/applied, хвост лога; кнопки аварий и стоп узла работают только при SEED_DEMO=1

03 · Как запустить

Запуск одной командой

git clone <репозиторий по запросу> raft-kv
cd raft-kv
SEED_DEMO=1 go run ./cmd/raft-kv   # дашборд http://localhost:8098, узлы на 8095-8097
curl -L -X PUT localhost:8096/kv/city -d Berlin   # фолловер перенаправит на лидера

04 · Код

Репозиторий приватный

Исходники лежат в приватном репозитории. Открою доступ по запросу: напишите, и пришлю приглашение.

Запросить код