← назад к разделу

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

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

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

5 реплик, кворум большинства = 3разрыв сетиABCDE3 из 5 — большинство2 из 5 — меньшинство согласованность (CP)Raft, etcd: кворум 3сторона из 3: пишет3 ≥ кворум 3, есть ответсторона из 2: отказ2 < 3, ждёт починки доступность (AP)два владельца именисторона из 3: пишетимя занято Алисойсторона из 2: пишетто же имя — Бобу 2PC поверх разрывакоординатор — узел Dучастники сказали «да»блокировки висят, отката неткоординатор молчитрешения нет ни у кого 3 + 3 = 6 > 5 — два большинства обязаны пересечьсяведущий, уникальность, фиксация — одна задача: консенсус

Разрыв не выбирают — выбирают, как считать голоса. Кворум большинства даёт одно решение на всю систему ценой отказа меньшинству; координатор 2PC за разрывом не даёт вообще ничего.

Обязательно

Линеаризуемость: иллюзия единственной копииспросят на собеседовании

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

Классический пример нарушения. Алиса и Боб смотрят финал матча в одной комнате. Алиса обновила страницу, увидела счёт и крикнула его Бобу. Боб обновляет у себя — а его запрос попал на отставшую реплику, и там матч «ещё идёт». Отставание реплики превратилось в видимый баг именно потому, что между Алисой и Бобом был второй канал связи — её голос.

t1 запись: счёт 2:1 t2 Алиса читает 2:1 t3 Алиса кричит счёт Бобу t4 Боб читает старый счёт

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

Линеаризуемость нужна не везде — она медленная (почему — см. про CAP ниже), — но в нескольких местах без неё нельзя:

  • Выбор ведущего. Ведущий должен быть ровно один (иначе split brain). Узлы обязаны прийти к единому мнению, кто это.
  • Ограничения уникальности. Два человека одновременно занимают одно имя / бронируют одно место / снимают с одного счёта — выиграть должен ровно один.
  • Межканальные зависимости. Пример выше: два разных пути к данным (кеш и база, очередь и хранилище) рождают гонку, которую как раз и закрывает линеаризуемость.

Важно не путать линеаризуемость с сериализуемостью. Сериализуемость — про изоляцию транзакций (несколько объектов, порядок транзакций между собой). Линеаризуемость — про актуальность чтения-записи одного объекта. Это разные гарантии, хотя названия похожи.

Теорема CAP без мифовспросят на собеседовании

Раз линеаризуемость такая полезная — почему бы не сделать линеаризуемым вообще всё? Мешает сеть. Представьте два дата-центра с репликацией между ними, и вот связь между ними оборвалась. Клиент на «отрезанной» стороне хочет что-то записать. Выбор жёсткий:

  • Настаиваем на линеаризуемости. Реплика в меньшинстве не может подтвердить, что её данные актуальны, поэтому обязана отказать — стать недоступной — пока связь не восстановится. Это буква «C»: согласованность ценой доступности.
  • Хотим доступность. Тогда принимаем запись локально, но реплики двух дата-центров неизбежно разойдутся (это уже не линеаризуемо). Это буква «A».

Это и есть CAP. Но популярная расшифровка «выбери 2 из 3: Consistency, Availability, Partition tolerance» — неверна. Разрыв сети (буква «P») — это не то, что можно «выбрать» или «не выбрать»: он просто случается, хотим мы того или нет. Честная формулировка короче: когда сеть разорвана — выбирай между согласованностью и доступностью. А пока сеть исправна, получаешь и то, и другое сразу.

Раз P (разрыв сети) в распределённой системе неизбежен, реальный выбор — это сторона CP или AP: что делать при разрыве — держать согласованность (и на время отказать в записи) или доступность (и позволить репликам разойтись). Третью сторону, CA, в учебниках принято рисовать рядом с этими двумя — как равноправный вариант выбора. Равноправного варианта там нет: «CA» означает всего лишь «одна машина, разрыву между узлами взяться неоткуда», то есть не про распределённую систему вовсе. И деление баз по сторонам условно: многие настраиваются и в ту, и в другую. Классические примеры — MongoDB и HBase на стороне CP, Cassandra и DynamoDB на стороне AP.

