Перейти к содержанию

B06 · Распределённые системы и масштабирование#

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

Навигация: DIST-01 · DIST-02 · DIST-03 · DIST-04 · DIST-05 · DIST-06 · DIST-07 · DIST-08 · DIST-09 · DIST-10 · DIST-11


DIST-01 · Вертикальное vs горизонтальное масштабирование#

Шаг W5

TL;DR. Вертикально — больше машина, горизонтально — больше машин. Первое проще и дольше остаётся правильным ответом, чем принято думать.

Проблема. Нагрузка выросла, ресурсов не хватает. Выбор способа масштабирования определяет не только производительность, но и то, придётся ли иметь дело со всем содержимым этого блока. Как только экземпляров становится больше одного, появляются согласованность, координация и частичные отказы.

Как работает.

  • Вертикально: больше CPU, памяти, быстрее диск. Приложение не меняется. Потолок — самая большая доступная машина; сегодня это сотни ядер и терабайты памяти, чего хватает подавляющему большинству систем.
  • Горизонтально: больше экземпляров за балансировщиком. Требует, чтобы сервис был stateless (без состояния): состояние вынесено в БД, кэш или хранилище сессий. Для данных горизонтальное масштабирование означает репликацию (DATA-09) для чтения и шардирование (DATA-10) для записи.
  • Практический порядок (см. W5): измерить → вертикально → кэш → реплики чтения → асинхронность → шардирование.

Когда применять. Вертикально — пока хватает и пока рост нагрузки предсказуем. Горизонтально — когда нужен запас по доступности (один экземпляр = SPOF, single point of failure — единая точка отказа), нагрузка скачет, либо потолок машины близок. Когда нет: горизонтальное масштабирование не лечит неэффективный запрос — оно множит его стоимость.

Альтернативы

Вариант Когда лучше Цена
Вертикально Простота важнее эластичности Потолок; простой при апгрейде; SPOF
Горизонтально Нужны эластичность и доступность Координация, согласованность, сложность эксплуатации
Оптимизация кода и запросов Узкое место — алгоритм, а не железо Требует профилирования
Разгрузка на CDN/кэш Читающая нагрузка Устаревание данных

Trade-offs и режимы отказа. Горизонтальное масштабирование добавляет сетевые вызовы — задержка растёт, даже если пропускная способность улучшилась. Липкие сессии частично возвращают состояние на узлы и мешают равномерности. Автоскейлинг реагирует минутами и бесполезен против секундного всплеска. Кроме того, при аварии control plane (управляющий слой) может быть недоступен (см. статическую стабильность в REL-06).

Что мерить.

  • утилизацию и насыщенность ресурсов — метод USE (utilization, saturation, errors);
  • пропускную способность на экземпляр;
  • стоимость единицы нагрузки;
  • эффективность масштабирования — прирост RPS на добавленный экземпляр. Она растёт медленнее линейной (PERF-07).

Связано: DATA-09 репликация · DATA-10 шардирование · EDGE-01 балансировка · PERF-07 закон Литтла

Источники: Кleppmann, DDIA, гл. 1 · AWS Well-Architected — Performance Efficiency Pillar · ByteByteGo — Vertical Vs Horizontal Scaling: Key Differences You Should Know · ByteByteGo — Scalability Simply Explained in 10 Minutes


DIST-02 · CAP и PACELC#

Шаг W6

TL;DR. При разрыве сети выбирают между согласованностью и доступностью; в отсутствие разрыва — между согласованностью и задержкой. Второе важнее в повседневности, но известно хуже.

Проблема. CAP-теорему обычно пересказывают как «выбери два из трёх» — и это вводит в заблуждение. Разделение сети (P) не выбирают: оно случается. Выбор возникает только в момент разрыва: отвечать ли узлу, который не уверен, что видит актуальные данные.

Как работает.

  • CAP (Gilbert, Lynch — формализация гипотезы Brewer): при сетевом разделении система не может одновременно гарантировать линеаризуемость (C) и отвечать на все запросы (A). Практический выбор:
    • CP — отказать в обслуживании до восстановления связи. Так ведут себя etcd, ZooKeeper, реляционные БД с синхронной репликацией;
    • AP — ответить, возможно, устаревшими данными и разобраться с конфликтами позже. Так ведут себя Cassandra и Dynamo в базовой настройке.
  • PACELC (Abadi) достраивает картину: if Partition → A or C, Else → L or C. То есть даже без разрыва есть постоянный компромисс между задержкой и согласованностью. Строгое чтение требует обращения к лидеру или кворуму, а это лишние миллисекунды — или десятки миллисекунд между регионами.
  • Гранулярность — операция, а не система. В одной системе оформление заказа может быть CP, а лента рекомендаций — AP/EL. «Наша система CP» — как правило, упрощение.

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

