Распределённые системы на Go: основы, практика и надёжность

Курс про проектирование и реализацию распределённых систем с использованием Go: от сетевых протоколов и RPC до консенсуса, отказоустойчивости и наблюдаемости. Вы научитесь собирать сервисы, которые устойчивы к сбоям, масштабируются и корректно работают в условиях задержек и частичных отказов.

1. Базовые принципы распределённых систем и коммуникации в Go

Базовые принципы распределённых систем и коммуникации в Go

Что такое распределённая система

Распределённая система — это набор независимых процессов (узлов), которые взаимодействуют по сети и совместно решают задачу так, что для пользователя система выглядит как единое целое.

Типичные примеры:

  • Микросервисная архитектура (несколько сервисов по HTTP/gRPC)
  • Система обработки событий (очереди, pub/sub)
  • Кластер баз данных или кэш (репликация, шардинг)
  • Ключевое отличие от монолита: части системы могут падать, тормозить, терять связь друг с другом, и это считается нормой, а не исключением.

    !Общая картина: несколько сервисов общаются по сети, и сеть вносит задержки и сбои

    Почему распределённые системы сложнее, чем кажутся

    Есть известный список заблуждений о распределённых вычислениях ("fallacies"), который полезно держать в голове:

  • Сеть надёжна
  • Задержка равна нулю
  • Пропускная способность бесконечна
  • Топология не меняется
  • Есть один администратор
  • Передача данных бесплатна
  • Сеть однородна
  • Практический вывод: любая коммуникация должна иметь план на случай проблем — таймауты, повторы, деградацию функциональности.

    Источник: Fallacies of distributed computing (Wikipedia)

    Частичные отказы: базовая модель реальности

    В распределённых системах часто случается частичный отказ: один компонент или один сетевой путь ломается, а остальные продолжают работать.

    Примеры ситуаций, которые встречаются постоянно:

  • Клиент получил таймаут, но сервер запрос обработал (вы потеряли ответ)
  • Запрос дошёл до сервера, но ответ потерялся в сети
  • Одна реплика базы данных отстаёт, другая отвечает быстро
  • Зависший сервис держит соединения и исчерпывает пул у клиентов
  • Важная привычка инженера: думать не "запрос успешен или нет", а "насколько я уверен, что произошло ровно один раз".

    Консистентность, доступность и сетевые разделения (CAP — в прикладном смысле)

    В распределённой системе сеть может "разделиться" (partition): часть узлов не видит другую часть. Это называют сетевым разделением.

    Интуитивная версия CAP:

  • Consistency (согласованность): все видят одно и то же состояние
  • Availability (доступность): система отвечает на запросы
  • Partition tolerance (устойчивость к разделениям): система продолжает работать при проблемах сети
  • При сетевом разделении приходится выбирать, что важнее в данный момент: отвечать всем (доступность) или отвечать только когда уверены в согласованности.

    Источник: CAP theorem (Wikipedia)

    Время, порядок и почему "просто сравнить timestamps" опасно

    Время в распределённых системах коварно:

  • Часы на разных машинах могут расходиться
  • Задержки сети меняются постоянно
  • "Раньше" и "позже" в разных узлах может восприниматься по-разному
  • Практические правила для начала:

  • Не полагайтесь на "идеально синхронные" часы для логики корректности
  • Используйте таймауты и дедлайны как инструмент управления ожиданием, а не как доказательство того, что "там точно ничего не произошло"
  • Проектируйте операции так, чтобы повтор запроса был безопасным (идемпотентность)
  • В Go важно знать, что пакет time поддерживает монотонную составляющую времени для измерения длительностей (это помогает корректно измерять интервалы, даже если системные часы сдвинулись).

    Источник: Package time (Go)

    Модели коммуникации между компонентами

    На практике чаще всего встречаются три модели:

  • Request/Response: синхронный запрос и ответ (HTTP, gRPC)
  • Streaming: длительное соединение и поток сообщений (HTTP/2, gRPC streaming, WebSocket)
  • Async messaging: запись сообщения в брокер и обработка позже (очереди, pub/sub)
  • Как выбирать на старте:

  • Нужен немедленный ответ пользователю — обычно request/response
  • Нужна доставка событий и развязка по времени — async messaging
  • Нужны частые обновления или большие потоки данных — streaming
  • Минимальный набор "протокольных" понятий

    Даже если вы используете высокоуровневые библиотеки, полезно понимать основу:

  • TCP: надёжный поток байт, но не "сообщения" (границы сообщений вы определяете сами)
  • HTTP: прикладной протокол поверх TCP (обычно), удобен для request/response
  • HTTP/2: мультиплексирование потоков поверх одного соединения (важно для gRPC)
  • gRPC: RPC-фреймворк на базе HTTP/2 и protobuf
  • Ссылки:

  • net/http (Go)
  • Introduction to gRPC (grpc.io)
  • gRPC-Go (GitHub)
  • Инструменты Go для распределённых систем

    Go удобен для сетевых сервисов из-за простых параллельных вычислений и сильной стандартной библиотеки.

    Goroutine как единица конкурентности

    goroutine — лёгкий поток выполнения. В сетевых сервисах типично:

  • один входящий запрос обслуживается в отдельной goroutine (например, HTTP handler)
  • фоновые задачи (очистка, ретраи, консюмеры очереди) работают в отдельных goroutine
  • Главная инженерная задача — контролировать жизненный цикл goroutine, чтобы они не утекали.

    context: дедлайны, отмена, перенос метаданных

    context.Context — стандартный способ:

  • задавать таймауты и дедлайны
  • отменять работу при уходе пользователя или остановке сервиса
  • прокидывать метаданные (например, request ID) через слои кода
  • Важно:

  • context передаётся первым аргументом в функции, которые могут блокироваться
  • нельзя хранить context внутри структур "навсегда"; его область жизни — один запрос/операция
  • Источник: context (Go)

    Таймауты по умолчанию не всегда включены

    Одна из самых частых причин инцидентов: сетевые операции без таймаута.

    Практическое правило:

  • у каждого исходящего запроса должен быть таймаут (http.Client.Timeout или context.WithTimeout)
  • Источник: http.Client (Go)

    Надёжность: базовые защитные паттерны

    Таймауты

    Таймаут — это ограничение ожидания, чтобы ресурсы (goroutine, соединения, память) не зависали бесконечно.

    Где ставить:

  • на клиенте (внешний вызов)
  • на сервере (ограничение времени обработки)
  • Повторы (retries) с ограничениями

    Повтор запроса полезен при временных сбоях, но опасен:

  • может усилить перегрузку (retry storm)
  • может привести к двойному выполнению операции
  • Практические правила:

  • повторяйте только операции, которые идемпотентны, или используйте идемпотентные ключи
  • добавляйте backoff (увеличение паузы между попытками)
  • ставьте лимит попыток
  • Идемпотентность

    Операция идемпотентна, если повтор её выполнения даёт тот же эффект, что и однократное выполнение.

    Примеры:

  • GET /resource обычно идемпотентен
  • PUT /resource/123 часто можно сделать идемпотентным
  • POST /payments обычно не идемпотентен без идемпотентного ключа
  • Circuit breaker и bulkhead (на уровне идеи)

  • Circuit breaker: если зависимый сервис часто падает, временно прекращаем к нему ходить, чтобы не тратить ресурсы впустую
  • Bulkhead: разделяем ресурсы (пулы, лимиты), чтобы проблема в одном направлении не уронила весь сервис
  • !Как таймауты, ретраи и circuit breaker влияют на поток вызовов

    Наблюдаемость: чтобы понимать, что происходит

    Минимум, который почти всегда нужен:

  • Логи: события и ошибки с контекстом
  • Метрики: RPS, latency, error rate, saturation (например, заполнение пулов)
  • Трейсинг: путь запроса через несколько сервисов
  • Стандарт де-факто для трейсинга и метрик в современном стеке — OpenTelemetry.

    Ссылки:

  • OpenTelemetry (сайт)
  • OpenTelemetry for Go (GitHub)
  • Практический приём, который пригодится уже в первых упражнениях курса: correlation/request ID.

  • генерируете request_id на входе
  • прокидываете его через context
  • пишете его в логи и передаёте в исходящие запросы
  • Так вы сможете связать события в разных компонентах.

    Минимальный пример: HTTP-сервер и клиент с дедлайном

    Ниже — маленький пример, который показывает базовую дисциплину: обработчик уважает context, а клиент ходит с таймаутом.

    Сервер

    Клиент

    Что важно заметить:

  • Клиент ограничивает время ожидания через context.WithTimeout
  • Сервер может прекратить работу, если ctx.Done() закрыт
  • Даже в этом примере видно: таймаут на клиенте не доказывает, что сервер "не успел" — он мог успеть, но ответ не дошёл
  • Как эта тема свяжется с дальнейшими материалами курса

    Эта статья задаёт фундамент, на который дальше будут опираться практики реализации:

  • проектирование API и контрактов между сервисами
  • корректные ретраи, идемпотентность и дедупликация
  • балансировка, пулы соединений и контроль нагрузки
  • консистентность данных, репликация и распределённые блокировки (когда они действительно нужны)
  • наблюдаемость и отладка распределённых инцидентов
  • 2. RPC и API: gRPC, HTTP, схемы данных и обратная совместимость

    RPC и API: gRPC, HTTP, схемы данных и обратная совместимость

    Зачем в распределённой системе думать про API как про контракт

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

    API — это внешний договор между компонентами.

    RPC — частный случай API, где взаимодействие выглядит как вызов функции у удалённого сервиса.

    Практический вывод: ваша система будет стабильнее, если вы проектируете API так, чтобы:

  • его можно было безопасно повторять (идемпотентность)
  • ошибки и таймауты были ожидаемыми и описанными
  • изменения можно было выкатывать независимо (обратная совместимость)
  • !API как контракт между сервисами и сетью

    HTTP API и RPC: в чём разница на практике

    HTTP API (обычно REST-подобный стиль)

    HTTP API строится на ресурсах и методах:

  • GET читает
  • POST создаёт
  • PUT заменяет
  • PATCH частично изменяет
  • DELETE удаляет
  • Обычно данные передаются как JSON, иногда как бинарные форматы.

    Плюсы:

  • простая отладка (curl, браузер, прокси)
  • легко интегрировать внешних клиентов
  • кэширование и инфраструктура вокруг HTTP хорошо развиты
  • Минусы:

  • слабее типизация контрактов (особенно при “свободном” JSON)
  • сложнее выразить стриминг и строгие схемы без дополнительных соглашений
  • Ссылка: net/http (Go)

    RPC и gRPC

    В RPC вы проектируете методы сервиса, например CreateOrder, GetOrder, и вызываете их как функции. В экосистеме Go самый распространённый вариант — gRPC:

  • транспорт: HTTP/2
  • сериализация: Protocol Buffers (protobuf)
  • строгие схемы, генерация кода для клиента и сервера
  • Плюсы:

  • строгая схема и типы, меньше “неожиданных” полей и форматов
  • быстрее и компактнее JSON в большинстве случаев
  • встроенные стриминги (client/server/bidirectional)
  • Минусы:

  • сложнее дебажить “вручную” без инструментов
  • для браузеров нужен gRPC-Web или прокси
  • Ссылки:

  • gRPC: Introduction
  • grpc-go (репозиторий)
  • Как выбирать: HTTP или gRPC

    Выбор редко бывает “навсегда”, но на старте полезны простые критерии.

    Когда чаще подходит HTTP

  • внешний публичный API для партнёров
  • требуется простая диагностика, совместимость со стандартными прокси
  • модель “ресурсы + CRUD” без сложных стримингов
  • Когда чаще подходит gRPC

  • много внутренних сервисов и важны единые схемы
  • высокие требования к производительности и объёму трафика
  • нужны стриминги (например, события, телеметрия, чаты)
  • хотите сильную контрактность и генерацию клиента
  • Практический совет: внутри кластера часто выгоден gRPC, а на границе (public edge) — HTTP/JSON, при необходимости через gateway.

    Схемы данных: JSON против Protobuf

    JSON: гибкость и цена этой гибкости

    JSON удобен, но “схема” часто живёт в документации и тестах.

    Типичные проблемы:

  • поле неожиданно приходит строкой вместо числа
  • разные команды трактуют null по-разному
  • незаметные изменения формата ломают клиентов в рантайме
  • Чтобы JSON был управляемым, обычно добавляют:

  • явную JSON Schema или OpenAPI
  • строгую валидацию на сервере
  • контрактные тесты
  • Ссылка: OpenAPI Specification

    Protobuf: строгий контракт и правила совместимости

    В protobuf схема выражена в .proto файле, а код генерируется.

    Ключевая идея protobuf для эволюции: у каждого поля есть номер (tag), и именно он определяет совместимость.

    Базовые правила совместимости, которые стоит запомнить:

  • Добавлять новое поле безопасно, если старые клиенты могут его игнорировать.
  • Нельзя переиспользовать номера удалённых полей. Вместо этого номера “резервируют”.
  • Нельзя менять тип поля так, чтобы нарушалась совместимость кодирования (в сомнительных случаях считайте, что нельзя).
  • Ссылки:

  • Protocol Buffers (документация)
  • Language Guide (proto3)
  • Мини-пример protobuf-схемы

    Что здесь важно:

  • id = 1 означает “поле с номером 1”. Именно номер — главный идентификатор на проводе.
  • reserved 4 защищает вас от случайного повторного использования номера.
  • Ошибки как часть API: коды, детали и ретраи

    HTTP: статус-коды и тело ошибки

    HTTP даёт базовую семантику через статус-коды:

  • 400 ошибка клиента (валидация, неверные параметры)
  • 401/403 аутентификация/доступ
  • 404 не найдено
  • 409 конфликт (например, версия/конкурентное обновление)
  • 429 лимиты
  • 5xx ошибка сервера
  • Ссылка: HTTP response status codes (MDN)

    Хорошая практика: стандартизировать JSON ошибки, чтобы клиенты могли отличать типы проблем.

    Пример структуры (идея, не стандарт):

    gRPC: status codes и детали

    gRPC возвращает статус (например, INVALID_ARGUMENT, NOT_FOUND, UNAVAILABLE) и может передавать дополнительные детали.

    Ссылка: gRPC status codes

    Практическая связка с надёжностью из прошлой статьи:

  • UNAVAILABLE часто означает временную проблему, и клиент может применить ретрай с backoff.
  • INVALID_ARGUMENT ретраить бессмысленно: ошибка в запросе.
  • Таймаут (DEADLINE_EXCEEDED) не доказывает, что операция “не произошла” на сервере.
  • Таймауты и дедлайны: как они проходят через API

    HTTP в Go

    Два минимально важных уровня:

  • дедлайн на запрос: http.NewRequestWithContext(ctx, ...)
  • таймаут клиента: http.Client.Timeout
  • Ссылка: http.Client (Go)

    gRPC в Go

    gRPC использует дедлайны из context.Context. Клиент должен передавать context.WithTimeout, а сервер обязан уважать отмену (например, прерывать работу, если ctx.Done() закрыт).

    Ссылка: context (Go)

    Важный дизайн-вывод: дедлайны должны быть частью контракта. Если метод иногда работает 30 секунд, это нужно либо менять, либо оформлять как асинхронную операцию (job), либо как стриминг прогресса.

    Идемпотентность и ключи идемпотентности

    Из прошлой статьи: повторы неизбежны, но опасны. Чтобы повторы были безопаснее, API часто делают идемпотентным.

    Примеры

  • GET /orders/{id} идемпотентен естественно
  • PUT /orders/{id} можно сделать идемпотентным (состояние “как в запросе”)
  • POST /payments обычно не идемпотентен без доп. механизма
  • Идемпотентный ключ

    Для “создающих” операций распространён паттерн idempotency key:

  • Клиент генерирует уникальный ключ (например, UUID) для операции.
  • Сервер сохраняет результат выполнения по этому ключу.
  • Повтор с тем же ключом возвращает тот же результат, без повторного эффекта.
  • Это резко снижает риск двойных списаний/созданий при ретраях и таймаутах.

    Версионирование API: что реально работает

    Есть две цели, которые конфликтуют:

  • быстро развивать API
  • не ломать клиентов
  • Стратегии версионирования HTTP

    На практике чаще встречаются:

  • версия в пути: /v1/orders (простая, заметная)
  • версия в заголовке: Accept: application/vnd.company.v1+json (гибче, но сложнее)
  • Важно: версию обычно повышают, когда нельзя сохранить обратную совместимость.

    Версионирование в gRPC

    Частый подход — версия в package:

  • package orders.v1;
  • package orders.v2;
  • И параллельная поддержка нескольких версий сервиса.

    Семантическое версионирование

    Если вы публикуете SDK или библиотеку клиента, полезно следовать SemVer:

  • MAJOR — ломающие изменения
  • MINOR — добавления без ломания
  • PATCH — исправления
  • Ссылка: Semantic Versioning

    Обратная совместимость: типовые “можно” и “нельзя”

    Для HTTP/JSON

    Обычно безопасно:

  • добавлять новые поля в JSON ответы
  • добавлять новые эндпоинты
  • добавлять новые необязательные query-параметры
  • Обычно опасно:

  • переименовывать поля
  • менять тип поля (число на строку)
  • менять семантику существующего поля
  • удалять поля, которые клиенты могли использовать
  • Правило “выживания”: будьте консервативны в том, что отправляете, и либеральны в том, что принимаете, но не доводите “либеральность” до игнорирования ошибок валидации.

    Для protobuf

    Обычно безопасно:

  • добавлять новые поля с новыми номерами
  • добавлять новые RPC методы (клиенты их просто не вызывают)
  • Обычно опасно:

  • переиспользовать номера удалённых полей
  • менять тип/repeated/map без понимания совместимости кодирования
  • менять смысл поля, оставляя номер прежним
  • Дизайн RPC-методов: имена, границы и “чаты” вместо CRUD

    Если вы проектируете gRPC-API, частая ошибка — механически копировать CRUD. Иногда лучше выражать действие.

    Примеры:

  • вместо UpdateOrder сделать CancelOrder (явная бизнес-операция)
  • вместо “синхронно посчитать отчёт 30 секунд” сделать StartReport и GetReportStatus
  • Смысл: распределённая система любит короткие операции, ясные ошибки и понятные ретраи.

    Streaming в gRPC: где он действительно полезен

    gRPC поддерживает 4 формы:

  • unary: один запрос — один ответ
  • server streaming: один запрос — поток ответов
  • client streaming: поток запросов — один ответ
  • bidirectional streaming: поток туда и обратно
  • Стриминг полезен, когда:

  • данные большие и лучше отдавать частями
  • есть поток событий (подписка)
  • нужен интерактивный протокол
  • Но стриминг требует дисциплины:

  • лимиты (чтобы не держать бесконечные соединения без контроля)
  • heartbeats/keepalive на инфраструктурном уровне
  • ясная политика реконнекта и возобновления
  • Минимальные примеры на Go: HTTP и gRPC как клиентский контракт

    HTTP: обязательный таймаут и request id

    gRPC: дедлайн на вызов

    Что важно унести дальше по курсу

  • API — это контракт, который должен учитывать таймауты, ретраи и частичные отказы.
  • HTTP проще как публичная поверхность, gRPC сильнее для внутренних строго типизированных взаимодействий.
  • Схема данных — часть надёжности. Protobuf даёт строгие правила эволюции, но требует дисциплины с номерами полей.
  • Обратная совместимость — не “документация”, а набор практик: безопасные изменения, версионирование, идемпотентность.
  • В следующих материалах эти идеи будут использоваться для более “приземлённых” задач: ретраи и дедупликация, лимиты и пулы, балансировка, а также наблюдаемость (как понять, что контракт реально выполняется в продакшене).

    3. Согласованность, репликация и консенсус: Raft на практике

    Согласованность, репликация и консенсус: Raft на практике

    Как эта тема связана с предыдущими статьями

    В первых двух статьях курса мы закрепили две опоры:

  • В распределённой системе сеть ненадёжна, таймауты и повторы неизбежны.
  • API — это контракт, который должен учитывать частичные отказы, идемпотентность и обратную совместимость.
  • Теперь добавляем третий слой: данные и состояние. Как только одно и то же состояние существует в нескольких местах (реплики), появляется вопрос: какое состояние считать истинным и кто имеет право его менять.

    Эта статья отвечает на вопросы:

  • что такое согласованность при репликации
  • зачем нужен консенсус
  • как Raft обеспечивает согласованную репликацию
  • как подойти к Raft практически в Go: компоненты, таймауты, хранение, снапшоты, интеграция
  • Базовые определения: репликация, согласованность, консенсус

    Репликация

    Репликация — хранение одного и того же логического состояния на нескольких узлах.

    Зачем она нужна:

  • отказоустойчивость (один узел упал — система продолжает работать)
  • масштабирование чтений (можно читать с разных реплик)
  • геораспределение (реплики ближе к пользователю)
  • Согласованность

    Согласованность в прикладном смысле отвечает на вопрос: что увидит клиент, если читает данные с разных узлов в разные моменты времени.

    В реальных системах встречаются разные ожидания:

  • сильная согласованность: после успешной записи последующие чтения видят эту запись
  • слабее (eventual consistency): после записи какое-то время разные реплики могут отвечать по-разному, но со временем сходятся
  • Важно не путать:

  • согласованность данных
  • доступность (будет ли ответ вообще)
  • корректность протокола при сетевых разделениях
  • Это напрямую связано с интуитивным CAP из первой статьи: при проблемах сети часто приходится выбирать между тем, чтобы отвечать всегда и тем, чтобы отвечать только когда уверены в согласованном состоянии.

    Консенсус

    Консенсус — класс задач, где несколько узлов должны договориться об одном результате, несмотря на сбои.

    В контексте репликации самый практичный вариант консенсуса — договориться об одном порядке операций (например, порядок команд изменения состояния). Если все применят команды в одинаковом порядке к одинаковому начальному состоянию, они придут к одному результату.

    Эта идея называется replicated state machine: реплицированная машина состояний.

    Почему просто “сделаем несколько реплик” не работает

    Представьте, что у вас есть 3 реплики ключ-значение, и клиент делает Set(balance=100).

    Если репликация устроена наивно, вы быстро встретите проблемы:

  • две реплики приняли разные записи одновременно
  • лидер недоступен, и две стороны сетевого разделения продолжают принимать записи
  • клиент получил таймаут, повторил запрос, и операция выполнилась дважды
  • Из прошлых статей мы уже знаем причину: таймаут не доказывает, что операция не произошла. Если система не контролирует единственный порядок изменений, реплики расходятся.

    Рациональный подход: выбрать протокол, который гарантирует единый порядок подтверждённых изменений. Один из самых популярных протоколов для этого — Raft.

    Источник: In Search of an Understandable Consensus Algorithm (Raft)

    Quorum и большинство: базовая идея “безопасного подтверждения”

    В большинстве протоколов консенсуса используется кворум — подмножество узлов, достаточное для принятия решения.

    В Raft используется большинство.

    Если в кластере узлов, то размер большинства:

    Где:

  • — общее число узлов
  • — округление вниз
  • — минимальное количество узлов, которое должно согласиться
  • Практический смысл: любые два большинства пересекаются хотя бы одним узлом. Это пересечение помогает не “подтвердить” два разных решения одновременно.

    Raft: что именно он гарантирует

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

  • в каждый момент времени был выбран лидер (в нормальном режиме)
  • лидер принимал команды от клиентов и реплицировал их на последователей
  • команда считается подтверждённой (committed), когда её приняло большинство
  • все узлы применяют подтверждённые команды в одном и том же порядке
  • Raft обычно используют как основу для:

  • метаданных кластера
  • распределённых блокировок (если они действительно нужны)
  • конфигурации, лидершипа
  • согласованного KV-хранилища (например, etcd)
  • !Общая картина: клиент пишет в лидера, лидер реплицирует записи на остальные узлы

    Модель Raft: роли, термы, лог

    Роли

    Узел Raft всегда в одной из ролей:

  • Follower: пассивно принимает команды от лидера
  • Candidate: пытается стать лидером (во время выборов)
  • Leader: принимает команды, реплицирует лог, управляет подтверждением
  • Термы

    Term — монотонно растущий номер “эпохи” выборов лидера. Он нужен, чтобы узлы могли понять, что информация устарела.

    Правило на уровне интуиции:

  • узел доверяет сообщениям с большим term больше, чем со старым
  • если узел видит term больше своего, он “обновляет знание” и становится follower
  • Лог

    Raft реплицирует не состояние целиком, а лог команд.

    Каждая запись лога имеет:

  • индекс (позиция в логе)
  • term, в котором запись была добавлена
  • команду для state machine (бизнес-операцию)
  • После подтверждения записи применяются к state machine (вашей логике), и так получается итоговое состояние.

    Выбор лидера: как Raft избегает “двух лидеров”

    Выборы запускаются, когда follower долго не получает heartbeat от лидера.

    Механика высокого уровня:

  • follower не видит лидера в течение election timeout
  • он становится candidate, увеличивает term и голосует за себя
  • рассылает RequestVote другим узлам
  • если получает большинство голосов — становится leader
  • лидер начинает слать AppendEntries как heartbeat, чтобы остальные не стартовали выборы
  • Деталь, которая важна на практике: таймауты должны быть рандомизированы. Иначе узлы будут часто стартовать выборы одновременно.

    !Почему рандомизация election timeout снижает вероятность split vote

    Репликация лога: AppendEntries, подтверждение и применение

    Лидер принимает команду от клиента и превращает её в запись лога.

    Дальше:

  • лидер отправляет AppendEntries followers
  • follower проверяет, что лог “стыкуется” с лидером (по предыдущему индексу и term)
  • если стыковка не проходит, follower отклоняет, лидер “откатывается” назад и пробует догнать follower правильной историей
  • когда запись находится на большинстве узлов, лидер считает её подтверждённой
  • лидер сообщает commitIndex followers (в следующих AppendEntries)
  • каждый узел применяет подтверждённые записи к state machine строго по порядку индексов
  • Ключевой практический вывод: узел не должен применять неподтверждённые записи (иначе можно показать клиенту состояние, которое позже откатится).

    Безопасность Raft: почему подтверждённые записи “не пропадают”

    В распределённых системах опасная ситуация выглядит так:

  • лидер подтвердил запись клиенту
  • лидер умер
  • новый лидер “забыл” эту запись
  • Raft предотвращает это набором ограничений. Самое важное, на уровне инженерной интуиции:

  • лидер подтверждает запись только после большинства
  • новый лидер может быть выбран только если его лог достаточно свежий
  • В Raft это выражено через правило “log up-to-date” при голосовании: follower отдаёт голос кандидату только если у кандидата лог не старее.

    Источник: Raft paper

    Чтения в Raft: почему “читать с любой реплики” опасно

    Если вы читаете с follower, вы можете получить устаревшие данные, потому что follower может отставать.

    Чтобы получить сильные чтения, типичные стратегии такие:

  • читать только с лидера
  • использовать механизм, который подтверждает актуальность лидера относительно большинства (например, ReadIndex в некоторых реализациях)
  • Практический вывод для дизайна API:

  • если вам нужны строгие гарантии “прочитал то, что только что записал”, документируйте это и обеспечивайте через лидера
  • если допускается устаревание, можно разгружать лидера чтениями с followers, но это уже другой контракт
  • Снапшоты и компакция: почему лог не может расти бесконечно

    Лог со временем становится большим. Держать бесконечный лог дорого:

  • дольше старт и восстановление
  • больше диска
  • дольше догонять новую реплику
  • Поэтому практический Raft делает:

  • snapshot: периодически сохраняет снимок состояния state machine на некотором индексе
  • log compaction: удаляет старые записи лога до snapshot-индекса
  • При догоне сильно отставшей реплики лидер может отправить snapshot вместо бесконечной цепочки AppendEntries.

    Raft в Go на практике: как это обычно выглядит в коде

    Вы редко пишете Raft “с нуля” в продакшене. В Go наиболее распространённый подход — использовать готовую реализацию Raft как библиотеку и поверх неё строить:

  • транспорт (как узлы общаются по сети)
  • устойчивое хранилище лога и состояния
  • state machine (ваша бизнес-логика)
  • API для клиентов (HTTP/gRPC)
  • Две популярные реализации:

  • etcd Raft library
  • HashiCorp Raft
  • Ниже — практическая “карта компонентов”, которую полезно держать в голове.

    Компоненты узла Raft

  • Raft core: машина состояний протокола (election, replication, commit)
  • Storage:
  • - persistent: term, vote, entries (на диске) - volatile: то, что можно восстановить
  • Transport: доставка сообщений RequestVote и AppendEntries
  • Apply loop: применение подтверждённых записей к вашей state machine
  • Snapshotter: создание и установка снапшотов
  • Минимальный каркас: цикл обработки Ready в etcd/raft

    Ниже пример структуры кода (упрощённый), показывающий ключевую идею: библиотека Raft выдаёт порции работы, а вы обязаны правильно их сохранить/отправить/применить.

    Что важно в этом каркасе:

  • порядок действий имеет значение: сначала сохранение на диск, потом сеть, потом apply
  • CommittedEntries — это то, что можно применять (и обычно именно это можно отвечать клиенту как “успешно”)
  • этот цикл должен корректно завершаться по ctx.Done() (связь с дисциплиной context из первой статьи)
  • Документация библиотеки: Package raft (etcd)

    Как связать Raft и ваш API: где живёт идемпотентность

    Raft гарантирует порядок подтверждённых команд, но не решает за вас проблему “клиент повторил запрос после таймаута”. Это слой API/бизнес-логики из предыдущих статей.

    Типичный практический подход:

  • Клиент передаёт idempotency_key (или request_id) для команд изменения состояния.
  • Команда, которая кладётся в Raft-лог, включает этот ключ.
  • State machine хранит таблицу “какие ключи уже применялись” и возвращает прежний результат при повторе.
  • Так вы соединяете:

  • повторы и таймауты (уровень коммуникации)
  • строгий порядок команд (уровень консенсуса)
  • отсутствие двойного эффекта (уровень state machine)
  • Таймауты и параметры Raft: практические ориентиры

    Raft чувствителен к настройкам времени.

    Heartbeat и election timeout

    Обычно:

  • heartbeat interval делают небольшим
  • election timeout делают заметно больше и рандомизируют
  • Важно учитывать реальность продакшена:

  • паузы GC
  • stop-the-world эффекты
  • кратковременные просадки сети
  • нагрузка на диск (fsync)
  • Плохая настройка даёт:

  • постоянные перевыборы лидера
  • резкие скачки latency
  • “дребезг” кластера при нагрузке
  • Отказы и как Raft ведёт себя в реальности

    Падение лидера

  • followers перестают получать heartbeat
  • один из них выигрывает выборы
  • система продолжает принимать команды (после failover задержки)
  • Сетевое разделение

  • часть с большинством может выбрать лидера и продолжать подтверждать команды
  • часть без большинства не сможет подтверждать новые команды (иначе нарушилась бы безопасность)
  • Практический контракт для клиентов: при разделениях возможны ошибки/таймауты, но подтверждённые записи не должны “откатиться”.

    Медленный follower

  • лидер продолжает работу, не ожидая полного догоняющего
  • follower догоняется через AppendEntries или snapshot
  • Где Raft используют, а где лучше не надо

    Raft полезен, когда вам нужно:

  • сильная согласованность для небольшой по объёму, но критичной части состояния
  • понятный и практичный механизм лидерства и подтверждения
  • Raft может быть плохим выбором, когда:

  • нужно хранить огромные объёмы данных и масштабировать запись горизонтально (обычно выбирают шардинг и другие подходы)
  • допустима eventual consistency и важнее доступность и масштабирование
  • Частый дизайн: Raft используют для метаданных и управления, а сами большие данные хранят в системах, где другая модель согласованности.

    Итоги

  • Репликация без протокола консенсуса быстро приводит к расхождению реплик.
  • Консенсус для репликации чаще всего означает “согласовать порядок команд”.
  • Raft делает это через лидера, термы, репликацию лога и подтверждение большинством.
  • В Go Raft обычно берут готовой библиотекой (etcd/raft или HashiCorp Raft) и аккуратно строят вокруг неё транспорт, storage, apply loop, снапшоты.
  • Raft не отменяет требования из предыдущих статей: таймауты, ретраи, идемпотентность и контракт ошибок всё ещё обязательны.
  • 4. Надёжность: таймауты, ретраи, идемпотентность и шаблоны устойчивости

    Надёжность: таймауты, ретраи, идемпотентность и шаблоны устойчивости

    Как это связано с предыдущими статьями курса

    В первой статье мы зафиксировали базовую реальность распределённых систем: сеть ненадёжна, задержки плавают, частичные отказы нормальны, а таймаут не доказывает, что операция не произошла.

    Во второй статье мы смотрели на API как на контракт: какие ошибки возможны, какие операции можно повторять, как обеспечивать обратную совместимость.

    В третьей статье (про Raft) мы добавили слой согласованного состояния: консенсус помогает упорядочить команды, но сам по себе не решает вопросы таймаутов, повторов и двойных эффектов на границе API.

    Эта статья про прикладную надёжность: как проектировать и реализовывать вызовы между сервисами на Go так, чтобы система была устойчивой к сбоям.

    Что именно “ломается” при сетевом вызове

    Даже у простого request/response есть несколько независимых точек отказа:

  • DNS может отвечать медленно или ошибкой
  • TCP соединение может не установиться
  • TLS handshake может зависнуть
  • сервер может принять запрос, но клиент не получить ответ
  • сервер может отвечать медленно из-за очереди, GC, диска или зависимого сервиса
  • Главная практическая мысль: надёжность строится вокруг ограничения ожиданий и контроля повторов, а не вокруг надежды на “успешные ответы”.

    !Где именно может сломаться один сетевой вызов

    Таймауты и дедлайны: фундамент дисциплины

    Таймаут нужен не для “ускорения”, а чтобы ограничить расход ресурсов:

  • goroutine не ждёт бесконечно
  • соединения не зависают в пуле
  • очередь запросов не раздувается до OOM
  • система быстрее переходит в деградацию вместо полного падения
  • Таймауты в Go HTTP: что важно настроить

    Есть два уровня:

  • дедлайн на конкретный запрос через context.Context
  • глобальные сетевые таймауты в http.Client и http.Transport
  • Минимально безопасный подход для исходящих HTTP вызовов:

    Что здесь принципиально:

  • context.WithTimeout задаёт общий дедлайн операции с точки зрения вашего бизнес-контракта
  • таймауты в Transport страхуют от “вечного ожидания” на отдельных стадиях
  • Документация: http.Client, http.Transport, context.

    Таймауты на сервере: защита от “медленных клиентов” и перегруза

    На HTTP-сервере важны таймауты уровня протокола:

  • ReadHeaderTimeout защищает от медленной отправки заголовков
  • ReadTimeout и WriteTimeout ограничивают время чтения/записи
  • IdleTimeout ограничивает простаивающие keep-alive соединения
  • Документация: http.Server.

    Внутри handler’ов всегда ориентируйтесь на r.Context() и корректно прекращайте работу при отмене.

    Дедлайны в gRPC

    gRPC в Go использует дедлайны из context.Context.

  • клиент задаёт context.WithTimeout
  • сервер должен уважать ctx.Done()
  • Документация: grpc-go (репозиторий).

    Ретраи: как повторять безопасно и не устроить катастрофу

    Ретрай полезен только если причина сбоя временная. Если причина постоянная (невалидный запрос, нет прав), ретрай лишь увеличит нагрузку.

    Какие ошибки обычно ретраят

    В инженерной практике чаще всего ретраят:

  • сетевые ошибки установления соединения
  • временные 5xx от сервера (не всегда)
  • 429 (rate limit) с учётом Retry-After, если он есть
  • в gRPC — статусы UNAVAILABLE и иногда DEADLINE_EXCEEDED (осторожно)
  • Важно: если вы получили таймаут, вы не знаете, выполнилась ли операция на сервере. Поэтому ретраи особенно опасны для неидемпотентных операций.

    Exponential backoff и jitter

    Если повторять “сразу”, вы рискуете получить retry storm: все клиенты синхронно долбят сервис и добивают его.

    Базовый паттерн: экспоненциальная задержка с ограничением сверху.

    Одна из удобных формул:

    Где:

  • — задержка перед попыткой номер
  • — базовая задержка (например, 50–100 мс)
  • — экспоненциальный рост задержки
  • — максимальная задержка (например, 1–2 секунды)
  • Почти всегда добавляют jitter (случайность), чтобы клиенты “рассинхронизировались”.

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

    Retry budget: защита от бесконечной “самоатаки”

    Даже корректные ретраи увеличивают трафик. Полезная дисциплина: бюджет ретраев.

    Простая идея:

  • разрешайте ретраи только для небольшого процента запросов
  • если сервис уже в ошибках, сокращайте ретраи ещё сильнее
  • Это снижает шанс, что при деградации зависимого сервиса вы добьёте и свой.

    Минимальный ретрай-обёртка на Go

    Пример показывает принцип, а не “идеальную библиотеку”: ограничение попыток, backoff, уважение ctx.

    Что обязательно продумать в реальном сервисе:

  • какие ошибки считаются retryable
  • какие методы вообще можно ретраить по контракту
  • где хранить метки попыток (логи, метрики, трейсы)
  • Идемпотентность: защита от двойного эффекта

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

    В распределённых системах это критично из-за ситуации:

  • клиент не получил ответ (таймаут, разрыв)
  • клиент повторил запрос
  • сервер выполнил действие дважды
  • Идемпотентные и неидемпотентные операции

    Типичная (но не абсолютная) классификация:

  • GET обычно идемпотентен
  • PUT можно сделать идемпотентным, если “состояние равно телу запроса”
  • POST часто неидемпотентен, если создаёт новую сущность или списывает деньги
  • Ключ идемпотентности (idempotency key)

    Практический паттерн для “создающих” операций:

  • Клиент генерирует уникальный ключ (например, UUID) для операции.
  • Клиент передаёт ключ в запросе (HTTP заголовок или поле protobuf).
  • Сервер сохраняет результат выполнения по этому ключу.
  • Повтор запроса с тем же ключом возвращает тот же результат, не выполняя эффект повторно.
  • Важно определить границы:

  • область уникальности: ключ уникален в рамках клиента, аккаунта или всего сервиса
  • время жизни: сколько хранить запись дедупликации (часто TTL)
  • Официальный разбор подхода широко известен в платёжных API: [Stripe: Idempotent Requests.

    Дедупликация на стороне state machine (связь с Raft)

    Если вы пишете команду в согласованный лог (например, через Raft), то часто делают так:

  • команда включает idempotency_key
  • state machine хранит “уже применённые ключи” и результат
  • повтор команды возвращает сохранённый результат
  • Это соединяет два слоя курса:

  • Raft гарантирует единый порядок подтверждённых команд
  • идемпотентность гарантирует отсутствие двойного эффекта при повторной доставке/выполнении
  • Шаблоны устойчивости: как не падать каскадом

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

    Ниже — самые прикладные шаблоны.

    Circuit breaker

    Circuit breaker временно прекращает вызовы к зависимому сервису, если тот систематически отвечает ошибками или таймаутами.

    Цели:

  • не тратить ресурсы на заведомо провальные попытки
  • дать зависимому сервису “восстановиться”
  • быстрее возвращать понятную ошибку клиенту
  • Классическая модель состояний:

  • closed: работаем обычно
  • open: быстро отказываем без реального вызова
  • half-open: пробуем ограниченное число запросов, чтобы проверить восстановление
  • Описание паттерна: Martin Fowler: Circuit Breaker.

    Go-библиотека, которую часто используют: sony/gobreaker.

    Bulkhead

    Bulkhead означает разделение ресурсов, чтобы проблема в одном направлении не “съела” всё:

  • отдельные пулы соединений к разным зависимостям
  • отдельные worker pool для разных типов задач
  • лимиты параллелизма на endpoint или клиента
  • Описание идеи: Martin Fowler: Bulkheads.

    Практический инструмент в Go для ограничения параллелизма: x/sync/semaphore.

    Rate limiting и load shedding

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

  • rate limiting ограничивает входной поток
  • load shedding возвращает быстрый отказ, когда ресурсы на пределе
  • Типичный строительный блок в Go: x/time/rate.

    Deadline propagation: дедлайн должен проходить через всю цепочку

    Если пользовательский запрос имеет бюджет времени 800 мс, то внутренние вызовы не должны “жить” по 5 секунд каждый.

    Практика:

  • дедлайн создаётся на входе (edge)
  • далее он передаётся в context во все исходящие вызовы
  • каждый слой может выделять под-бюджет, но не расширять общий дедлайн
  • Это напрямую продолжает дисциплину context из первой статьи.

    Наблюдаемость для надёжности: что измерять

    Чтобы управлять устойчивостью, нужно видеть её в цифрах.

    Минимальный набор сигналов:

  • доля запросов, завершившихся таймаутом
  • число ретраев и распределение по попыткам
  • доля запросов, остановленных circuit breaker’ом
  • текущие лимиты и насыщение (очередь, семафор, пул соединений)
  • Практика для трейсинга: добавляйте атрибут попытки, например retry_attempt=2, чтобы в трейсе было видно, где вы тратите время.

    Про базовые подходы к SLI/SLO и надёжности: Google SRE Book.

    Практические правила по умолчанию

    Ниже — консервативные правила, которые помогают избежать самых частых инцидентов.

    | Проблема | Риск | Базовая защита | |---|---|---| | Нет таймаута на исходящий вызов | утечки goroutine, забитые пулы | context.WithTimeout + таймауты Transport | | Агрессивные ретраи | retry storm, усиление перегруза | backoff + jitter + лимит попыток | | Ретраи неидемпотентных операций | двойной эффект | idempotency key + дедупликация | | Зависимый сервис “умирает медленно” | каскадное падение | circuit breaker + ограничение параллелизма | | Перегруз очередями | рост latency и OOM | rate limiting + load shedding |

    Итог

  • Таймауты ограничивают ожидания и ресурсы, должны быть в каждом сетевом вызове.
  • Ретраи допустимы только для временных ошибок и только с backoff+jitter, иначе они усиливают сбой.
  • Идемпотентность превращает неизбежные повторы в безопасный механизм, особенно для “создающих” операций.
  • Circuit breaker, bulkhead и лимиты защищают систему от каскадных отказов.
  • Дальше эти принципы обычно закрепляют практикой: стандартизируют клиентские обёртки, вводят единые политики ретраев/таймаутов, добавляют метрики и трейсинг, и проверяют поведение в сбоях с помощью нагрузочного и хаос-тестирования.

    5. Наблюдаемость и эксплуатация: метрики, логи, трейсинг и деплой

    Наблюдаемость и эксплуатация: метрики, логи, трейсинг и деплой

    Как эта тема связана с предыдущими статьями

    В прошлых статьях курса мы разобрали, почему распределённые системы ломаются частично, как проектировать API-контракты, что даёт консенсус (Raft) и как повышать надёжность через таймауты, ретраи, идемпотентность и шаблоны устойчивости.

    Наблюдаемость и эксплуатация отвечают на другой практический вопрос: как понять, что система работает правильно в реальности, и как безопасно её менять.

    Если коротко:

  • Без наблюдаемости вы не отличите ошибку клиента от сбоя сети и от деградации зависимости.
  • Без дисциплины деплоя вы легко сломаете обратную совместимость и превратите нормальные ретраи в лавину.
  • !Как логи, метрики и трейсы дополняют друг друга на одном запросе

    Что такое наблюдаемость и чем она отличается от мониторинга

    Мониторинг обычно отвечает на вопрос: есть ли проблема прямо сейчас (алерты, пороги).

    Наблюдаемость шире: способность по внешним сигналам понять, что происходит внутри, особенно когда заранее неизвестно, какая именно поломка случится.

    Практический набор сигналов почти везде один и тот же:

  • Метрики: агрегированные числа во времени (RPS, ошибки, latency, очереди, насыщение).
  • Логи: события и контекст (кто, что, где, почему).
  • Трейсинг: путь одного запроса через несколько компонентов.
  • Полезный ориентир для того, что измерять, — «golden signals» из SRE-подхода: latency, traffic, errors, saturation. Источник: Site Reliability Engineering book.

    Корреляция: request_id и trace_id как «клей» распределённой отладки

    В распределённой системе почти никогда не хватает одного источника правды.

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

  • request_id — ваш собственный идентификатор запроса (часто кладут в HTTP заголовок X-Request-Id).
  • trace_id — идентификатор трассы в системах распределённого трейсинга.
  • Практическое правило: если вы видите ошибку в трейсе, вы должны уметь быстро найти соответствующие логи по trace_id или request_id.

    Логи: структурированные события вместо «текста ради текста»

    Что такое структурированные логи

    Структурированный лог — это событие с полями (ключ-значение), а не просто строка.

    Это даёт:

  • фильтрацию по полям (request_id, user_id, status, error_kind)
  • нормальную агрегацию (например, топ ошибок по error_code)
  • связь с трейсингом (trace_id, span_id)
  • В Go стандартный вариант для структурного логирования — пакет log/slog. Документация: log/slog.

    Минимальный набор полей, который окупается почти всегда

    Если вы не знаете, с чего начать, добавьте как базовый стандарт:

  • service и version (чтобы видеть, какая версия ведёт себя плохо)
  • request_id и/или trace_id
  • method, path (или имя RPC метода)
  • status (HTTP статус или gRPC code)
  • latency_ms
  • error и классификацию ошибки (например, error_kind=timeout|validation|dependency)
  • Пример: middleware для request_id и логирования в net/http

    Что важно с точки зрения прошлых статей:

  • request_id помогает разбирать таймауты и ретраи, потому что один пользовательский запрос может породить несколько попыток.
  • поля status и latency_ms позволяют позже построить метрики и алерты даже на основе логов, если метрик пока нет.
  • Частые ошибки логирования

  • Писать слишком много одинаковых логов на каждый запрос без пользы.
  • Логировать секреты (токены, пароли, номера карт).
  • Пытаться «лечить» отсутствие метрик огромным объёмом логов.
  • Метрики: измеряем поведение системы числами

    Чем метрики полезны именно в распределённых системах

    Метрики отвечают на вопросы масштаба:

  • сколько запросов падает
  • как сильно выросла задержка
  • какая часть таймаутов — у конкретного метода
  • насыщены ли лимиты (пулы, семафоры, очереди)
  • Это напрямую связано с надёжностью:

  • таймауты и ретраи должны быть видны как метрики, иначе вы не поймёте, что система «съедает» время на повторы
  • circuit breaker и rate limit должны иметь счётчики срабатываний
  • Практичные схемы метрик: RED и USE

    Обычно начинают с двух простых мнемоник:

  • RED для HTTP/RPC:
  • - Rate (RPS) - Errors (доля ошибок) - Duration (latency)
  • USE для ресурсов:
  • - Utilization (занятость) - Saturation (очереди, ожидание) - Errors (ошибки уровня ресурса)

    Prometheus в Go: минимальная интеграция

    Де-факто стандарт для метрик в инфраструктуре — Prometheus. Сайт: Prometheus.

    Клиентская библиотека Go: prometheus/client_golang.

    Пример: отдаём /metrics и считаем latency по endpoint.

    Замечание по контракту метрик: не используйте в label’ах высокую кардинальность (например, user_id, случайные request_id), иначе вы перегрузите хранилище метрик.

    Полезные системные метрики в Go

    Для диагностики в продакшене часто полезны:

  • метрики рантайма (GC, память)
  • pprof-профили для CPU/heap
  • Ссылки:

  • runtime/metrics
  • net/http/pprof
  • Трейсинг: увидеть один запрос сквозь несколько сервисов

    Что такое trace, span и контекст

    Trace — трасса одного запроса.

    Span — участок работы внутри трассы (например, «HTTP handler», «вызов gRPC в сервис B», «SQL запрос»).

    В каждом span обычно есть:

  • имя операции
  • начало и длительность
  • статус (успех/ошибка)
  • атрибуты (метки), например http.method, rpc.system, db.system
  • Критично важное правило из предыдущих статей: дедлайны и отмена должны проходить по цепочке через context.Context. Трейсинг тоже использует контекст для распространения trace_id.

    OpenTelemetry как стандарт

    Стандарт де-факто для трейсинга (и часто метрик/логов) — OpenTelemetry.

  • Сайт: OpenTelemetry
  • Go SDK: opentelemetry-go
  • Что даёт трейсинг для надёжности

    Трейсинг помогает отличить:

  • медленный сервис A от медленной зависимости B
  • проблемы DNS/соединения от проблем обработки
  • повторные попытки (ретраи) как отдельные span’ы
  • Практика: помечайте попытки ретраев атрибутом, например retry.attempt=2, чтобы было видно, куда ушло время.

    !Пример того, как трейс показывает узкое место на одном запросе

    Health checks: liveness, readiness и что именно проверять

    Эксплуатация почти всегда включает оркестратор (часто Kubernetes) или балансировщик, которым нужно понимать, можно ли слать трафик в экземпляр.

  • Liveness: процесс жив или завис навсегда.
  • Readiness: экземпляр готов принимать трафик (прогрелся, поднял соединения, догнал состояние).
  • В Kubernetes это выражается пробами. Документация: Kubernetes: Probes.

    Практические рекомендации:

  • readiness должен падать, если сервис не готов обслуживать корректно (например, нет подключения к критичной зависимости, или узел Raft не лидер, если вы обслуживаете записи только на лидере)
  • liveness не должен зависеть от внешних зависимостей, иначе вы получите циклические рестарты из-за проблем сети
  • Graceful shutdown: как не ронять запросы при деплое

    В распределённых системах деплой — это плановое частичное падение.

    Цель graceful shutdown:

  • перестать принимать новые запросы
  • дать завершиться текущим
  • закрыть фоновые goroutine и соединения
  • не потерять данные при записи
  • Для net/http обычно используют Server.Shutdown(ctx). Документация: http.Server.

    Минимальный пример с обработкой сигналов:

    Связь с предыдущими статьями:

  • если клиент видит обрыв во время деплоя, он может ретраить; поэтому операции записи должны быть идемпотентны или защищены идемпотентным ключом
  • сервер должен уважать r.Context(), чтобы прекращать работу при отмене и не держать ресурсы
  • Деплой без боли: совместимость, миграции, стратегии выкладки

    Главный принцип: изменения должны быть совместимыми во время выкладки

    Во время деплоя разные версии сервисов живут одновременно.

    Поэтому требования из статьи про API-контракты становятся эксплуатационными правилами:

  • новые версии сервиса должны уметь читать старые форматы
  • изменения схем данных должны быть совместимыми
  • нельзя выкатывать «ломающее изменение» одновременно на всех клиентах и серверах, если у вас нет жёсткой координации
  • Для protobuf это означает следовать правилам эволюции схем и не переиспользовать номера полей. Для HTTP/JSON — не менять типы и смысл полей без версии.

    Стратегии деплоя

    Самые распространённые стратегии:

  • Rolling update: постепенная замена экземпляров
  • - плюс: простота - риск: если ошибка зависит от состояния/нагрузки, вы увидите её постепенно
  • Blue/Green: две среды, переключение трафика целиком
  • - плюс: быстрый откат - риск: нужно уметь переключать и держать две среды
  • Canary: сначала маленький процент трафика на новую версию
  • - плюс: раннее обнаружение регрессий - риск: нужна хорошая наблюдаемость и маршрутизация

    !Как canary-деплой использует метрики для безопасной выкладки

    Миграции данных: безопасный порядок

    Типичная безопасная последовательность изменений:

  • Добавить новое поле/колонку или новый формат данных так, чтобы старая версия могла продолжать работать.
  • Выкатить код, который умеет писать в оба формата или читает из нового с фолбеком.
  • Провести бэкфилл данных, если нужно.
  • Только потом удалять старое поле/формат.
  • Это напрямую уменьшает риск инцидентов из-за «разъехавшихся» версий.

    Инциденты и эксплуатационная готовность: что должно быть заранее

    Наблюдаемость — это не только графики, но и готовность команды действовать.

    Минимум, который стоит подготовить:

  • Runbook: что проверять при росте latency/ошибок
  • Дашборды: RED по каждому сервису и ключевым зависимостям
  • Алерты:
  • - на симптомы (ошибки, p95/p99) - на насыщение (очереди, CPU, память, pool exhaustion)
  • Версионирование и метки релиза: чтобы связать всплеск ошибок с деплоем
  • Минимальный чеклист для Go-сервиса в распределённой системе

    | Область | Минимум по умолчанию | Зачем | |---|---|---| | Логи | JSON/структурные, request_id/trace_id, status, latency | быстро искать и связывать события | | Метрики | RPS, error rate, latency histogram, насыщение лимитов | понимать масштаб и динамику | | Трейсы | входящий запрос, исходящие вызовы, попытки ретраев | видеть узкие места в цепочке | | Health checks | liveness и readiness с корректной семантикой | безопасная маршрутизация трафика | | Shutdown | graceful shutdown + отмена по context | не ронять запросы на деплое | | Деплой | canary/rolling + наблюдаемость + быстрый откат | менять систему без инцидентов |

    Итоги

  • Наблюдаемость держится на трёх столпах: метрики, логи, трейсы.
  • Корреляция через request_id и trace_id превращает разрозненные сигналы в единый рассказ о запросе.
  • Эксплуатационные практики (health checks, graceful shutdown, canary/rolling, совместимые миграции) напрямую продолжают темы таймаутов, ретраев, идемпотентности и контрактов API.
  • В распределённых системах деплой — это контролируемый сбой, и ваша задача сделать его предсказуемым и диагностируемым.