К тому же CAP очень узок: он говорит лишь про один тип сбоя (разрыв связи) и одну модель (линеаризуемость) и молчит про задержки и мёртвые узлы. Поэтому на практике у теоремы скорее исторический интерес.

И отдельный любопытный факт: удивительно мало систем на самом деле линеаризуемы — причём отказываются от неё не ради переживания сбоев, а ради скорости. Линеаризуемость медленная всегда (время ответа тянется за сетевой задержкой), и большинство систем сознательно её не дают.

Порядок, причинность и рассылка

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

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

  1. У каждого узла есть счётчик, изначально ноль. Перед каждой своей операцией узел увеличивает его на единицу.
  2. Отправляя сообщение, узел прикладывает к нему текущее значение счётчика.
  3. Получив сообщение, узел берёт максимум из своего счётчика и полученного, увеличивает на единицу и работает дальше.

Метка операции — это пара «счётчик, идентификатор узла». Сравнивают их по счётчику, а при равенстве — по идентификатору узла, чтобы порядок был однозначным. Пример на двух узлах: A выполнил две операции (1,A), (2,A) и отправил сообщение с меткой 2; узел B, у которого счётчик был 1, берёт максимум (2), прибавляет единицу и получает (3,B). Порядок (1,A) → (2,A) → (3,B) согласован с причинностью: операция B, вызванная сообщением от A, гарантированно оказывается позже.

узел A (1,A) (2,A) шлёт метку 2 узел B счётчик 1 max(1,2)+1 (3,B)

Счётчик едет в сообщении, получатель берёт максимум своего и полученного и прибавляет единицу, поэтому у B выходит 3, а не 2.

Что этого не даёт — и почему уникальность так не решается. Если два узла независимо заняли один ник, у них получатся метки (5,A) и (5,B), и сравнить их можно: (5,A) раньше. Но узнать это можно только когда обе метки встретятся в одном месте, то есть потом. В момент операции узел A не знает о существовании (5,B) и честно отвечает клиенту «ник ваш» — второй узел отвечает то же самое. Метки дают порядок, но не дают момента, в который порядок окончательно известен; для этого и нужна рассылка общей последовательности из следующего абзаца.

Что действительно нужно — понимать, в какой момент последовательность окончательно зафиксирована. Это даёт рассылка общей последовательности (total order broadcast): протокол, который гарантирует, что все узлы получают одни и те же сообщения в одном и том же порядке, надёжно, даже при сбоях. По сути это и есть журнал репликации: «доставить сообщение» = «дописать в журнал», и все реплики, проигрывая журнал в одном порядке, обязательно сходятся к одному состоянию.

Почему узлов нечётное числоспросят на собеседовании

«Кворум — обычно больше половины» — это ответ, из которого не видно следствий, а следствия в эксплуатации самые практические.

Считать надо не «сколько узлов переживёт кластер», а сколько узлов должно остаться живыми, чтобы система работала. Для большинства из N узлов нужно N/2 + 1 живых, то есть выдержать можно (N-1)/2 отказов:

УзловКворумПереживает отказов
321
431
532
642
743

Вот и ответ, почему нечётное число. Пять узлов переживают два отказа, шесть — те же два: шестой узел не добавляет устойчивости, зато добавляет машину, которую надо обслуживать, и лишний обмен сообщениями на каждое решение. Чётное число вредит и по второй причине: при разделении ровно пополам (три против трёх) большинства нет ни у кого, и кластер останавливается целиком, тогда как в пятёрке разделение всегда даёт большинство одной из сторон.

Что происходит при потере большинства. Кластер не «работает медленнее» — он перестаёт принимать записи. Это не поломка, а осознанное поведение: без большинства нельзя гарантировать, что решение не противоречит другому решению, принятому в отделившейся половине. Чтения могут продолжаться (с оговоркой про устаревшие данные), записи — нет. Отсюда практическое следствие для размещения: три узла в двух дата-центрах — плохая идея, потому что потеря того, где стоят два, останавливает кластер; для двух площадок берут либо третью маленькую площадку под один узел-свидетель, либо смиряются с тем, что одна из площадок главная.

Сколько узлов брать. Три — стандарт для одной площадки: переживает перезапуск одного узла, в том числе плановый. Пять берут, когда нужно выдерживать отказ узла во время плановых работ на другом (то есть два одновременно) или когда узлы разнесены по трём площадкам. Больше семи не берут почти никогда: каждое решение требует ответа от большинства, и чем больше узлов, тем медленнее записи и тем дольше выборы ведущего.