Альтернативы (уточняющие модели)

Модель Что добавляет
PACELC Компромисс задержка/согласованность в штатном режиме
Модели согласованности (DIST-03) Спектр вместо бинарного «C или не C»
Harvest & Yield (Fox, Brewer) Деградация полноты ответа вместо отказа

Trade-offs и режимы отказа. «A» в CAP — это гарантия ответа каждого живого узла, а не бытовая доступность: система с 99.99 % аптайма может быть CP. «C» — это линеаризуемость, а не ACID-согласованность, хотя буква та же. Из CAP не следует ни одного конкретного архитектурного решения — она лишь запрещает невозможное.

Что мерить.

  • поведение при искусственном разрыве (REL-10);
  • долю чтений, требующих строгой согласованности;
  • разницу p99 строгих и обычных чтений.

Связано: DIST-03 модели согласованности · DIST-04 кворумы · DATA-09 репликация

Источники: Gilbert & Lynch — Brewer's Conjecture (2002) · Eric Brewer — CAP Twelve Years Later · Daniel Abadi — Consistency Tradeoffs in Modern Distributed Database System Design (PACELC) · ByteByteGo — CAP Theorem Simplified


DIST-03 · Модели согласованности#

Шаг W6

TL;DR. Между «актуально в любой момент» и «когда-нибудь сойдётся» лежит спектр гарантий. Дизайн выбирает точку на нём для каждого чтения.

Проблема. «Eventual consistency» (отложенная согласованность) ничего не обещает о том, что пользователь увидит сейчас. А от этого зависит, будет ли интерфейс выглядеть сломанным. Без явных гарантий возникают классические баги: пользователь опубликовал комментарий и не видит его; ответ появился раньше вопроса; счётчик скачет туда-обратно при обновлении страницы.

Как работает — спектр от сильного к слабому.

  • Линеаризуемость (strong). Система ведёт себя так, будто существует одна копия данных и операции выполняются мгновенно в некоторый момент между запросом и ответом. Дороже всего: требует лидера или кворума на каждое чтение.
  • Sequential / serializable. Родственные, но про другое: serializability — свойство транзакций (DATA-08), линеаризуемость — свойство отдельных операций. Их сочетание (strict serializable) — самая сильная и самая дорогая гарантия.
  • Causal consistency (причинная согласованность). Причинно связанные операции видны всем в одном порядке; независимые — в любом. Достаточна для большинства интерфейсов: ответ не появится раньше вопроса.
  • Гарантии сессии — практический минимум для UI: read-your-writes (вижу свою запись), monotonic reads (не «уезжаю» в прошлое), monotonic writes (мои записи применяются по порядку). Реализуются закреплением сессии за репликой или чтением с учётом версии.
  • Eventual. Реплики сойдутся при отсутствии новых записей. Ничего не обещает о промежутке.

Реализация гарантий сессии обычно проще, чем кажется. После записи клиент какое-то время читает с лидера либо носит с собой номер версии, и реплика ждёт, пока догонит.

Когда применять. Определять модель по операции: профиль пользователя — read-your-writes; лента друзей — causal или eventual; остаток на счёте — линеаризуемость.

Альтернативы

Модель Стоимость Типичное применение
Линеаризуемость Высокая (кворум/лидер, RTT) Деньги, уникальность, блокировки
Causal Средняя Комментарии, сообщения, ленты
Гарантии сессии Низкая Большинство пользовательских экранов
Eventual Минимальная Счётчики, рекомендации, аналитика

Trade-offs и режимы отказа. Строгая согласованность стоит задержки и доступности при разрыве (DIST-02). Слабая — стоит сложности в приложении: конфликты и «странные» состояния UI приходится обрабатывать вручную. Смешение гарантий в одном экране даёт наблюдаемые аномалии: одна панель обновилась, соседняя нет.

Что мерить.

  • лаг репликации, p50 и p99;
  • долю чтений с лидера;
  • число обращений в поддержку с формулировкой «данные не обновились».

