Senior Go Interview Prep - Core Go: https://go.vbloher.org/docs/01-core-go/ - Механика defer в Go: https://go.vbloher.org/docs/01-core-go/defer/ - Встраивание структур и интерфейсов (Embedding): https://go.vbloher.org/docs/01-core-go/embedding/ - Ошибки в Go: error, wrapping, errors.Is/As/Join: https://go.vbloher.org/docs/01-core-go/errors/ - Дженерики в Go (1.18+): https://go.vbloher.org/docs/01-core-go/generics/ - Интерфейсы в Go: https://go.vbloher.org/docs/01-core-go/interfaces/ - Устройство map в Go: https://go.vbloher.org/docs/01-core-go/maps/ - panic / recover: механика, раскрутка стека и runtime-паники: https://go.vbloher.org/docs/01-core-go/panic-recover/ - Указатели в Go: https://go.vbloher.org/docs/01-core-go/pointers/ - Рефлексия в Go (reflect): https://go.vbloher.org/docs/01-core-go/reflection/ - Внутреннее устройство слайсов в Go: https://go.vbloher.org/docs/01-core-go/slices/ - Строки, руны и байты в Go: https://go.vbloher.org/docs/01-core-go/strings-runes-bytes/ - Система типов Go: defined types, alignment, memory layout: https://go.vbloher.org/docs/01-core-go/type-system/ - Concurrency: https://go.vbloher.org/docs/02-concurrency/ - sync/atomic: https://go.vbloher.org/docs/02-concurrency/atomic/ - Буферизованные vs небуферизованные каналы: https://go.vbloher.org/docs/02-concurrency/buffered-unbuffered/ - Канал vs Mutex: когда что выбрать: https://go.vbloher.org/docs/02-concurrency/channel-vs-mutex/ - Каналы: устройство hchan: https://go.vbloher.org/docs/02-concurrency/channels/ - Утечки горутин, дедлоки, livelock, starvation: https://go.vbloher.org/docs/02-concurrency/common-leaks-deadlocks/ - sync.Cond: https://go.vbloher.org/docs/02-concurrency/cond/ - context: https://go.vbloher.org/docs/02-concurrency/context/ - Горутины: жизненный цикл, стоимость, стек: https://go.vbloher.org/docs/02-concurrency/goroutines-lifecycle/ - sync.Mutex и sync.RWMutex: https://go.vbloher.org/docs/02-concurrency/mutex-rwmutex/ - sync.Once: https://go.vbloher.org/docs/02-concurrency/once/ - Паттерны конкурентности: https://go.vbloher.org/docs/02-concurrency/patterns/ - Race Detector (гонки данных и -race): https://go.vbloher.org/docs/02-concurrency/race-detector/ - Планировщик GMP: https://go.vbloher.org/docs/02-concurrency/scheduler-gmp/ - select: https://go.vbloher.org/docs/02-concurrency/select/ - sync.WaitGroup: https://go.vbloher.org/docs/02-concurrency/waitgroup/ - Runtime и память: https://go.vbloher.org/docs/03-runtime-memory/ - Паттерны аллокаций и снижение давления на GC: https://go.vbloher.org/docs/03-runtime-memory/allocation-patterns/ - Escape Analysis: когда переменная убегает в кучу: https://go.vbloher.org/docs/03-runtime-memory/escape-analysis/ - Сборщик мусора Go: concurrent tri-color mark-sweep: https://go.vbloher.org/docs/03-runtime-memory/gc/ - Тюнинг GC: GOGC и GOMEMLIMIT: https://go.vbloher.org/docs/03-runtime-memory/gogc-gomemlimit/ - GOMAXPROCS: параллелизм планировщика и проблема контейнеров: https://go.vbloher.org/docs/03-runtime-memory/gomaxprocs/ - Утечки горутин (goroutine leaks): https://go.vbloher.org/docs/03-runtime-memory/goroutine-leaks/ - Утечки памяти в Go (несмотря на GC): https://go.vbloher.org/docs/03-runtime-memory/memory-leaks/ - Модель памяти Go (Go Memory Model): happens-before и синхронизация: https://go.vbloher.org/docs/03-runtime-memory/memory-model/ - pprof: профилирование CPU, памяти и блокировок в Go: https://go.vbloher.org/docs/03-runtime-memory/pprof/ - Execution Tracer и runtime/trace: тайминги вместо агрегатов: https://go.vbloher.org/docs/03-runtime-memory/runtime-tracing/ - Стек vs Куча: где живут данные в Go: https://go.vbloher.org/docs/03-runtime-memory/stack-vs-heap/ - Тестирование: https://go.vbloher.org/docs/04-testing/ - testify, assert/require и golden files: https://go.vbloher.org/docs/04-testing/assertions-testify/ - Бенчмарки в Go: https://go.vbloher.org/docs/04-testing/benchmarks/ - Покрытие, -race и флаки-тесты: https://go.vbloher.org/docs/04-testing/coverage-race/ - Нативный fuzzing в Go (1.18+): https://go.vbloher.org/docs/04-testing/fuzzing/ - Интеграционные тесты, testcontainers-go, TestMain: https://go.vbloher.org/docs/04-testing/integration-testcontainers/ - Моки, стабы и тестируемость: https://go.vbloher.org/docs/04-testing/mocks/ - Table-driven тесты, subtests и параллельность: https://go.vbloher.org/docs/04-testing/table-driven/ - Backend: https://go.vbloher.org/docs/05-backend/ - Аутентификация и авторизация: AuthN/AuthZ, сессии vs токены, RBAC/ABAC, API keys, mTLS, секреты: https://go.vbloher.org/docs/05-backend/auth-authz/ - Graceful Shutdown HTTP/gRPC сервера в Go: https://go.vbloher.org/docs/05-backend/graceful-shutdown/ - gRPC: типы RPC, интерсепторы, контекст, метаданные, error model: https://go.vbloher.org/docs/05-backend/grpc/ - JWT (JSON Web Token): https://go.vbloher.org/docs/05-backend/jwt/ - Middleware-паттерн в Go: https://go.vbloher.org/docs/05-backend/middleware/ - net/http: Server, Handler, ServeMux, таймауты, Client и контекст: https://go.vbloher.org/docs/05-backend/net-http/ - OAuth2: роли, grant types, OIDC, токены и типовые ошибки: https://go.vbloher.org/docs/05-backend/oauth2/ - OpenAPI/Swagger, code generation, contract-first vs code-first, валидация: https://go.vbloher.org/docs/05-backend/openapi/ - Protocol Buffers: схемы, wire format, эволюция и совместимость: https://go.vbloher.org/docs/05-backend/protobuf/ - REST: принципы, версионирование, идемпотентность, статусы, пагинация, ошибки: https://go.vbloher.org/docs/05-backend/rest/ - Сети и протоколы: https://go.vbloher.org/docs/06-networking/ - Пулы соединений: http.Transport, БД, утечки: https://go.vbloher.org/docs/06-networking/connection-pooling/ - DNS: записи, резолвинг, кэширование, DNS в Go: https://go.vbloher.org/docs/06-networking/dns/ - Версии HTTP: 1.1, 2, 3: https://go.vbloher.org/docs/06-networking/http-versions/ - TCP/IP: модель, транспорт и что важно бэкендеру: https://go.vbloher.org/docs/06-networking/tcp-ip/ - TLS: handshake, сертификаты, mTLS, производительность: https://go.vbloher.org/docs/06-networking/tls/ - UDP и надёжность поверх UDP: https://go.vbloher.org/docs/06-networking/udp/ - WebSocket: upgrade, фреймы, масштабирование: https://go.vbloher.org/docs/06-networking/websocket/ - Базы данных: https://go.vbloher.org/docs/07-databases/ - Пул соединений к PostgreSQL в Go: database/sql, pgx, pgxpool, PgBouncer: https://go.vbloher.org/docs/07-databases/connection-pooling-pgx/ - Взаимоблокировки (Deadlocks) в PostgreSQL: https://go.vbloher.org/docs/07-databases/deadlocks/ - Индексы в PostgreSQL: https://go.vbloher.org/docs/07-databases/indexes/ - Уровни изоляции транзакций в PostgreSQL: https://go.vbloher.org/docs/07-databases/isolation-levels/ - MVCC в PostgreSQL: версии строк, видимость, VACUUM и bloat: https://go.vbloher.org/docs/07-databases/mvcc/ - Обзор NoSQL и Redis: https://go.vbloher.org/docs/07-databases/nosql-redis/ - Партиционирование таблиц в PostgreSQL: https://go.vbloher.org/docs/07-databases/partitioning/ - Архитектура PostgreSQL: https://go.vbloher.org/docs/07-databases/postgresql-architecture/ - Планирование и оптимизация запросов в PostgreSQL: https://go.vbloher.org/docs/07-databases/query-planning/ - Репликация в PostgreSQL: https://go.vbloher.org/docs/07-databases/replication/ - Шардирование (горизонтальное масштабирование): https://go.vbloher.org/docs/07-databases/sharding/ - Транзакции в PostgreSQL и Go (database/sql, pgx): https://go.vbloher.org/docs/07-databases/transactions/ - Распределённые системы: https://go.vbloher.org/docs/08-distributed-systems/ - CAP теорема: https://go.vbloher.org/docs/08-distributed-systems/cap-theorem/ - Circuit Breaker: https://go.vbloher.org/docs/08-distributed-systems/circuit-breaker/ - Консенсус и Raft: репликация состояния в присутствии отказов: https://go.vbloher.org/docs/08-distributed-systems/consensus-raft/ - Модели согласованности: https://go.vbloher.org/docs/08-distributed-systems/consistency/ - Гарантии доставки сообщений: at-most-once / at-least-once / exactly-once: https://go.vbloher.org/docs/08-distributed-systems/delivery-guarantees/ - Eventual Consistency: https://go.vbloher.org/docs/08-distributed-systems/eventual-consistency/ - Идемпотентность в распределённых системах: https://go.vbloher.org/docs/08-distributed-systems/idempotency/ - Apache Kafka: https://go.vbloher.org/docs/08-distributed-systems/kafka/ - Transactional Outbox: https://go.vbloher.org/docs/08-distributed-systems/outbox/ - RabbitMQ: AMQP 0-9-1, маршрутизация, надёжность доставки и сравнение с Kafka: https://go.vbloher.org/docs/08-distributed-systems/rabbitmq/ - Ретраи: backoff, jitter, budgets и идемпотентность: https://go.vbloher.org/docs/08-distributed-systems/retries/ - Saga Pattern: https://go.vbloher.org/docs/08-distributed-systems/saga/ - Observability: https://go.vbloher.org/docs/09-observability/ - Grafana: https://go.vbloher.org/docs/09-observability/grafana/ - Метрики: RED, USE, Golden Signals: https://go.vbloher.org/docs/09-observability/metrics/ - OpenTelemetry: https://go.vbloher.org/docs/09-observability/opentelemetry/ - Prometheus: https://go.vbloher.org/docs/09-observability/prometheus/ - SLI / SLO / SLA: https://go.vbloher.org/docs/09-observability/slo-sli/ - Структурированное логирование (slog): https://go.vbloher.org/docs/09-observability/structured-logging/ - Distributed Tracing: https://go.vbloher.org/docs/09-observability/tracing/ - System Design: https://go.vbloher.org/docs/10-system-design/ - Analytics Pipeline: https://go.vbloher.org/docs/10-system-design/analytics-pipeline/ - Chat System: https://go.vbloher.org/docs/10-system-design/chat/ - Фреймворк System Design интервью: https://go.vbloher.org/docs/10-system-design/framework/ - Notification Service: https://go.vbloher.org/docs/10-system-design/notification-service/ - Order Service: https://go.vbloher.org/docs/10-system-design/order-service/ - Payment Service: https://go.vbloher.org/docs/10-system-design/payment-service/ - Rate Limiter: https://go.vbloher.org/docs/10-system-design/rate-limiter/ - URL Shortener: https://go.vbloher.org/docs/10-system-design/url-shortener/ - DevOps: https://go.vbloher.org/docs/11-devops/ - CI/CD: пайплайны, стадии, стратегии деплоя: https://go.vbloher.org/docs/11-devops/cicd/ - Облака (AWS / GCP) для бэкендера: https://go.vbloher.org/docs/11-devops/cloud-aws-gcp/ - Docker для Go-разработчика: https://go.vbloher.org/docs/11-devops/docker/ - GitHub Actions и GitLab CI: https://go.vbloher.org/docs/11-devops/github-gitlab-ci/ - Kubernetes для Go-разработчика: https://go.vbloher.org/docs/11-devops/kubernetes/ - Terraform / Infrastructure as Code: https://go.vbloher.org/docs/11-devops/terraform/ - Алгоритмы: https://go.vbloher.org/docs/12-algorithms/ - Типовые алгоритмические задачи и паттерны: https://go.vbloher.org/docs/12-algorithms/common-problems/ - Асимптотическая сложность (Big-O): https://go.vbloher.org/docs/12-algorithms/complexity/ - Структуры данных в Go: https://go.vbloher.org/docs/12-algorithms/data-structures/ - Специфика live-coding на Go: https://go.vbloher.org/docs/12-algorithms/go-specifics/ - Behavioral: https://go.vbloher.org/docs/13-behavioral/ - Конфликты, разногласия и работа со стейкхолдерами: https://go.vbloher.org/docs/13-behavioral/conflicts/ - Как проходит senior-интервью: этапы, оценка, оффер: https://go.vbloher.org/docs/13-behavioral/interview-flow/ - Лидерство и менторство: https://go.vbloher.org/docs/13-behavioral/leadership-mentoring/ - Типовые поведенческие вопросы для Senior: https://go.vbloher.org/docs/13-behavioral/senior-questions/ > Модуль: Распределённые системы · Уровень: Senior+ ## TL;DR - **Консенсус** — задача договориться о едином значении/порядке между несколькими узлами так, чтобы решение пережило отказы части узлов. Лежит в основе **replicated state machine (RSM)**: одинаковый детерминированный лог команд -> одинаковое состояние на всех репликах. - **FLP impossibility**: в *асинхронной* сети (нет границ на задержки) невозможно гарантировать консенсус при наличии даже одного отказавшего узла, который нельзя отличить от медленного. Практика обходит это через **частичную синхронность** и **таймауты** (рандомизированные) — жертвуем гарантированной liveness, сохраняя safety. - **Raft** — алгоритм консенсуса, спроектированный как *understandable* альтернатива Paxos. Декомпозиция на три подзадачи: **leader election**, **log replication**, **safety**. - Роли: **leader / follower / candidate**. Время делится на **terms** (логические эпохи). В каждом term не более одного лидера; все записи идут только через лидера (**leader-only writes**). - Запись коммитится, когда её реплицировало **большинство (кворум N/2+1)**. Поэтому нужно **нечётное** число нод — кворум тот же, а fault tolerance не теряется при чётности. - Применяется в **etcd**, **Consul**, **CockroachDB** (Raft per range), **TiKV**. **ZooKeeper** использует ZAB — близкий по духу leader-based atomic broadcast. ## Теория ### Зачем нужен консенсус Распределённая система хочет вести себя как один надёжный узел, несмотря на отказы отдельных машин. Для этого данные/решения реплицируются. Возникает вопрос: как несколько реплик договариваются об **одном и том же значении и одном и том же порядке** операций? Формально алгоритм консенсуса должен обеспечивать: - **Agreement (соглашение):** все корректные узлы решают одно и то же значение. - **Validity (целостность):** решённое значение было кем-то предложено (не выдумано). - **Termination (завершимость, liveness):** все корректные узлы в итоге принимают решение. - **Integrity:** каждый узел решает не более одного раза. #### Replicated State Machine (RSM) Главное применение консенсуса. Идея: если у нас есть **детерминированная** state machine и все реплики применяют **один и тот же лог команд в одном и том же порядке**, то все реплики придут в идентичное состояние. ``` consensus-обеспеченный лог (total order) [c1][c2][c3][c4]... <- одинаков на всех репликах | | | | v v v v apply в детерминированную FSM -> одинаковое состояние везде ``` Таким образом, задача «реплицировать БД/KV-хранилище» сводится к задаче «согласовать порядок команд в логе» — это и решает консенсус. **Total order broadcast** (atomic broadcast) и консенсус эквивалентны по силе. ### FLP impossibility (Fischer–Lynch–Paterson, 1985) Теорема: в **полностью асинхронной** модели (нет верхней границы на задержку сообщений и скорость процессов), детерминированный алгоритм консенсуса **не может гарантировать termination**, если хотя бы один процесс может отказать (crash). Интуиция: в асинхронной сети **невозможно отличить упавший узел от очень медленного**. Узел не отвечает — он умер или просто тормозит? Если ждать его ответа — рискуем зависнуть навсегда (он мёртв). Если не ждать и пойти дальше — рискуем нарушить agreement (он жив и решит иначе). Всегда существует сценарий, в котором система бесконечно «не может определиться». Важно: FLP запрещает гарантированную **liveness (termination)**, но **не safety**. Поэтому практические алгоритмы: - Сохраняют **safety всегда** (никогда не примут противоречивые решения). - Обеспечивают **liveness при условии частичной синхронности** — то есть «рано или поздно сеть начинает вести себя достаточно стабильно» (модель partial synchrony, Dwork–Lynch–Stockmeyer). - Используют **таймауты** как практический способ заподозрить отказ, а **рандомизацию таймаутов** — чтобы разорвать симметрию (избежать вечного повторения неудачных раундов). Именно это делает Raft рандомизированными election timeouts. Вывод для собеседования: Raft/Paxos не «нарушают» FLP — они гарантируют корректность всегда и прогресс только при достаточно стабильной сети. ### Raft подробно Raft (Ongaro & Ousterhout, 2014) задумывался так, чтобы инженер мог реализовать его, действительно понимая. Декомпозиция: #### Роли узлов - **Follower** — пассивен, отвечает на RPC лидера/кандидатов. Стартовое состояние. - **Candidate** — узел, который инициировал выборы (после таймаута без сигналов от лидера). - **Leader** — единственный обработчик клиентских записей; рассылает heartbeats и реплицирует лог. #### Terms (термы) Время делится на **terms** — последовательные пронумерованные периоды. Term начинается с выборов. В каждом term — **не более одного лидера** (или ноль, если выборы провалились — split vote). Term — это **логические часы** Raft: каждый узел хранит `currentTerm`; каждое RPC несёт term. Правила: - Если узел видит term **больше** своего — обновляет свой `currentTerm` и **становится follower** (старый лидер с устаревшим term немедленно «сдаётся»). - RPC со **старым** term отвергаются. Это автоматически разруливает разделённого/«воскресшего» лидера. #### Leader election (RequestVote RPC) - Каждый follower держит **election timeout**. Если за это время не пришёл ни heartbeat, ни AppendEntries от лидера — он считает, что лидера нет. - Follower инкрементит `currentTerm`, переходит в **candidate**, голосует за себя и рассылает **RequestVote** всем. - Узел отдаёт голос, если: term кандидата не меньше его собственного, он ещё не голосовал в этом term (`votedFor`), **и лог кандидата не «отстаёт»** (election restriction, см. safety). - Кандидат, набравший **большинство голосов (кворум)**, становится лидером и сразу начинает слать heartbeats, чтобы подавить новые выборы. **Split vote** — несколько кандидатов стартовали одновременно, голоса разделились, никто не набрал большинство. Решение: **randomized election timeouts** (например, 150–300 мс случайно). Узлы таймаутят в разное время, обычно один успевает первым -> асимметрия разрывает цикл. Это прямой ответ на FLP-проблему симметрии. ``` RequestVote(term, candidateId, lastLogIndex, lastLogTerm) -> voteGranted, если: term >= currentTerm && (votedFor == null || votedFor == candidateId) && candidate's log is at least as up-to-date as receiver's ``` #### Log replication (AppendEntries RPC) - Клиент шлёт команду **лидеру** (followers перенаправляют). Лидер дописывает запись в свой лог (term + команда). - Лидер рассылает **AppendEntries** с новыми записями всем followers (этот же RPC с пустым набором записей служит **heartbeat**). - Когда запись реплицирована на **большинство** узлов, лидер двигает **commitIndex** и применяет команду к своей FSM, затем отвечает клиенту. Следующими AppendEntries он сообщает followers новый commitIndex, и они тоже применяют. ``` Лог (index: term:command): 1:1:x=1 2:1:y=2 3:2:x=5 4:2:del z ^ commitIndex (реплицировано большинством) AppendEntries(term, leaderId, prevLogIndex, prevLogTerm, entries[], leaderCommit) ``` **Consistency check:** AppendEntries несёт `prevLogIndex` + `prevLogTerm`. Follower принимает записи, только если у него в `prevLogIndex` лежит запись с тем же `prevLogTerm`. Если нет — отвергает, и лидер откатывается назад (nextIndex--) и пересылает, пока логи не сойдутся. Так лидер принудительно приводит логи followers к своему (**лог лидера — источник истины**). #### Кворум и majority - «Большинство» = `floor(N/2) + 1`. Для N=3 -> 2, для N=5 -> 3. - Любые два большинства **пересекаются** хотя бы в одном узле. Это и есть фундамент safety: новый лидер обязательно получит голос от узла, который видел последнюю закоммиченную запись -> закоммиченные данные не теряются. ### Safety properties Raft гарантирует пять свойств; ключевые для senior: 1. **Election Safety:** в одном term не более одного лидера (обеспечено правилом «один голос на term» + кворум). 2. **Leader Append-Only:** лидер никогда не перезаписывает/не удаляет записи в своём логе, только дописывает. 3. **Log Matching Property:** если две записи в логах разных узлов имеют одинаковый index и term, то (а) они содержат одну команду, и (б) **все предыдущие записи тоже идентичны**. Поддерживается consistency check'ом AppendEntries. 4. **Leader Completeness:** если запись закоммичена в term T, она присутствует в логах всех будущих лидеров (term > T). Обеспечивается **election restriction**. 5. **State Machine Safety:** если узел применил запись на index i, никакой другой узел не применит другую запись на том же index. #### Election restriction (почему голосуют только за up-to-date лог) Чтобы новый лидер не потерял закоммиченные данные, узел **отдаёт голос только кандидату, чей лог не менее «свежий»**, чем собственный. «Свежесть» сравнивается так: больше `lastLogTerm` свежее; при равном term — больше `lastLogIndex` свежее. Поскольку закоммиченная запись лежит на большинстве, а лидер набирает большинство голосов, среди голосующих обязательно есть узел с этой записью — и он не проголосует за кандидата без неё. Значит лидером может стать только узел, содержащий все закоммиченные записи. Это и есть гарантия Leader Completeness. > Тонкость (часто копают): лидер **не коммитит записи из прошлых terms по одному только факту репликации большинством**. Он коммитит запись прошлого term только косвенно — реплицировав запись **своего текущего term** поверх неё. Это закрывает классический сценарий, описанный в §5.4.2 статьи Raft, где иначе можно было бы «откоммитить и потерять» запись. #### Heartbeats Лидер периодически шлёт пустые AppendEntries (heartbeat interval << election timeout). Они: (а) подавляют выборы у followers, сбрасывая их election timer; (б) распространяют commitIndex. Если followers перестают слышать лидера -> запускаются новые выборы. ### Membership changes (joint consensus) — кратко Менять состав кластера (добавить/убрать узел) опасно: если перейти от старой конфигурации к новой «мгновенно», на короткий период могут образоваться **два непересекающихся большинства** (по старому и по новому составу) -> два лидера -> split-brain. Raft решает это **joint consensus** (двухфазно): вводится промежуточная конфигурация `C_old,new`, где решение требует большинства **и в старом, и в новом** составе одновременно. Пока активна `C_old,new`, никакое одностороннее большинство невозможно. После её фиксации переходят к `C_new`. (Альтернатива в современных реализациях — **single-server membership changes**: добавлять/убирать по одному узлу за раз, что тоже исключает разрыв большинств.) ### Paxos кратко **Paxos** (Lamport) — исторически первый доказанный алгоритм консенсуса для асинхронной сети с crash-отказами. - **Basic (single-decree) Paxos** согласовывает **одно** значение. Роли: **proposer, acceptor, learner**. Две фазы: 1. **Prepare/Promise** — proposer выбирает номер n, спрашивает большинство acceptor'ов «обещаете не принимать предложения < n?»; acceptor'ы обещают и сообщают уже принятое значение, если было. 2. **Accept/Accepted** — proposer рассылает значение (своё или ранее принятое с наибольшим номером) для номера n; при согласии большинства значение выбрано. - **Multi-Paxos** — чтобы согласовать **последовательность** значений (лог), запускать basic Paxos на каждый слот дорого. Multi-Paxos выбирает **стабильного лидера (distinguished proposer)**, который пропускает фазу Prepare для последующих записей -> остаётся фактически одна фаза на запись. Это уже очень похоже на лидер-ориентированную репликацию Raft. **Почему создали Raft:** статья Paxos печально известна сложностью для понимания и тем, что **между «basic Paxos» и работающей реплицированной системой огромный разрыв** — Multi-Paxos описан расплывчато, реализации расходятся. Raft с самого начала проектировался вокруг **понятности (understandability)**: явный сильный лидер, цельный лог (не разрозненные слоты), явные terms, декомпозиция на election/replication/safety. По силе и гарантиям Raft и Multi-Paxos эквивалентны. ### Где применяется | Система | Алгоритм | Заметки | |---|---|---| | **etcd** | Raft | Эталонная реализация; на ней стоит Kubernetes (хранилище состояния кластера). | | **Consul** | Raft | Service discovery / KV; Raft через библиотеку hashicorp/raft. | | **CockroachDB** | Raft **per range** | БД шардирована на ranges (~512 МБ), у каждого range — свой Raft-кворум. Тысячи независимых Raft-групп. | | **TiKV** (TiDB) | Raft (Multi-Raft) | Distributed KV; регион = Raft-группа, реализация на базе подхода etcd/raft. | | **ZooKeeper** | **ZAB** (не Raft) | Zookeeper Atomic Broadcast — leader-based atomic broadcast, идейно близок Raft (лидер, эпохи, кворум, total order). | | **Apache Kafka (KRaft)** | Raft | Современный Kafka заменил ZooKeeper на встроенный Raft (KRaft) для метаданных контроллера. | ### Почему нечётное число нод и кворум N/2+1 Fault tolerance: кластер из N узлов с кворумом большинства переживает отказ **`floor((N-1)/2)`** узлов (пока живо большинство, можно коммитить). | N | кворум | переживает отказов | |---|---|---| | 3 | 2 | 1 | | 4 | 3 | 1 | | 5 | 3 | 2 | | 6 | 4 | 2 | Видно: переход с 3 на 4 узла **не повышает** отказоустойчивость (всё ещё 1), но **увеличивает** размер кворума (нужно 3 вместо 2) -> больше латентность записи и выше шанс потерять кворум. То же с 5 vs 6. Поэтому берут **нечётное** число: максимум устойчивости при минимальном кворуме. Плюс нечётность исключает «ровный раскол 50/50» при сетевом разделении — большинство всегда определимо однозначно (нет ничьей). ### ASCII диаграмма переходов состояний ``` starts up / recovers | v +-------------+ discovers | | times out (no heartbeat leader or | FOLLOWER | from leader within higher term | | randomized election timeout) +------------->| |-------------------+ | +-------------+ | | ^ | | discovers | v | current leader | discovers higher +-------------+ | or higher term | term / new leader | | | | | CANDIDATE | | +-------------+ | | | | | wins +-------------+ +--------------| LEADER |<--majority---| starts new | higher term seen | | of votes | election, | (step down to +-------------+ | votes self, | follower) ^ | RequestVote | | times out +-------------+ | (election fails: | +---- split vote) ----------+ retry election, new term ``` Краткая словесная версия: - Follower -> Candidate: election timeout без сигналов лидера. - Candidate -> Leader: набрал большинство голосов. - Candidate -> Candidate: split vote / timeout -> новый term, повторные выборы. - Candidate/Leader -> Follower: увидел больший term (или нового легитимного лидера). ## Подводные камни / gotchas - **Кворум != запись на всех.** Коммит требует большинства; отстающие реплики догоняют асинхронно. Чтение с follower может вернуть устаревшие данные (нужен read-from-leader / ReadIndex / lease read для linearizable reads). - **Чётное число узлов — антипаттерн.** Не повышает fault tolerance, повышает кворум и риск. Всегда нечётное (3/5/7). - **Запись прошлого term нельзя коммитить «по большинству».** Лидер коммитит её только через репликацию записи текущего term поверх — иначе возможна потеря данных (§5.4.2). Классическая ошибка наивных реализаций. - **Split-brain при mgmt-change без joint consensus.** Мгновенная смена конфигурации может породить два большинства -> два лидера. Только joint consensus / single-server change. - **Слишком короткий election timeout -> постоянные перевыборы**, слишком длинный -> долгая недоступность после падения лидера. Heartbeat interval должен быть << election timeout (обычно в разы). - **Нерандомизированные таймауты -> вечный split vote.** Рандомизация обязательна для liveness. - **FLP -> нет гарантии прогресса при флапающей сети.** При нестабильной сети возможны бесконечные перевыборы; safety при этом сохраняется, но система может «не двигаться». - **Лог растёт бесконечно** без **снапшотов** (log compaction). Реальные реализации (etcd) делают snapshot + InstallSnapshot RPC для отставших узлов. - **Время и lease-reads.** Lease-based linearizable reads опираются на ограниченный clock drift; нарушение предположений о часах ломает корректность чтений. - **Дубликаты от клиента.** Клиентский retry после таймаута может задвоить команду; нужны идемпотентные client request id (dedup в state machine). ## Вопросы на собеседовании **В:** Что такое replicated state machine и при чём тут консенсус? **О:** RSM — это подход, где несколько реплик применяют одинаковый детерминированный лог команд в одинаковом порядке и потому приходят в идентичное состояние. Задача консенсуса здесь — согласовать сам порядок команд в логе (total order broadcast). Решив консенсус, мы автоматически реплицируем любую детерминированную систему (KV, БД). **В:** Сформулируйте FLP impossibility и как Raft с ней живёт. **О:** В полностью асинхронной сети детерминированный консенсус не может гарантировать завершимость при возможности хотя бы одного отказа, потому что упавший узел неотличим от медленного. Raft не нарушает теорему: он гарантирует safety всегда, а liveness — только при частичной синхронности. Практически это реализуется таймаутами (рандомизированными), которые дают прогресс, когда сеть стабильна. **В:** Зачем рандомизировать election timeout? **О:** Чтобы избежать split vote — ситуации, когда несколько кандидатов стартуют одновременно и делят голоса так, что никто не набирает большинство. Случайные таймауты разносят старты выборов во времени, обычно один кандидат успевает первым и выигрывает. Это разрыв симметрии — практический ответ на FLP. **В:** Как Raft гарантирует, что новый лидер не потеряет закоммиченные данные? **О:** Через election restriction: узел голосует только за кандидата, чей лог не менее свежий (по lastLogTerm, затем lastLogIndex). Закоммиченная запись лежит на большинстве, лидер набирает большинство голосов, два большинства пересекаются -> среди голосующих есть узел с этой записью, и он не выберет кандидата без неё. Значит лидером станет узел со всеми закоммиченными записями (Leader Completeness). **В:** Что такое Log Matching Property и как она поддерживается? **О:** Если записи в двух логах имеют одинаковые index и term, то они содержат одну команду и все предшествующие записи идентичны. Поддерживается consistency check в AppendEntries: RPC несёт prevLogIndex/prevLogTerm, follower принимает записи только при совпадении предыдущей; иначе лидер откатывается назад, пока логи не сойдутся, и переписывает расхождение. **В:** Почему берут нечётное число узлов? **О:** Кворум — большинство (N/2+1). Нечётное N даёт максимум отказоустойчивости при минимальном кворуме: 3 узла переживают 1 отказ, 4 — тоже только 1, но требуют кворум 3 (выше латентность, выше риск). Плюс нечётность исключает раскол 50/50 при сетевом разделении. **В:** Чем Multi-Paxos похож на Raft и почему создали Raft? **О:** Multi-Paxos вводит стабильного лидера, который пропускает фазу Prepare и фактически реплицирует лог одной фазой — это близко к лидер-репликации Raft, и по гарантиям они эквивалентны. Raft создали ради понятности: явный сильный лидер, цельный лог вместо разрозненных слотов, явные terms и чёткая декомпозиция на election/replication/safety. Paxos сложен и оставляет большой разрыв между теорией и реализацией. **В:** Можно ли коммитить запись из предыдущего term, как только она реплицирована большинством? **О:** Нет. Это небезопасно (сценарий §5.4.2): такую запись можно потерять при последующих выборах. Лидер коммитит запись прошлого term только косвенно — реплицировав поверх неё запись своего текущего term, которая, будучи закоммиченной большинством, «тянет» за собой и предыдущие по Log Matching. **В:** Как Raft меняет состав кластера без split-brain? **О:** Через joint consensus: вводится промежуточная конфигурация C_old,new, где для любого решения нужно большинство и в старом, и в новом составе одновременно. Это исключает существование двух непересекающихся большинств. После фиксации переходят к C_new. Современная альтернатива — менять по одному узлу за раз. **В:** ZooKeeper использует Raft? **О:** Нет, ZooKeeper использует ZAB (Zookeeper Atomic Broadcast). Идейно он близок Raft — лидер, эпохи (аналог terms), кворум, total order broadcast, — но это отдельный, более ранний алгоритм. Raft, etcd, Consul, CockroachDB (per range), TiKV — это лагерь Raft. ## На что копают на senior+ - **Граница safety vs liveness и связь с FLP.** Senior должен чётко разделять: safety никогда не нарушается, liveness гарантируется лишь при частичной синхронности. Это объясняет, почему «вечные перевыборы» при флапающей сети — не баг корректности. - **Тонкость коммита записей прошлого term (§5.4.2).** Знание этого нюанса — маркер, что человек реально читал статью Raft, а не пересказ. - **Linearizable reads.** Коммит ≠ свежее чтение с любой реплики. Как сделать линеаризуемые чтения: read-from-leader, ReadIndex (подтвердить лидерство кворумом перед чтением), lease reads (с допущениями о часах) и их компромиссы. - **Кворумная интуиция через пересечение большинств.** Почему именно большинство, почему два большинства пересекаются и как это даёт и election safety, и сохранность данных. - **Log compaction / snapshots.** Реальный Raft невозможен без снапшотов и InstallSnapshot для отставших/новых узлов; рост лога — практическая проблема. - **Membership changes без split-brain** (joint consensus vs single-server) и почему «просто заменить конфиг» опасно. - **Multi-Raft и шардирование.** Как CockroachDB/TiKV держат тысячи Raft-групп (per range/region), балансировка лидеров, hot range, и почему один глобальный Raft не масштабируется по записи. - **Сравнение моделей отказов.** Raft/Paxos — это crash-fault tolerance; для византийских отказов (произвольное/злонамеренное поведение) нужны другие алгоритмы (PBFT, и кворум 3f+1). Умение провести эту границу ценится. - **Связь с CAP.** Консенсус-системы — это выбор CP: при потере кворума жертвуют доступностью на запись ради консистентности. Это перекликается с поведением quorum queues в RabbitMQ.