Консенсус: всё это — одна задачаспросят на собеседовании

А вот и кульминация. Оказывается, все следующие задачи эквивалентны — реши одну по-настоящему, и получишь решение остальных:

  • рассылка общей последовательности;
  • линеаризуемая ячейка с операцией «сравни и присвой» (compare-and-set);
  • выбор ведущего;
  • ограничение уникальности;
  • атомарная фиксация распределённой транзакции.

Почему это одно и то же, видно на двух из них. Выбор ведущего — это compare-and-set над одной ячейкой «кто ведущий»: победит тот, чей запрос «если пусто — запиши меня» пройдёт первым, а для этого узлы должны согласиться, какой запрос первый. Ограничение уникальности — тот же compare-and-set над ячейкой «кому принадлежит ник».

Все они — это консенсус: заставить несколько узлов договориться об одном значении. И у консенсуса есть строгие требования: единое решение для всех, его нельзя отменить, оно должно быть реально предложенным кем-то из узлов, и — завершённость: алгоритм обязан рано или поздно принять решение, пока живо большинство.

Атомарная фиксация — знакомый частный случай. Классический протокол — двухфазная фиксация (2PC). Координатор рассылает всем участникам «приготовьтесь» (фаза 1), собирает от всех «да», записывает решение в свой журнал (это точка невозврата) и рассылает «фиксируйте» (фаза 2). Ответив «да», участник теряет право откатиться сам. И если координатор упал ровно между фазами, участник застревает в неопределённости — держит блокировки и ждёт, пока координатор воскреснет. Поэтому 2PC называют блокирующим: координатор здесь — единая точка отказа. Это, по сути, плохой консенсус.

фаза 1 приготовьтесь все ответили да блокировки взяты журнал координатора точка невозврата фаза 2 фиксируйте

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

Хороший консенсус — это Raft, Paxos, Zab, VSR. Все они опираются на кворум большинства (в системе из пяти узлов большинство — это минимум три, два таких большинства дают 3 + 3 = 6 голосов на 5 узлов, значит хотя бы один узел попал в оба → двух противоречащих решений не будет) и на «эпохи»: в каждой эпохе не больше одного ведущего, а решение принимает голосование большинства.

Есть и предел: результат FLP говорит, что в системе вообще без допущений о времени алгоритм консенсуса может не завершиться никогда. На практике это не мешает: тайм-ауты (та самая частичная синхронность из предыдущей статьи) и случайная задержка перед новыми выборами ведущего выводят алгоритм из тупика.

Практический вывод один и жёсткий: не пишите консенсус сами. Сделать это правильно почти невозможно. Возьмите готовый сервис — ZooKeeper или etcd: они дают линеаризуемое хранилище, выбор ведущего, распределённые блокировки и ограждающие маркеры «из коробки». Именно так внутри устроены HBase и Kubernetes. Kafka раньше тоже держала координацию в ZooKeeper, но ушла к собственной реализации Raft — режим KRaft: в версии 3.3 его объявили пригодным для боевой работы, а в 4.0 поддержку ZooKeeper убрали совсем. На вывод это работает даже лучше: либо берёшь готовый консенсус, либо пишешь свой годами и силами отдельной команды.

Сколько стоит консенсус

«Линеаризуемость медленная всегда» — верное утверждение без числа, а число тут простое и объясняет все архитектурные решения вокруг.

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

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

Отсюда три архитектурных правила, которые применяют все.

Через консенсус не гоняют трафик данных. Служба согласования хранит мелочи: кто ведущий, кому принадлежит блокировка, где лежит какой раздел, какая версия конфигурации. Килобайты, меняющиеся редко. Рабочие данные живут в обычном хранилище, которое читает эти мелочи.

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

Где можно — не консенсус. Уникальность ника проверяется уникальным индексом в одной базе, а не распределённым протоколом. Порядок событий держится ключом партиции. Согласованность двух сервисов — последовательностью шагов с отменой. Консенсус берут тогда, когда без общего решения действительно нельзя, и это довольно узкий список: один ведущий, одна блокировка, одно членство в кластере.

Как это выглядит на практике