Связано: DIST-02 CAP и PACELC · DATA-09 репликация · DIST-10 разрешение конфликтов · DATA-08 уровни изоляции

Источники: Jepsen — Consistency Models · Kleppmann, DDIA, гл. 5 и 9 · Highly Available Transactions: Virtues and Limitations (Bailis et al.)


DIST-04 · Кворумы чтения и записи#

Шаг W6

TL;DR. При N репликах запись подтверждается W узлами, чтение опрашивает R; если R + W > N, множества пересекаются и чтение видит последнюю подтверждённую запись.

Проблема. Ждать подтверждения от всех реплик — значит зависеть от самой медленной и терять доступность при отказе любой. Не ждать никого — читать устаревшее. Кворум даёт настраиваемую середину.

Как работает.

  • Типичная конфигурация: N = 3, W = 2, R = 2. Запись успешна, когда ответили 2 из 3; чтение опрашивает 2 из 3 и берёт версию с максимальным номером/меткой.
  • Перекос настраивается под нагрузку: W = 1, R = N — быстрые записи, медленные чтения; W = N, R = 1 — наоборот.
  • Read repair (починка при чтении) и anti-entropy (фоновая сверка реплик) доводят отставшие реплики до актуального состояния.
  • Кворум применим и в консенсусных системах (DIST-05), где большинство (⌊N/2⌋ + 1) требуется для выбора лидера и фиксации записи в журнал.

Когда применять. В хранилищах без единого лидера (Dynamo-подобные: Cassandra, Riak, ScyllaDB), где нужен явный контроль компромисса «свежесть против задержки». Когда нет: если требуется линеаризуемость, кворума самого по себе недостаточно — нужен консенсус.

Альтернативы

Вариант Когда лучше Цена
Кворум (leaderless) Нужна настраиваемость и запись при отказе узла Не даёт линеаризуемости; конфликты версий
Одна ведущая реплика (leader-based) Нужен простой строгий порядок записи Лидер — узкое место и точка отказа
Консенсус (Raft/Paxos) Нужна линеаризуемость с автоматическим failover (переключением на резерв) Дороже по задержке, сложнее эксплуатация
Асинхронная репликация Максимальная скорость записи Потеря данных при отказе лидера

Trade-offs и режимы отказа. R + W > N не даёт линеаризуемости: при параллельных записях возможны конфликтующие версии, которые придётся разрешать (DIST-10). При sloppy quorum (нестрогом кворуме) с hinted handoff (передачей записи по подсказке) запись принимают узлы вне обычного набора реплик: доступность выше, но гарантия пересечения теряется. Кворум не спасает от отката: если запись подтвердили W узлов и все они потеряны, данные исчезли. Реальная задержка кворума определяется самым медленным из необходимых ответов — то есть хвостом распределения.

Что мерить.

  • задержку чтения и записи по уровням согласованности;
  • число конфликтующих версий;
  • объём работы read repair;
  • число hinted handoff.

Связано: DIST-05 консенсус · DATA-09 репликация · DIST-10 конфликты · DIST-03 модели согласованности

Источники: Dynamo: Amazon's Highly Available Key-value Store (SOSP 2007) · Kleppmann, DDIA, гл. 5 · Apache Cassandra — Consistency levels


DIST-05 · Консенсус: Raft, leader election#

Шаг W6

TL;DR. Консенсус позволяет группе узлов договориться о едином журнале решений и пережить отказ меньшинства. Он лежит под каждым «надёжным» координатором.

Проблема. Многие задачи сводятся к одной: заставить несколько узлов согласиться на одно значение — кто сейчас лидер, кому принадлежит шард, какая конфигурация актуальна, зафиксирована ли транзакция. Простые схемы («каждый решает сам», «решает первый успевший») ломаются при разрыве сети, порождая два лидера и расхождение данных — split-brain («раздвоение»).

Как работает (на примере Raft).

  • Узлы находятся в одном из состояний: follower (ведомый), candidate (кандидат), leader (лидер). Время разбито на термы (эпохи) с монотонно растущим номером.
  • Выбор лидера: follower, не получивший вестей от лидера за случайный таймаут, становится кандидатом и просит голоса. Лидером становится набравший большинство. Случайность таймаутов разводит кандидатов во времени.
  • Репликация журнала: клиентские команды принимает только лидер, дописывает в журнал и рассылает followers. Запись считается зафиксированной, когда её приняло большинство — после этого она применяется к машине состояний.
  • Безопасность: узел с отстающим журналом не может быть избран; большинство гарантирует, что любые два кворума пересекаются, а значит зафиксированное решение не потеряется.
  • Кворум большинства означает: кластер из 3 узлов переживает отказ 1, из 5 — двух. Чётное число узлов бесполезно (4 переживает столько же, сколько 3).

