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

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

Как это настраивается на практике, мы разбираем в статьях про репликацию в PostgreSQL и replica set в MongoDB. Эта статья — на уровень выше и без привязки к конкретной базе: что вообще ломается, когда копий несколько, и какие бывают модели репликации. Их три, и различаются они ответом на один вопрос: кто принимает записи и кто разбирается с конфликтами. Начнём с самой простой и частой модели — один ведущий, много реплик, — потому что почти все грабли живут именно в ней.

Почему реплика отстаёт

Когда вы пишете что-то в мастер, изменение не появляется на репликах мгновенно — оно должно до них «доехать» по сети. Обычно это доли секунды, и никто не замечает. Но под нагрузкой, при всплеске трафика или проблемах с сетью реплика может отстать на секунды, а иногда и на минуты.

Такой режим — «реплики отстают, но догонят, если перестать писать» — называют конечной согласованностью (eventual consistency). Слово «конечная» звучит как обещание, но на деле оно ни к чему не обязывает: никто не говорит, когда именно реплики догонят. Обычно быстро, но гарантии на время нет.

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

Три аномалии отставания — и как с ними бороться

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

Противоядие — гарантия «читай свои записи» (read-your-writes): свои собственные данные пользователь должен читать либо с мастера, либо с реплики, которая уже догнала момент его записи. Тогда «пропавший комментарий» не случится. Как это делают в PostgreSQL — в статье про репликацию.

2. «Время идёт назад». Вы обновляете страницу два раза подряд. Первый запрос попал на реплику, которая почти догнала мастер, — комментарий виден. Второй запрос попал на другую реплику, отставшую сильнее, — и комментарий пропал. Данные будто откатились в прошлое.

Противоядие — монотонные чтения (monotonic reads): каждое следующее чтение не должно быть «старее» предыдущего. Самый простой способ добиться этого — стабильно отправлять одного и того же пользователя на одну и ту же реплику (например, выбирая реплику по хешу его id), чтобы он не прыгал между «более свежей» и «более отставшей».

3. «Ответ раньше вопроса». В переписке видно ответ на сообщение, но самого сообщения ещё нет: реплика с ответом догнала мастер быстрее, чем реплика с вопросом. Причина и следствие поменялись местами.

Противоядие — согласованное префиксное чтение (consistent prefix reads): если записи произошли в каком-то порядке, то и читаться они должны в том же порядке. Особенно болезненно это в системах, где данные разрезаны на части (шарды): у разных частей нет общего порядка записей, и «склеить» их в правильной последовательности сложнее.

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

Когда падает мастер: failover и его ловушки

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

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

Расщепление мозга (split brain). Иногда старый мастер не умер, а просто на время пропал из сети — и вот уже два узла считают себя мастером и оба принимают записи. Данные расходятся, и потом их приходится мучительно сводить. Поэтому системы придумывают способы гарантировать, что мастер в каждый момент один.

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

Несколько ведущих (multi-leader): свобода ценой конфликтов

Иногда один мастер мешает не из-за нагрузки, а из-за географии. Тогда ведущих делают несколько. Три типичных сценария:

  • Несколько дата-центров. В каждом ЦОДе свой мастер: запись обрабатывается локально и потом не спеша реплицируется в остальные. Пользователю не надо ждать, пока запрос сбегает на другой континент и обратно, а отказ целого дата-центра не останавливает приём записей.
  • Офлайн-клиенты. Календарь на телефоне и на ноутбуке принимает записи даже без сети. По сути каждое устройство — маленький мастер со своей локальной базой, а синхронизация — та же репликация, только с задержкой в часы.
  • Совместное редактирование. Google Docs, где несколько человек правят документ одновременно, — это та же идея, доведённая до предела: своя локальная копия в каждой вкладке.

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

  • «Выигрывает последний» (last write wins, LWW). У каждой записи есть метка времени, побеждает та, что «позже». Просто и распространено (в Cassandra это вообще единственный вариант). Но у этого способа злая цена — тихая потеря данных: проигравшая запись, за которую клиент уже получил «сохранено», молча исчезает. Для кеша сойдёт; для всего, что жалко терять, — нет.
  • Слияние. Сохранить оба варианта и потом слить: либо кодом приложения при следующем чтении, либо автоматически — специальными структурами данных CRDT (они спроектированы так, что одновременные изменения сливаются без потерь: счётчики, множества, тексты).
  • Не допускать конфликтов. Самый практичный вариант — сделать так, чтобы все изменения одной записи всегда шли через один и тот же мастер (данные пользователя — всегда в его «родной» дата-центр). Тогда для этой записи система фактически становится «один ведущий», и конфликтов нет.

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

Без ведущего (leaderless): кворумы вместо мастера

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

Чтобы это работало, договариваются о числах. Пусть всего реплик n, запись считается успешной после w подтверждений, а чтение опрашивает r узлов. Магическое условие — кворум: если w + r > n, то множество узлов, куда мы записали, и множество, откуда читаем, обязательно пересекаются хотя бы в одном узле — а значит, при чтении мы точно наткнёмся на узел со свежими данными. Типичный набор: n=3, w=2, r=2. Одна реплика может лежать, а запись и чтение продолжают работать — и отдельная процедура failover тут вообще не нужна.

Отставшие узлы догоняют двумя способами: исправление при чтении (read repair — клиент, заметив у одного узла устаревший ответ, тут же дописывает туда свежее значение) и фоновый процесс анти-энтропии, который сверяет реплики между собой и докатывает разницу.

Только не считайте кворум железной гарантией — у него есть оговорки:

  • Нестрогий кворум (sloppy quorum): при проблемах с сетью запись могут временно принять «не те» узлы (с последующей досылкой на правильные — hinted handoff). В этот момент w и r могут и не пересечься.
  • Одновременные записи всё равно требуют разрешения конфликтов — и часто это тот же LWW с той же тихой потерей.
  • Следить за «отставанием» тут сложнее, чем в модели с одним мастером, где есть чёткая позиция в журнале.

Честный итог: кворумные базы по своей природе конечно-согласованные, а числа w и r управляют не гарантией, а вероятностью прочитать устаревшее.

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

Выбор модели репликации — это выбор, где именно будет болеть:

  • Один ведущий (single-leader) — прост и даёт понятный порядок записей, но требует аккуратного failover и упирается в один узел по нагрузке на запись.
  • Несколько ведущих (multi-leader) — развязывает географию и офлайн, но приносит конфликты записи.
  • Без ведущего (leaderless) — убирает failover, но взамен даёт кворумную арифметику и лишь вероятностные гарантии.

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

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

  • Читают с реплики всё подряд — и ловят «пропавшие комментарии» и «время назад». Гарантии чтения выбирают сознательно, под конкретный сценарий, а не «как получится».
  • Верят в LWW. «Выигрывает последний» звучит безобидно, а на деле означает «проигравшие записи исчезают молча, хотя клиент получил подтверждение».
  • Считают w+r>n абсолютной гарантией — нестрогие кворумы, одновременные записи и восстановление из старой реплики оставляют щели.
  • Включают multi-leader ради «надёжности» внутри одного дата-центра — и получают конфликты записи без всякой выгоды: в одном ЦОДе честнее держать одного ведущего.

Что почитать дальше: репликация PostgreSQL — один ведущий на практике, включая «читай свои записи» и мониторинг отставания; репликация и шардинг MongoDB — replica set и failover; строительные блоки системного дизайна — как репликация встраивается в общую картину.