Совет «возьмите ZooKeeper или etcd» оставляет открытым главное: что именно вы там делаете. Обе службы дают одно и то же — линеаризуемое хранилище маленьких значений с двумя механизмами: временем жизни, привязанным к живости клиента, и уведомлением об изменении. Из этих двух собирается всё остальное.

Выбор ведущего. Последовательность действий одинакова для обеих служб:

  1. Клиент создаёт запись с признаком «живёт, пока я жив»: в ZooKeeper это временный узел (ephemeral), в etcd — ключ с арендой (lease), которую клиент продлевает сердцебиением.
  2. Создание идёт с условием «только если ключа ещё нет». Успех означает «я ведущий», отказ — «ведущий кто-то другой».
  3. Проигравшие подписываются на исчезновение этого ключа и ждут. Как только владелец умер или перестал продлевать аренду, служба удаляет ключ сама, подписчики просыпаются и повторяют шаг 2.
  4. Ведущий получает при создании номер (ревизию), и это тот самый ограждающий маркер: его прикладывают к каждой записи в общий ресурс.

Главное в этой схеме — пункт 4. Без него остаётся та же дыра, что и с любой блокировкой: ведущий, замерший на паузе сборщика мусора, теряет аренду, но сам об этом не знает и продолжает писать.

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

Если двойная запись означает двойное списание, нужна либо служба согласования с маркером, либо проверка версии в самом хранилище — оптимистичная блокировка, о которой раздел ниже. Разбор этой ловушки с кодом — в задаче про лок без ограждения.

Готовые обёртки. Писать эти шаги руками не нужно. Для etcd у официального клиента Go clientv3 есть пакет concurrency с NewSession (аренда), NewElection и NewMutex, и именно его логику повторяют клиенты других языков: etcd3 для Node с классами Election и Lock, python-etcd3 с Lock и Lease, jetcd для Java с примитивами аренды и наблюдения. Для ZooKeeper рецепты выбора ведущего и блокировки дают Apache Curator в Java (LeaderLatch, LeaderSelector, InterProcessMutex) и kazoo в Python (Election, Lock); у go-zookeeper рецептов нет, и их собирают из эфемерных узлов руками. В Kubernetes отдельная служба часто вообще не нужна: там уже есть etcd, и выбор ведущего делается штатным механизмом аренды через API кластера (в Go это k8s.io/client-go/tools/leaderelection).

А что считать достаточным. Схема считается правильной, если выполняются три условия: ключ живёт по аренде, а не по фиксированному сроку, который вы назначили на глазок; ведущий получает монотонный номер и прикладывает его к записям; ресурс этот номер проверяет. Если хоть одно не выполняется, у вас не блокировка, а соглашение о хороших манерах.

2PC на практике: XA и Jakarta Transactionsспросят на собеседовании

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

XA — стандарт взаимодействия менеджера транзакций с ресурсами (базой, брокером), реализующий ровно описанные выше две фазы. Драйверы это умеют: PGXADataSource у PostgreSQL, XA-фабрики соединений у брокеров. Jakarta Transactions (прежнее имя — JTA) — это тот самый программный интерфейс, через который приложение управляет такой транзакцией; менеджер транзакций даёт сервер приложений или отдельная библиотека (Narayana, Atomikos).

Как это выглядит: @Transactional в приложении с настроенным менеджером XA охватывает и запись в базу, и отправку в очередь, а фиксация проходит в две фазы. Соблазн очевиден — «база и брокер атомарно, без всяких таблиц исходящих сообщений». В Go, Node и Python такого менеджера в экосистеме нет: драйверы умеют PREPARE TRANSACTION у PostgreSQL, но координатора, который сведёт базу и брокер, никто не пишет, и распределённую транзакцию там не строят вовсе, а сразу берут таблицу исходящих.

Почему в новых сервисах так почти не делают. Все недостатки 2PC остаются: координатор — единая точка отказа, а его падение между фазами оставляет ресурсы в неопределённости с удерживаемыми блокировками, и разгребать это приходится руками, по журналу восстановления. К этому добавляются практические: менеджер транзакций требует своего постоянного хранилища для журнала (то есть перестаёт быть безразличным к перезапускам контейнера), поддержка XA у брокеров разного качества и часто медленная, а Kafka её не поддерживает вовсе. В итоге XA живёт там, где уже стоял, — в системах на серверах приложений с базой и брокером JMS, — а между сервисами по сети её не тянут.

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