Когда применять. Через готовые системы — etcd, ZooKeeper, Consul — для выборов лидера, конфигурации, блокировок, регистрации сервисов. Внутри распределённых БД (CockroachDB, TiDB, YugabyteDB, Kafka KRaft) консенсус обеспечивает согласованность реплик шарда. Когда нет: писать собственный консенсус в прикладной системе — как правило, ошибка. И не стоит гонять через него высокочастотные данные: он для метаданных.

Альтернативы

Вариант Когда лучше Цена
Raft Нужен понятный, широко реализованный консенсус Задержка кворума на каждую запись
Multi-Paxos Исторически в основе многих систем Сложнее для понимания и реализации
Lease (аренда) через внешний координатор Нужны просто выборы лидера Зависимость от координатора
Детерминированный выбор (по хэшу, по конфигурации) Кластер статичен Не переживает отказ автоматически

Trade-offs и режимы отказа. Каждая запись стоит round-trip до большинства — это плохо переносится на межрегиональные кластеры (десятки миллисекунд на операцию). Потеря большинства останавливает запись целиком: консенсусные системы предпочитают недоступность расхождению (это CP по DIST-02). Частые перевыборы — флаппинг (flapping) — при нестабильной сети деградируют кластер сильнее, чем честный отказ узла.

Что мерить.

  • число смен лидера за период;
  • задержку фиксации записи;
  • отставание followers;
  • доступность кворума.

Связано: DIST-04 кворумы · DIST-07 распределённые блокировки · EDGE-08 service discovery · DIST-08 распределённые транзакции

Источники: In Search of an Understandable Consensus Algorithm (Raft), Ongaro & Ousterhout · raft.github.io — визуализация · Leslie Lamport — Paxos Made Simple · etcd — Learning: Data model and Raft


DIST-06 · Обнаружение отказов и мембершип#

Шаг W7

TL;DR. Весь блок опирается на фразу «узел отказал». Установить это точно невозможно — есть только подозрение с настраиваемой чувствительностью, и от её настройки зависит, будет система флаппить или тормозить.

Проблема. Узел не отвечает. Он умер, перегружен, застрял в паузе сборщика мусора или до него пропал сетевой путь? В асинхронной сети эти случаи неразличимы: нет способа отличить «мёртв» от «медленно». Любой детектор — это эвристика, у которой две ошибки: объявить живой узел мёртвым (ложное срабатывание) и не заметить мёртвый (задержка обнаружения). Уменьшение одной увеличивает другую, и выбор точки на этой шкале — архитектурное решение, а не настройка по умолчанию.

Как работает.

  • Heartbeat (сигнал жизни) и таймаут — базовая схема: узел периодически подаёт признаки жизни, отсутствие сигнала дольше порога означает подозрение. Порог задаёт компромисс напрямую: секунда даёт быстрое обнаружение и ложные срабатывания при всплеске задержки, тридцать секунд — надёжность и полминуты недоступности.
  • Накопительный детектор (phi-accrual) вместо булева ответа выдаёт степень подозрения, вычисленную по наблюдаемому распределению задержек. Порог задаётся один раз, а чувствительность подстраивается сама: в медленной сети детектор терпеливее. Применяется в Cassandra и Akka.
  • Непрямая проверка (SWIM). Прежде чем объявить узел мёртвым, инициатор просит несколько случайных узлов проверить его самостоятельно. Это отсекает главный источник ложных срабатываний — проблему на стороне проверяющего, а не проверяемого. Затем состояние проходит стадию «подозревается» и лишь потом «мёртв», что даёт узлу шанс опровергнуть.
  • Распространение слухами (gossip). Узлы периодически обмениваются известным им состоянием со случайными соседями; информация расходится по кластеру за логарифмическое число раундов, без координатора и без нагрузки на один узел. Так работают Cassandra, Consul, Serf.
  • Мембершип (membership) — это список живых узлов, и он бывает двух видов. Строгий: состав хранится в консенсусном хранилище (DIST-05), все видят одно и то же, но цена — координация и ограничение по размеру кластера. Слабый: состав распространяется слухами, масштабируется на тысячи узлов, но узлы временно видят разные картины. Выбор определяется тем, нужен ли точный ответ на вопрос «кто в кворуме».

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

