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

Модели согласованности и retryable writes

junior~20 мин

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

В прошлом уроке мы выбирали, сколько узлов должны подтвердить запись и какие данные можно читать. Сегодня два последних вопроса про надёжность. Первый: что будет, если читать со вторичных узлов, и как при этом не увидеть прошлое. Второй: что делать, если связь оборвалась посреди записи и непонятно, прошла она или нет. Примеры сняты с настоящего replica set из трёх узлов: в песочнице узел один.

Копии расписания

На вокзале расписание висит в главном зале и копии на платформах. Главное правят сразу, копии переписывает дежурный, обходя платформы. Если поезд перенесли минуту назад, на дальней платформе ещё висит старое время. Смотреть на копию быстрее, но она может отставать. А если ты сам только что попросил перенести поезд, тебе обидно увидеть на копии старое время, как будто тебя не услышали.

Вторичные узлы replica set это такие копии. Они повторяют изменения первичного из oplog с небольшой задержкой, и чтение с них может вернуть немного устаревшие данные. Насколько тебя это устраивает, решают две настройки: read preference и причинная согласованность.

Read preference: откуда читать

Read preference говорит драйверу, к какому узлу идти за чтением:

РежимЧитаем сЦена
primaryтолько первичногопо умолчанию, данные свежие
primaryPreferredпервичного, при аварии со вторичногопри аварии можно увидеть старое
secondaryтолько со вторичныхразгружает первичный, данные могут отставать
secondaryPreferredсо вторичных, если их нет, с первичногото же
nearestс ближайшего по сетибыстро для далёких клиентов, может отставать

Запись при этом всегда идёт на первичный. Read preference касается только чтения.

Устаревшее чтение вживую

Обычно вторичные узлы отстают на миллисекунды, и разницу не поймать. Чтобы увидеть её наверняка, мы заморозили оба вторичных узла командой db.fsyncLock(): она останавливает запись на узле, и он перестаёт применять изменения из oplog. Потом подключились ко всему replica set и записали заказ:

db.orders.insertOne({ _id: 6, customer_id: 3, status: 'new', items: [] }, { writeConcern: { w: 1 } })
db.orders.findOne({ _id: 6 })
db.getMongo().setReadPref('secondary')
db.orders.findOne({ _id: 6 })
db.orders.countDocuments()
{ acknowledged: true, insertedId: 6 }
{ _id: 6, customer_id: 3, status: 'new', items: [] }
null
5

С первичного заказ читается, а после переключения на secondary его нет, и заказов в магазине пять, а не шесть. Покупатель только что оформил заказ, открыл «мои заказы» и не нашёл его. После db.fsyncUnlock() вторичные узлы догнали первичный, и то же чтение вернуло заказ.

Мы использовали w: 1, потому что с замороженными вторичными узлами запись с w: 'majority' ждала бы подтверждения вечно. В обычной жизни отставание короткое, но оно есть всегда, и при нагрузке или сетевых проблемах растёт до секунд.

Причинная согласованность

Хочется, чтобы покупатель видел свои собственные действия, даже если читает со вторичного узла. Это гарантия причинной согласованности: внутри одной сессии каждая операция видит результаты предыдущих.

Как это устроено: после каждой операции сессия запоминает время кластера, когда она случилась. Следующее чтение отправляется вместе с этим временем, и вторичный узел, который ещё не догнал этот момент, ждёт, пока догонит. Вот пример на драйвере Node.js:

const session = client.startSession({ causalConsistency: true });
await orders.insertOne(
  { _id: 42, customer_id: 1, status: 'new', items: [] },
  { session, writeConcern: { w: 'majority' } },
);
const fromSecondary = await orders.findOne(
  { _id: 42 },
  { session, readPreference: ReadPreference.SECONDARY, readConcern: { level: 'majority' } },
);
console.log(fromSecondary);
console.log(session.operationTime);
{ _id: 42, customer_id: 1, status: 'new', items: [] }
new Timestamp({ t: 1791605519, i: 1 })

Чтение ушло на вторичный узел и всё равно вернуло только что записанный заказ. operationTime это и есть то время кластера, которое сессия передаёт дальше.

Важная деталь: документация обещает все гарантии причинной согласованности только вместе с readConcern: 'majority' и writeConcern: 'majority'. С w: 1 и local сессия тоже передаёт время, но после выборов нового первичного запись может откатиться, как мы видели в прошлом уроке, и гарантия ломается.

урок из курса

Урок из курса «MongoDB с нуля». Курс покупается один раз и остаётся навсегда: 500 ₽.

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