Replica Set: отказоустойчивость
Один сервер БД — это одна точка отказа: упал mongod, и приложение лежит. Replica set убирает эту проблему, держа несколько копий данных и переключаясь на живую автоматически.
Replica set — группа процессов
mongod, хранящих одинаковый набор данных. Один из них —PRIMARY(принимает записи), остальные —SECONDARY(копируют данные и готовы заменить primary при сбое).
Зачем это на практике
В проде MongoDB почти всегда запускают не одиночным сервером, а replica set. Причин две: отказоустойчивость (high availability) и сохранность данных (redundancy). Если в продакшене один mongod упадёт из-за сбоя диска, перезагрузки хоста или ошибки ядра, приложение без реплик потеряет доступ к базе — а возможно, и к данным. С replica set данные лежат на нескольких машинах, и при падении primary кластер сам выбирает нового. Приложение этого почти не замечает: драйвер обнаруживает смену primary и продолжает работать.
Минимальная боевая конфигурация — три узла с данными (схема PSS: primary + два secondary). Три — потому что для выборов нужно большинство голосов, а с тремя узлами кластер переживёт падение любого одного и сохранит большинство (2 из 3).
Роли участников
PRIMARY — единственный узел, принимающий записи. Все insert/update/delete идут на него. В replica set в любой момент времени ровно один primary.
SECONDARY — реплика. Постоянно копирует операции с primary и держит идентичную копию данных. С secondary можно читать (об этом ниже) и любой из них может стать primary при сбое.
ARBITER — особый узел: он не хранит данные, а только участвует в голосовании, чтобы разбить «ничью» при чётном числе узлов. Арбитр дешёвый (не нужен диск под данные), но он не повышает сохранность — это просто голос. Современная рекомендация — избегать арбитров и держать нечётное число узлов с данными; схема P-S-A коварна (об этом в «Частых ошибках»).
Поднимаем replica set
Узлы запускают с общим именем набора (--replSet), а затем инициализируют конфигурацию из mongosh:
// каждый mongod стартует с одинаковым именем набора
mongod --replSet rs0 --port 27017 --dbpath /data/db1
mongod --replSet rs0 --port 27018 --dbpath /data/db2
mongod --replSet rs0 --port 27019 --dbpath /data/db3
// один раз — инициализация набора (подключившись к любому узлу)
rs.initiate({
_id: "rs0",
members: [
{ _id: 0, host: "mongo1:27017" },
{ _id: 1, host: "mongo2:27018" },
{ _id: 2, host: "mongo3:27019" }
]
})
rs.status() // увидим, кто PRIMARY, кто SECONDARY
rs.conf() // текущая конфигурация набора
rs.add("mongo4:27020") // добавить узел позже
Приложение подключается не к одному узлу, а ко всему набору через параметр replicaSet — драйвер сам найдёт primary:
mongodb://mongo1:27017,mongo2:27018,mongo3:27019/shop?replicaSet=rs0&w=majority
Чтение со вторичных узлов
По умолчанию все операции (и записи, и чтения) идут на primary — это даёт строгую согласованность. Но чтение можно разрешить и с secondary через read preference:
| Значение | Откуда читать |
primary | только primary (по умолчанию, всегда свежие данные) |
primaryPreferred | primary, а если его нет — secondary |
secondary | только secondary (разгрузить primary) |
nearest | ближайший по сети узел (минимальная задержка) |
Важная оговорка: secondary отстаёт от primary на время репликации (миллисекунды, но иногда секунды). Поэтому чтение с secondary — это eventual consistency: можно прочитать слегка устаревшие данные. Для аналитики, отчётов и георазнесённых читателей это нормально; для «прочитать только что записанное» — нет.
Как это работает под капотом
Сердце репликации — oplog (operations log), специальная capped-коллекция local.oplog.rs на primary. Каждая изменяющая операция записывается в oplog в идемпотентной форме: например, $inc на единицу превращается в «установить поле в конкретное значение», чтобы повторное применение не сломало данные. Secondary постоянно «тянут» (tail) oplog primary и применяют операции у себя. Размер oplog определяет окно репликации: если secondary отстанет дальше, чем хранит oplog, ему потребуется полная пересинхронизация.
За живучесть отвечают heartbeat-сообщения: узлы пингуют друг друга каждые ~2 секунды. Если primary не отвечает дольше electionTimeoutMillis (по умолчанию 10 секунд), оставшиеся узлы запускают выборы. Побеждает узел, набравший большинство голосов и имеющий самые свежие данные; он становится новым primary. Протокол выборов (protocol version 1) построен на идеях Raft. Весь failover занимает обычно около 10–12 секунд, в течение которых записи временно невозможны — это плата за автоматику.
С сохранностью записи связан write concern. Значение w: 1 (умолчание во многих драйверах) подтверждает запись, как только её принял primary — но если primary упадёт до репликации, такая запись может быть откатена (rollback). w: "majority" ждёт подтверждения от большинства узлов: такая запись переживёт failover. Для важных данных используйте w: "majority".
Частые ошибки
- Схема P-S-A и ожидание durability. Если у вас primary + один secondary + arbiter и secondary выходит из строя, записи с
w: "majority"начнут блокироваться: большинство узлов с данными недостижимо. Арбитр голос даёт, а данные — нет. Безопаснее три узла с данными. - Чтение с secondary и ожидание свежих данных. Пользователь сохранил профиль, тут же открыл его и видит старую версию — классика. Для «read-your-own-writes» читайте с primary или используйте подходящий read/write concern.
- Полагаться на
w: 1для критичных данных. Платёж подтверждён primary, primary упал — платёж потерян. Используйтеw: "majority"там, где потеря недопустима. - Чётное число голосующих узлов. При сетевом разделении пополам ни одна половина не получит большинство — кластер останется без primary. Держите нечётное число голосов.
- Не следить за окном oplog. Под нагрузкой маленький oplog «прокручивается» быстро; отставший secondary вываливается в полную пересинхронизацию. Мониторьте лаг репликации.
Итоги
- Replica set — стандарт для прода: несколько копий данных + автоматическое переключение при сбое.
- Один
PRIMARYпринимает записи;SECONDARYреплицируют и готовы его заменить;ARBITERтолько голосует и данных не хранит. - Минимум для боя — три узла с данными; число голосующих узлов держите нечётным.
- Выборы запускаются при недоступности primary и занимают ~10–12 секунд; репликация идёт через идемпотентный
oplog. - Чтение с secondary разгружает primary, но даёт eventual consistency; для сохранности записей используйте
w: "majority".