Альтернативы

Механизм Обнаружение Масштаб Точность
Heartbeat к координатору Быстрое Ограничен координатором Ложные срабатывания при перегрузке сети
Phi-accrual Адаптивное Тот же Меньше ложных срабатываний
SWIM с непрямой проверкой Быстрое Тысячи узлов Заметно точнее
Gossip Медленнее (раунды) Наибольший Временные расхождения
Консенсус Медленное Десятки узлов Точный общий ответ

Trade-offs и режимы отказа. Самый неприятный случай детекторы не ловят вовсе: серый отказ (gray failure): узел жив, отвечает на проверки, но обслуживает запросы неправильно или впятеро медленнее. Против него работают не heartbeat, а наблюдение за реальным трафиком: пассивные проверки балансировщика и отсев выбросов (REL-05). Асимметричный разрыв, когда A видит B, а B не видит A, приводит к взаимному объявлению мёртвыми и колебаниям состава. Ложное объявление лидера мёртвым даёт двух лидеров — поэтому корректность не должна зависеть только от детектора: её страхует fencing token (DIST-07).

Что мерить.

  • время обнаружения отказа — медиану и хвост;
  • долю ложных срабатываний;
  • частоту изменений состава — флаппинг;
  • расхождение списков живых между узлами;
  • время сходимости после изменения.

Связано: DIST-05 консенсус · DIST-07 распределённые блокировки · REL-05 health checks · REL-08 failover

Источники: SWIM: Scalable Weakly-consistent Infection-style Process Group Membership Protocol (DSN 2002) · Hayashibara et al. — The φ Accrual Failure Detector · HashiCorp Serf — Gossip protocol · Kleppmann, DDIA, гл. 8


DIST-07 · Распределённые блокировки и lease#

Шаг W6

TL;DR. Распределённая блокировка — взаимное исключение между процессами на разных машинах. Она корректна, только если у неё ограниченный срок — lease (аренда) — и есть защита от «проснувшегося» держателя — fencing token (ограждающий маркер).

Проблема. Нужно, чтобы задачу выполнял ровно один экземпляр: обработчик по расписанию, миграция, отправка письма. Наивная схема «поставить ключ в Redis с TTL (временем жизни)» ломается в двух сценариях:

  • Держатель блокировки завис (GC-пауза, перегрузка) дольше TTL. Блокировку получает второй, а первый потом просыпается и продолжает работать, уверенный, что владеет ей.
  • Сеть разделилась, и обе стороны считают себя владельцем.

Как работает.

  • Lease — блокировка с временем жизни. Она обязательна: без неё упавший держатель блокирует ресурс навсегда.
  • Fencing token — монотонно растущий номер, выдаваемый вместе с блокировкой. Держатель передаёт его при каждой операции с защищаемым ресурсом, а ресурс отвергает операции с номером меньше уже виденного. Это единственный способ защититься от «проснувшегося» держателя, потому что паузу произвольной длины предотвратить нельзя.
  • Хранилище блокировок должно быть консенсусным (etcd, ZooKeeper) — тогда взаимное исключение опирается на кворум, а не на одну реплику. Сессии ZooKeeper и lease в etcd дают автоматическое освобождение при потере связи.
  • Альтернатива архитектурой: партиционирование работы по ключу, при котором каждый ключ обрабатывает ровно один потребитель, устраняет необходимость в блокировках вовсе. Обычно это лучшее решение.

Когда применять. Для координации редких операций (лидер обработчика, разовые миграции) и там, где ресурс поддерживает fencing. Когда нет: для защиты корректности данных — там правильнее транзакция БД, уникальный индекс или оптимистическая блокировка по версии.

Альтернативы

Вариант Когда лучше Цена
Уникальный индекс / транзакция БД Защищаемый ресурс — сама БД Только в пределах одной БД
Оптимистическая блокировка (версия строки) Конфликты редки Требует повтора при конфликте
Партиционирование по ключу Работу можно разделить Ребалансировка при изменении состава
Lease через консенсус + fencing Нужна настоящая взаимоисключающая блокировка Внешняя зависимость и её эксплуатация
Redlock (кворум Redis-узлов) — Спорная корректность при паузах и рассинхронизации часов

