Кейс · 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 · Как выглядит
Скриншоты


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 · Код
Репозиторий приватный
Исходники лежат в приватном репозитории. Открою доступ по запросу: напишите, и пришлю приглашение.
Запросить код