Как устроена классическая репликация «ведущий — ведомые», мы разбираем на практике: streaming replication в PostgreSQL и replica set в MongoDB. Эта статья — этажом выше: что именно ломается, когда реплика отстаёт, зачем некоторым системам несколько ведущих узлов сразу и как базы семейства Dynamo обходятся вообще без ведущего. Три модели репликации — это три разных ответа на один вопрос: кто принимает записи и кто разрешает конфликты.
Аномалии задержки: словарь гарантий
Асинхронная реплика отстаёт от ведущего — обычно на доли секунды, под нагрузкой на секунды и минуты. Состояние «реплики догонят, если перестать писать» называется конечной согласованностью (eventual consistency), и слово «конечная» намеренно ни к чему не обязывает. На практике задержка проявляется тремя конкретными аномалиями, у каждой — своя гарантия-противоядие:
- Пользователь не видит собственную запись. Отправил комментарий (запись ушла на ведущий), обновил страницу (чтение попало на отставшую реплику) — комментария нет, выглядит как потеря данных. Гарантия — чтение своих записей (read-your-writes): свои данные пользователь читает или с ведущего, или с реплики, догнавшей момент его записи. Практические приёмы — в статье про репликацию PostgreSQL.
- Время идёт назад. Два запроса подряд попали на разные реплики: первая почти догнала, вторая отстаёт сильнее — пользователь увидел комментарий, обновил страницу, комментарий исчез. Гарантия — монотонные чтения: следующие чтения не старее предыдущих; проще всего — стабильно направлять пользователя на одну и ту же реплику (например, по хешу id).
- Ответ раньше вопроса. В переписке реплика с ответом догнала раньше, чем реплика с вопросом, — наблюдатель видит причинно-следственную связь вывернутой. Гарантия — согласованное префиксное чтение: если записи произошли в некотором порядке, читаются они в том же порядке. Больнее всего это в шардированных системах, где у секций нет общего порядка записей.
Рабочее правило: проектируя чтение с реплик, спросите себя, какая из трёх аномалий ударит по вашему сценарию, — и обеспечьте ровно ту гарантию, что нужна. Прикидываться синхронной системой, будучи асинхронной, — источник самых загадочных багов.
Отдельная грабля самой модели «ведущий — ведомые» — failover: при отказе ведущего одну из реплик повышают, и если репликация была асинхронной, неотреплицированные записи старого ведущего обычно выбрасывают. Классический инцидент GitHub: повышенная отставшая реплика начала выдавать уже использованные первичные ключи — и чужие данные показались не тем пользователям. Вторая опасность — расщепление мозга (split brain): два узла одновременно считают себя ведущими и оба принимают записи.
Multi-leader: несколько ведущих и конфликты
Иногда один ведущий — это узкое место не по нагрузке, а по географии и связности. Три сценария, где ведущих делают несколько:
- Несколько дата-центров: ведущий в каждом ЦОДе, запись обрабатывается локально и асинхронно реплицируется в остальные — пользователи не ждут межконтинентального круга, а отказ целого ЦОДа не останавливает запись.
- Офлайн-клиенты: календарь на телефоне и ноутбуке принимает записи без сети — каждое устройство фактически ведущий узел со своей локальной базой, а синхронизация — асинхронная репликация с задержкой в часы.
- Совместное редактирование: Google Docs — та же модель, доведённая до предела: локальная реплика в каждой вкладке.
Цена модели одна, и она серьёзная: конфликты записи. Два ведущих одновременно приняли изменение одной записи, оба ответили «успех» — а при обмене изменениями выяснилось, что они несовместимы. Просить пользователя разобраться уже поздно. База обязана разрешить конфликт конвергентно — так, чтобы все реплики сошлись к одному значению:
- «Выигрывает последний» (last write wins, LWW): у каждой записи метка времени, побеждает поздняя. Просто, повсеместно (единственный вариант в Cassandra) — и это тихая потеря данных: проигравшие записи, за которые клиент получил подтверждение, молча исчезают. Приемлемо для кеша; для всего, что жалко терять, — нет.
- Слияние значений: сохранить оба варианта и слить — кодом приложения при следующем чтении или автоматически структурами CRDT (типы данных, спроектированные так, что конкурентные изменения сливаются без потерь — счётчики, множества).
- Предотвращение: самый практичный вариант — маршрутизировать все изменения конкретной записи через один и тот же ведущий узел (запись пользователя — всегда в его «родной» ЦОД). Тогда для каждой записи система фактически single-leader, и конфликтов нет, пока не случится failover между ЦОДами.
Ключ к пониманию конфликтов — отношение «происходит до» (happens-before): операция Б зависит от А, если знала про А. Если ни одна не знала про другую — они конкурентны, и именно тогда нужен механизм разрешения. Физические часы для этого ненадёжны (они рассинхронизированы) — базы отслеживают зависимость версиями и векторами версий.
Leaderless: кворумы вместо ведущего
Третья модель выбрасывает ведущего совсем (Dynamo-стиль: Cassandra, Riak): клиент шлёт запись параллельно нескольким репликам и считает её успешной после подтверждения от w узлов из n; читает тоже из r узлов сразу, сравнивая версии. Условие кворума: при w + r > n множества записи и чтения пересекаются — хотя бы один из опрошенных узлов видел последнюю запись. Типично n=3, w=r=2: одна реплика может лежать, а запись и чтение продолжаются — failover как процедура не нужен вовсе.
Отставшие узлы догоняют двумя механизмами: исправление при чтении (read repair — клиент, увидев устаревший ответ, дописывает туда свежее значение) и фоновая анти-энтропия (процесс сверяет реплики и докатывает разницу).
Оговорки, из-за которых кворум — не абсолютная гарантия: нестрогий кворум (sloppy quorum) при сбоях сети принимает записи на «чужие» узлы с последующей направленной передачей (hinted handoff) — и тогда w и r могут не пересечься; конкурентные записи всё равно требуют разрешения конфликтов (тот же LWW с теми же потерями); мониторить «отставание» тут сложнее, чем у single-leader, где есть позиция в журнале. Итог честный: кворумные базы настроены на конечную согласованность, а w и r управляют вероятностью устаревшего чтения, не гарантией.
Где это применяется
Выбор модели — это выбор, где болит: single-leader прост и даёт порядок записей, но требует failover и упирается в один узел; multi-leader развязывает географию ценой конфликтов; leaderless убирает failover ценой кворумной арифметики и вероятностных гарантий. В типовом бэкенде правильный дефолт — single-leader (PostgreSQL) с явными гарантиями чтения; остальные модели включаются, когда география или доступность записи того требуют.
Где спотыкаются начинающие:
- Читают с реплики всё подряд — и ловят «пропавшие комментарии» и «время назад». Гарантии чтения выбираются сознательно, по сценарию.
- Верят LWW. «Выигрывает последний» звучит безобидно, а означает «проигравшие записи исчезают молча, хотя клиент получил подтверждение».
- Считают w+r>n железной гарантией — нестрогие кворумы, конкурентные записи и восстановление из старой реплики оставляют щели.
- Включают multi-leader ради «надёжности» внутри одного ЦОДа — получают конфликты записи без какой-либо выгоды: в одном ЦОДе честнее один ведущий.
Что почитать дальше: репликация PostgreSQL — single-leader на практике, включая read-after-write и мониторинг отставания; репликация и шардинг MongoDB — replica set и failover; строительные блоки — где репликация живёт в общей картине. Первоисточник — Мартин Клеппман, «Высоконагруженные приложения», глава 5.