Trade-offs и режимы отказа. Блокировка — это точка сериализации: она ограничивает пропускную способность и создаёт очередь. Короткий TTL порождает ложные потери владения, длинный — долгую недоступность после отказа. Без fencing любая блокировка на TTL остаётся вероятностной гарантией, а не строгой.

Что мерить.

  • время удержания блокировки;
  • долю неудачных попыток захвата;
  • число случаев, когда у блокировки оказалось больше одного владельца, — должно быть 0;
  • число операций, отвергнутых по устаревшему fencing token.

Связано: DIST-05 консенсус · DATA-08 уровни изоляции · MSG-06 гарантии доставки · CASE-08 бронирование билетов

Источники: Martin Kleppmann — How to do distributed locking · Redis — Distributed Locks · etcd — Distributed locks / lease


DIST-08 · Распределённые транзакции: 2PC, saga, TCC#

Шаг W6

TL;DR. Когда операция затрагивает несколько сервисов или БД, атомарности «как в одной транзакции» не будет. Выбор — между блокирующим 2PC и компенсируемой saga.

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

Как работает.

2PC (двухфазная фиксация). Координатор спрашивает участников «готовы?» (prepare). Каждый готовит запись и удерживает блокировки, обещая, что сможет зафиксировать. Затем координатор рассылает commit или abort. Атомарность есть, но:

  • блокировки держатся весь протокол;
  • отказ координатора между фазами оставляет участников заблокированными до его восстановления;
  • нужна поддержка со стороны всех участников (XA).

Saga (сага). Последовательность локальных транзакций, каждая со своей компенсацией. Списали деньги → зарезервировали товар → создали доставку; на сбое выполняются компенсации в обратном порядке (вернуть деньги, снять резерв). Два стиля координации:

  • хореография (choreography): сервисы реагируют на события друг друга — меньше связности, но хуже видно, как идёт процесс;
  • оркестрация (orchestration): явный координатор процесса — понятнее и проще отлаживать, но появляется центральный компонент.

TCC (Try-Confirm-Cancel). Компромисс: на шаге Try ресурс резервируется (деньги заморожены, товар отложен), затем Confirm фиксирует, Cancel отменяет. Похоже на 2PC, но без удержания блокировок БД — резерв выражен в модели данных.

Ключевое ограничение saga. У неё нет изоляции: промежуточные состояния видны другим. Это лечится не техникой, а моделированием — статусами (ожидает подтверждения), семантическими блокировками, повторным чтением перед решением.

Когда применять. Saga — по умолчанию для бизнес-процессов между сервисами. TCC — когда резервирование естественно для домена (билеты, склад, лимиты). 2PC — внутри одной инфраструктуры, поддерживающей XA, при коротких транзакциях. Когда нет: если сущности меняются только вместе — это признак неверных границ (ARCH-02); объедините их в одну транзакцию и одно хранилище.

Альтернативы

Вариант Атомарность Изоляция Цена
Одна локальная транзакция Полная Полная Требует общих границ данных
2PC / XA Полная Есть (блокировки) Блокировки, уязвимость к отказу координатора
Saga Через компенсации Нет Компенсации, видимые промежуточные состояния
TCC Через резерв Частичная Три операции на каждый шаг, модель с резервами
Outbox + eventual (MSG-07) Отложенная Нет Простая реализация, отложенная согласованность

Trade-offs и режимы отказа. Компенсация возможна не для каждого шага (письмо отправлено, SMS доставлена) — такие шаги ставят последними или делают отменяемыми на уровне домена. Компенсация сама может упасть, поэтому она должна быть идемпотентной и повторяемой. Оркестратор становится критичным компонентом со своим состоянием и своей отказоустойчивостью.

Что мерить.

  • долю sagas, завершившихся компенсацией;
  • число «зависших» sagas и их возраст;
  • время прохождения процесса;
  • число ручных вмешательств.

Связано: MSG-07 outbox и CDC · NET-16 идемпотентность · ARCH-04 хореография vs оркестрация · CASE-09 платёжный поток

Источники: microservices.io — Saga pattern · Kleppmann, DDIA, гл. 9 · Pat Helland — Life beyond Distributed Transactions (CIDR 2007) · Garcia-Molina & Salem — Sagas (1987)


DIST-09 · Генерация уникальных ID#

Шаг W3

TL;DR. Автоинкремент не переживает шардирования, случайный UUID разрушает локальность индекса. Практический ответ — время + узел + счётчик.