Где это применяется

Каждый раз, когда системе нужен ровно один ведущий, уникальное имя, «атомарно занять ресурс» или согласованный порядок событий, — вы упёрлись в консенсус, даже если не называли его этим словом. Практическая рамка: не тяните линеаризуемость и распределённые транзакции без нужды (они медленные и хрупкие — приложения обходят их сагами и outbox); а там, где согласие узлов действительно необходимо, — не изобретайте протокол, отдайте координацию ZooKeeper или etcd.

Где спотыкаются начинающие:

  • Путают линеаризуемость и сериализуемость. Первая — про актуальность одного объекта, вторая — про изоляцию транзакций. Гарантии разные.
  • Расшифровывают CAP как «2 из 3». Разрыв сети не выбирают; выбор «согласованность или доступность» встаёт только в момент разрыва.
  • Упорядочивают события меткой Лампорта и думают, что решили уникальность. Полный порядок известен только постфактум; для проверки «сейчас» нужна рассылка общей последовательности.
  • Пишут свой распределённый лок или выбор ведущего. Это консенсус, который почти невозможно реализовать верно. Берите ZooKeeper/etcd.
  • Тянут 2PC между микросервисами. Это блокирующий протокол с координатором — единой точкой отказа; в микросервисах почти всегда лучше сага.
Дополнительно: при первом чтении можно пропустить

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

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

Версия записи. Самый честный и самый простой способ: у объекта номер версии, клиент читает версию вместе с данными и присылает её с правкой, сервер применяет правку, только если версия совпала, иначе отвечает конфликтом (412 или 409 в REST, о чём статья про заголовки). Второй менеджер видит «карточку изменили, посмотрите и повторите» и решает сам. Ничего не теряется, но конфликт поднимается до человека, и при частых правках это раздражает.

Версия по полям. Конфликт целой записи часто мнимый: один менеджер поправил цену, другой описание, и обе правки совместимы. Версия хранится на каждое поле (или правка описывается как набор полей, а не как новый объект целиком), и конфликт объявляется только по полю, которое правили оба. PATCH с перечислением изменённых полей и сравнение по этим полям на сервере решают большинство «одновременных» правок без вмешательства человека. Для наборов вместо перезаписи применяют операции: «добавить тег», «убрать тег», которые сходятся независимо от порядка.

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

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

Коротко

  • Линеаризуемость это иллюзия единственной копии, она дорога и нужна редко; причинного порядка чаще достаточно, и его дают логические метки, а не настенные часы.
  • CAP не «два из трёх»: выбор между согласованностью и доступностью встаёт только в момент разрыва сети.
  • Выбор ведущего, уникальность, атомарная фиксация и общий порядок это одна задача, консенсус; его не пишут сами, а берут из ZooKeeper или etcd.
  • 2PC между микросервисами это блокирующий протокол с единой точкой отказа; в сервисах почти всегда сага.
  • Одновременные правки: версия записи с конфликтом человеку для денег и документов, версия по полям для карточек, бесконфликтные типы для работы без сети; LWW только там, где потеря правки безвредна.
  • Метка Лампорта — счётчик плюс идентификатор узла: перед операцией увеличить, в сообщении передать, при получении взять максимум и прибавить единицу; порядок это даёт, а момента «занято прямо сейчас» — нет.
  • Узлов берут нечётное число: пять переживают два отказа, шесть те же два, а разделение пополам останавливает кластер целиком; при потере большинства записи прекращаются, и это осознанное поведение.
  • Одна запись через консенсус стоит обмена с большинством и записи в журнал: единицы миллисекунд в стойке, сотни между регионами. Поэтому через консенсус гоняют мелочи, а ведущего выбирают редко.
  • Выбор ведущего и блокировка — это ключ с арендой, условное создание, подписка на исчезновение и монотонный номер в качестве ограждающего маркера; готовое дают clientv3/concurrency в Go, etcd3 в Node, kazoo и python-etcd3 в Python, Curator и jetcd в Java, в Kubernetes — штатная аренда.
  • 2PC в Java — это XA и Jakarta Transactions с отдельным менеджером транзакций, в Go, Node и Python менеджера нет вовсе; в новых сервисах вместо неё берут таблицу исходящих, шаги с отменой и идемпотентность.

Что почитать дальше