В прошлом уроке драйвер повторял запись, когда сервер пропадал, а w: 'majority' ждал подтверждения от нескольких узлов. Пора посмотреть на сами узлы. В обзорном уроке мы назвали три слова: первичный, вторичный, выборы. Сегодня поднимем настоящий replica set из трёх серверов, сломаем его и посмотрим, как он чинится сам.
Хор и запевала
В хоре поёт запевала, а остальные повторяют за ним строчку за строчкой. Если запевала охрип, хор не замолкает: певцы быстро договариваются, кто ведёт дальше, и песня продолжается. Главное правило: новый запевала выбирается только большинством. Двое из пяти не могут объявить себя главными, иначе зазвучат две песни сразу.
Replica set устроен так же. Первичный узел ведёт, вторичные повторяют его изменения, а при аварии большинство выбирает нового ведущего.
Три узла в Docker
Для опыта хватит трёх контейнеров в одной сети. Флаг --replSet rs0 говорит каждому серверу, что он член набора с именем rs0:
docker network create mgrs
docker run -d --name mgrs1 --network mgrs mongo:8 --replSet rs0 --bind_ip_all
docker run -d --name mgrs2 --network mgrs mongo:8 --replSet rs0 --bind_ip_all
docker run -d --name mgrs3 --network mgrs mongo:8 --replSet rs0 --bind_ip_all
Сами по себе они ещё не знают друг о друге. Состав набора задаёт rs.initiate, его выполняют один раз на любом из узлов:
rs.initiate({
_id: 'rs0',
members: [
{ _id: 0, host: 'mgrs1:27017' },
{ _id: 1, host: 'mgrs2:27017' },
{ _id: 2, host: 'mgrs3:27017' }
]
})
Сервер отвечает { ok: 1, ... }, а через несколько секунд узлы проводят первые выборы. Кто кем стал, показывает rs.status(). Его ответ занимает пару экранов, поэтому оставим из каждого узла три поля:
rs.status().members.map(m => ({ name: m.name, stateStr: m.stateStr, health: m.health }))
[
{ name: 'mgrs1:27017', stateStr: 'PRIMARY', health: 1 },
{ name: 'mgrs2:27017', stateStr: 'SECONDARY', health: 1 },
{ name: 'mgrs3:27017', stateStr: 'SECONDARY', health: 1 }
]
Полная запись об узле длиннее: время последней применённой операции, у кого узел берёт данные (syncSourceHost), номер конфигурации. Пока хватит трёх полей. Заметь и приглашение mongosh: теперь оно показывает роль узла, например rs0 [direct: primary] test>.
Кто принимает запись
Записи принимает только первичный узел. Загрузим на него учебную базу магазина и подключимся ко второму узлу:
db.books.insertOne({ _id: 99, title: 'X' })
rs0 [direct: secondary] test> Uncaught MongoServerError[NotWritablePrimary]: not primary
Вторичный узел отказывается писать. А читать с него в прямом подключении mongosh разрешает: db.books.countDocuments() на mgrs2 вернёт 9, данные уже скопированы.
Как они туда попали? Через oplog. Это коллекция oplog.rs в служебной базе local. Поменяем цену и посмотрим последнюю запись журнала (из длинного документа оставлены главные поля):
db.books.updateOne({ _id: 2 }, { $set: { price: 410 } })
db.getSiblingDB('local').oplog.rs.find({ ns: 'test.books' }).sort({ $natural: -1 }).limit(1)
[
{
...
op: 'u',
ns: 'test.books',
o: { '$v': 2, diff: { u: { price: 410 } } },
o2: { _id: 2 },
ts: Timestamp({ t: 1791605206, i: 1 }),
...
}
]
op: 'u' значит обновление, o2 говорит, какой документ, а o что поменять. Важная деталь: в журнал попадает не команда $set, а готовый результат. Повторить такую запись дважды безопасно, и вторичный узел ничего не испортит, если применит её снова. Тот же приём лежит в основе репликации PostgreSQL, только там журнал называется WAL и хранит байты страниц.
урок из курса
Урок из курса «MongoDB с нуля». Курс покупается один раз и остаётся навсегда: 500 ₽.
Оплата пока не подключена: премиум и курсы открываются промокодом.