Проблема. Идентификатор нужен до вставки (чтобы клиент мог повторить запрос идемпотентно), должен быть уникален между шардами и не должен губить производительность индекса. Автоинкремент БД требует обращения к единой точке и ломается при нескольких шардах. Случайный UUIDv4 уникален, но вставляется в случайное место B-tree — страницы фрагментируются, кэш промахивается, скорость записи падает на больших таблицах.

Как работает.

  • Snowflake-подобная схема (64 бита): ~41 бит времени в миллисекундах, ~10 бит идентификатора узла, ~12 бит счётчика внутри миллисекунды. Даёт монотонность по времени, уникальность без координации и компактность.
  • UUIDv7 (RFC 9562): 128 бит, старшие — метка времени Unix в миллисекундах, остальное случайное. Стандартизован, сортируется по времени, не требует раздачи идентификаторов узлам. Практический выбор по умолчанию для новых систем.
  • ULID — 128 бит, тот же принцип (время + случайность), компактная текстовая запись из 26 символов.
  • Диапазоны (hi/lo) — узел получает блок значений из центрального счётчика и раздаёт локально. Просто, но требует координатора и оставляет дыры в нумерации.
  • Отдельный сервис ID (Flake-подобный) — если нужна строгая централизованная гарантия.

Когда применять. Время-упорядоченные ID — везде, где записи вставляются в индексируемую таблицу. Когда нет: идентификатор виден пользователю и не должен раскрывать время создания или объём — порядковые ID позволяют оценить число заказов. Тогда наружу отдают случайный идентификатор, а внутренний, непубличный, оставляют упорядоченным.

Альтернативы

Вариант Плюс Минус
Автоинкремент БД Компактность, идеальная локальность Не работает при шардировании; ID известен только после вставки
UUIDv4 Никакой координации Плохая локальность индекса, 128 бит
UUIDv7 / ULID Сортируемость + отсутствие координации 128 бит; раскрывает время создания
Snowflake 64 бита, сортируемость Нужна раздача ID узлов и синхронизация часов
Hi/Lo диапазоны Компактность Центральный счётчик, дыры в нумерации

Trade-offs и режимы отказа. Snowflake зависит от часов: сдвиг времени назад (NTP-коррекция) может породить дубликаты — реализации должны отказывать в выдаче до восстановления. Повторно выданный идентификатор узла (два экземпляра с одним номером) — тихая коллизия, которая обнаружится нескоро. Время-упорядоченные ID создают «горячий» край индекса: все вставки идут в последнюю страницу, что при экстремальной нагрузке становится точкой конкуренции.

Что мерить.

  • число коллизий — должно быть нулевым;
  • отклонение часов между узлами;
  • фрагментацию индекса и скорость вставки;
  • задержку выдачи ID при централизованной схеме.

Связано: DATA-10 шардирование · DATA-05 индексы · NET-16 идемпотентность · DIST-10 время и версии · CASE-01 кейс: сокращатель ссылок

Источники: RFC 9562 — Universally Unique IDentifiers (UUIDv7) · Twitter Snowflake · ULID specification


DIST-10 · Время, версии и разрешение конфликтов#

Шаг W6

TL;DR. Часам разных машин доверять нельзя. Порядок событий восстанавливают логическими версиями, а параллельные изменения либо разрешают явно, либо используют структуры, которые сходятся сами.

Проблема. Две реплики приняли изменение одной записи одновременно. Кто прав? Наивный ответ «у кого метка времени больше» опирается на физические часы, которые расходятся на десятки и сотни миллисекунд, а при коррекции NTP могут идти назад. Результат — тихая потеря данных: запись, сделанная позже, отбрасывается как «старая», и никто об этом не узнаёт.

Как работает.

  • Логические часы Лампорта — счётчик, растущий при каждом событии и при получении сообщения. Дают согласованный полный порядок, но не позволяют отличить причинную связь от параллельности.
  • Векторные часы / версионные векторы — по счётчику на реплику. Позволяют точно определить, что одно событие произошло до другого, а что параллельно ему. Цена — размер вектора растёт с числом реплик.
  • LWW (last write wins, побеждает последняя запись) — выбрать «последнюю» запись по метке. Просто и, как правило, означает потерю одной из версий; допустимо для данных, которые не жалко (кэш, сессии, телеметрия).
  • Сохранение конфликтующих версий (siblings) и разрешение на уровне приложения или пользователя — так работает корзина в Dynamo: конфликт разрешается объединением.
  • CRDT (conflict-free replicated data types, бесконфликтные реплицируемые типы данных) — типы данных, для которых операции коммутативны, поэтому реплики сходятся автоматически без координации: счётчики, множества, регистры, текст. Основа офлайн-редакторов и совместного редактирования.
  • Гибридные и физически-синхронизированные часы: TrueTime в Spanner использует атомарные часы и явную границу погрешности. Система ждёт её истечения перед фиксацией и так превращает неопределённость часов в известную задержку.

