Раздел 36 · MongoDB с нуля

Replica Set: отказоустойчивость

junior~20 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

В прошлом уроке драйвер повторял запись, когда сервер пропадал, а 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 ₽.

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