Когда применять. Как только запись возможна параллельно в двух местах: multi-master (несколько ведущих), multi-region active-active (запись во всех регионах), офлайн-клиенты. Когда нет: при единственной ведущей реплике порядок задаёт лидер, и вся эта машинерия не нужна.

Альтернативы

Вариант Когда лучше Цена
Единственный лидер Можно избежать параллельной записи Лидер — узкое место
LWW по метке времени Потеря данных допустима Тихая потеря; зависимость от часов
Версионные векторы + разрешение в приложении Данные ценны, конфликт осмыслен Логика разрешения и хранение версий
CRDT Нужна автоматическая сходимость Ограниченный набор типов, метаданные растут

Trade-offs и режимы отказа. «Одновременно» — не про часы, а про отсутствие причинной связи; без логических версий отличить одно от другого невозможно. LWW в системе, где параллельная запись возможна, гарантированно теряет данные — вопрос лишь в частоте. Хранение siblings без стратегии их разрешения приводит к бесконечному росту версий.

Что мерить.

  • число конфликтующих версий и время до их разрешения;
  • отклонение часов между узлами;
  • размер метаданных версий;
  • долю LWW-разрешений.

Связано: DIST-04 кворумы · DIST-03 модели согласованности · REL-08 multi-region · DIST-09 уникальные ID

Источники: Lamport — Time, Clocks, and the Ordering of Events (1978) · Shapiro et al. — Conflict-free Replicated Data Types · Spanner: Google's Globally-Distributed Database (OSDI 2012) · Kleppmann, DDIA, гл. 5 и 8


DIST-11 · Паттерны распределённых систем: обзор#

Шаг W4

TL;DR. Карта: какой паттерн какую задачу закрывает и где о нём читать подробно.

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

Карта паттернов по задачам

Задача Паттерны Подробно
Распределить нагрузку Балансировка, шардирование, партиционирование, consistent hashing EDGE-01, DATA-10, DATA-11
Ускорить чтение Кэш, реплики чтения, CDN, материализованные проекции CACHE-01, DATA-09, ARCH-06
Развязать компоненты Очередь, pub/sub, лог событий, outbox MSG-02, MSG-07
Договориться о состоянии Консенсус, выбор лидера, lease, кворум DIST-05, DIST-04, DIST-07
Сохранить корректность Идемпотентность, saga, TCC, оптимистическая блокировка NET-16, DIST-08
Пережить отказ Таймауты и повторы, circuit breaker, bulkhead, деградация REL-01, REL-02
Вынести общую функциональность Sidecar, ambassador, service mesh, BFF, API gateway ARCH-10, EDGE-03
Наблюдать за системой Сквозная трассировка, агрегация метрик и логов OPS-07

Принципы, которые стоят за паттернами.

  • BASE против ACID — для распределённых хранилищ доступность и мягкое состояние часто ценнее немедленной согласованности; выбор делается по операции, а не по системе (DIST-03).
  • KISS (keep it simple — делай проще) — в распределённых системах цена сложности выше, чем в монолите: каждый компонент нужно эксплуатировать, наблюдать и чинить ночью.
  • Разделение ответственности — работает и на уровне модулей, и на уровне сервисов; неверные границы дороже плохого кода (ARCH-02).

Как выбирать.

  • сформулировать проблему числом — из W1;
  • взять из таблицы кандидатов;
  • сравнить по осям W10;
  • зафиксировать решение в ADR (architecture decision record, запись архитектурного решения).

Паттерн, введённый «потому что так делают в бигтехе», обычно решает чужую проблему за ваши деньги.

Связано: весь блок B06 · ARCH-01 монолит vs микросервисы · W4

Источники: Azure Architecture — Cloud Design Patterns · microservices.io — Pattern language · Kleppmann, DDIA · ByteByteGo — Top 7 Most-Used Distributed System Patterns · ByteByteGo — KISS, SOLID, CAP, BASE: Important Terms You Might Not Know!