Привет, Хабр! На связи Сергей Гайдамаков. Продолжаем обсуждать и тестировать набор реплик MongoDB. 

В предыдущей статье мы рассмотрели структуру отдельного узла MongoDB, разобрали свойства параметров writeConcern и readConcern для работы с набором реплик MongoDB. 

В этой статье я покажу результаты тестов при аварийных ситуациях, которые могут происходить в распределенной системе. Сделаем выводы о свойствах набора реплик с точки зрения CAP- и PACELC-теорем для распределенных систем и посмотрим параметры управления CAP-свойствами неоднородных распределенных систем.

Аварии в наборе реплик MongoDB

Набор реплик MongoDB состоит из нескольких узлов, взаимодействующих по сети и хранящих одни и те же данные. В наборе реплик, который доступен на запись, есть один primary-узел, на котором клиент может менять данные, и несколько secondary-узлов, с которых клиент может читать данные. 

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

·   может выйти из строя один, несколько или все secondary-узлы;

·   может выйти из строя primary-узел;

·   может произойти разделение узлов, когда узлы «не видят» друг друга, но клиенты имеют доступ ко всем узлам;

·   может произойти временная недоступность узла.

Набор реплик MongoDB для тестирования
Набор реплик MongoDB для тестирования

Ключевая проблема любой распределенной системы в том, что из-за инертности и задержек в распространении информации она не всегда знает свое состояние в текущий момент. 

В ходе выполнения запроса на изменение данных может выясниться, что один или несколько узлов недоступны, поэтому в момент выполнения запроса нужно принять решение, как действовать. Можно сохранить данные, обеспечив доступность системы, или отказаться сохранять данные, обеспечив согласованность данных на узлах распределенной системы. 

Исходя из такой бинарности принятия решения появилась CAP-теорема: устойчивая к разделению распределенная система может обладать либо свойством доступности, либо свойством согласованности.

? Все тесты ниже выполнялись на наборе реплик MongoDB с тремя узлами, которые работают на портах 29001, 29002 и 29003.

Наблюдение за состоянием распределенной системы

Для наблюдения за состоянием набора реплик MongoDB разработана функция ConsistencyTestObserver(readConcern), которая выводит значение атрибута v в тестовом документе {"n": 1, "v": 1}.  

Документ читается локально с каждого узла (столбцы 29001, 29002, 29003) и из набора реплик при указанном readConcern (столбец replset). Знак минус в таблице означает, что значение на узле не совпадает со значением, выдаваемым набором реплик. Еще в таблицу выводится primary-узел (столбец Primary) и длительность чтения всех данных (столбец duration).

Результат функции ConsistencyTestObserver
Результат функции ConsistencyTestObserver

При увеличении номера версии документа можно наблюдать, как меняются данные на узлах.

Обновление документа
Обновление документа
Результат обновления
Результат обновления

Тест демонстрирует состояния при всех работающих узлах набора реплик. В этом случае нет проблем c обновлением документа для разных параметров writeConcern: {w: 1}, {w: “majority”} и {w: 3}. Функция наблюдения ConsistencyTestObserver может поймать разные состояния узлов, например, когда на всех узлах находится новое значение, при этом набор реплик в целом выдает старое значение. 

При интерпретации результатов следует учитывать, что функция читает данные последовательно для каждого столбца и по мере чтения с узлов данные на них могут обновиться.

Авария и восстановление работы secondary-узла

Посмотрим, как поведет себя набор реплик и пройдут обновления документа с разными writeConcern, если в наборе реплик будет отсутствовать один secondary-узел.

Запустим функцию наблюдения ConsistencyTestObserver, «убьем» процесс MongoDB, который обеспечивает работу secondary-узла 29002, и выполним операции обновления документа.

Обновление документа без secondary-узла
Обновление документа без secondary-узла
Результат обновления без secondary-узла
Результат обновления без secondary-узла

Функция наблюдения показывает, что в первый момент узел 29002 доступен и на нем версия документа с номером 1. После аварии узел 29002 становится недоступным, хотя набор реплик успешно выдает значение 1. 

Обе операции обновления документа увеличивают номер версии. При этом операция обновления с writeConcern: {w: 3, j: true, wtimeout: 3000} через три секунды завершается с ошибкой waiting for replication timeout, так как в наборе реплик работает только два узла из трех.

При отсутствии одного узла операция чтения данных выполняется успешно при всех значениях readConcern.

Результат чтения без secondary-узла
Результат чтения без secondary-узла

Если теперь восстановить работоспособность узла 29002, то функция наблюдения покажет, как сначала на узле 29002 будет старая версия документа с номером 1, а потом реплика 29002 обновится и покажет обновленное значение версии документа, равное 3.

Результат восстановления работы secondary-узла
Результат восстановления работы secondary-узла

Авария и восстановление работы primary-узла

Посмотрим, как поведет себя набор реплик и пройдет обновление документа, если в наборе реплик будет отсутствовать primary-узел.

Запустим функцию наблюдения ConsistencyTestObserver, «убьем» процесс MongoDB, который обеспечивает работу primary-узла 29001, и выполним операции чтения и обновления документа с ожиданием операции в 20 секунд.

Чтение и обновление документа без primary-узла
Чтение и обновление документа без primary-узла

Операция чтения с readPreference: “PrimaryPreferred” выполнится моментально, несмотря на то что primary-узел будет недоступен:  старый primary-узел «убит», а новый еще не будет выбран. Для значения PrimaryPreferred чтение данных при недоступности primary-узла автоматически переключится на один из secondary-узлов.

Функция наблюдения показывает, что сначала узел 29001 доступен и на нем версия документа с номером 1. После аварии узел 29001 становится недоступным, но набор реплик успешно выдает значение 1, хотя primary-узел отсутствует.

Результат обновления без primary-узла
Результат обновления без primary-узла

Выбор нового primary-узла выполняется в течение 10 секунд, и этим узлом становится 29002. Именно в этот момент выполняется обновление документа и версия становится равной 2.

Если теперь восстановить работоспособность узла 29001, то функция наблюдения покажет, как в первый момент на узле 29001 будет старая версия документа, равная 1, а в следующий момент реплика 29001 обновится и покажет обновленное значение версии документа, равное 2.

Результат восстановления primary-узла
Результат восстановления primary-узла

Через несколько секунд primary-узлом станет 29001, так как в конфигурации набора реплик узел 29001 имеет более высокий приоритет, чем узел 29002

Авария и восстановление работы обоих secondary-узлов

Посмотрим, как поведет себя набор реплик и пройдет обновление документа, если в наборе реплик будут отсутствовать оба secondary-узла.

Запустим функцию наблюдения ConsistencyTestObserver, «убьем» процессы MongoDB, которые обеспечивают работу secondary-узлов 29002 и 29003, и выполним операции обновления документа.

Обновление документа без secondary-узлов
Обновление документа без secondary-узлов

Операция обновления с writeConcern: {w: 1, j: true} выполнится моментально, несмотря на то что secondary-узлы уже недоступны: primary-узел поймет это только через 10 секунд. Операция обновления с writeConcern: {w: “majority”, j: true} ожидаемо выполнится с ошибкой waiting for replication timed out.

Функция наблюдения показывает, что узлы 29002 и 29003 доступны и на них версия документа с номером 1. После аварии на узлах 29002 и 29003 набор реплик выдает значение 1 даже после обновлений документа, пока primary-узел 29001 не «осознал» отсутствия secondary-узлов. После исчезновения в наборе реплик primary-узла обновление данных на узле 29001 невозможно.

Результат обновления без secondary-узлов
Результат обновления без secondary-узлов

Если теперь восстановить работоспособность узлов 29002 и 29003, то функция наблюдения покажет, как документ реплицируется на secondary-узлы, выбирается primary-узел 29001 и в наборе реплик версия документа обновляется до значения 3.

Результат восстановления secondary-узлов
Результат восстановления secondary-узлов

Если в момент аварии попытаться прочитать данные при разных readConcern, то получим результаты как на рисунке.

Чтение документа без secondary-узлов
Чтение документа без secondary-узлов

Первый запрос вернет последнюю версию документа, равную 3, так как читаются локальные данные с работающего узла. 

Второй запрос вернет версию документа, которая находится на большинстве узлов, а так как secondary-узлы недоступны и данные на них не реплицируются, на большинстве узлов (включая недоступные) находится версия 1. 

Третий запрос с readConcern: “linearizable” вернет ошибку, так как нет доступа к secondary-узлам, чтобы получить подтверждение репликации текущих изменений на большинство узлов набора реплик.

После восстановления работоспособности secondary-узлов запрос с readConcern: “linearizable” выполнится успешно и вернет новое значение версии документа, равное 3.

Потеря данных в наборе реплик

Проведем тест, в котором создадим условия потери данных в наборе реплик.

Запустим функцию наблюдения ConsistencyTestObserver, как и в предыдущем тесте «убьем» процессы MongoDB, которые обеспечивают работу secondary-узлов 29002 и 29003. Выполним операции обновления документа с увеличением номера версии на 5.

Обновление документа без узлов 29002 и 29003
Обновление документа без узлов 29002 и 29003

Как и в предыдущем тесте, попытка обновить документ с writeConcern: {w: “majority”, j: true} приведет к ошибке: закончится время ожидания репликации данных на secondary-узлах, так как эти узлы недоступны. Именно эта ошибка служит звоночком, что данные в наборе реплик находятся в несогласованном состоянии и система не гарантирует их сохранность.

Функция наблюдения показывает, что номер версии документа на узле 29001 обновился до 11-й версии, но набор реплик выдает значение 1. То есть в этот момент значения на primary-узле и в наборе реплик не согласованы. После этого «убьем» процесс MongoDB для узла 29001 — и набор реплик становится полностью неработоспособным. Теперь восстановим работоспособность узлов 29002 и 29003 и дождемся выбора primary-узла.

Результат обновления без узлов 29002 и 29003
Результат обновления без узлов 29002 и 29003

После выбора primary-узла можно успешно выполнить обновление документа до версии 2 с writeConcern: {w: “majority”, j: true}.

Обновление документа без узла 29001
Обновление документа без узла 29001

Наблюдение покажет, что набор реплик будет возвращать версию 2.

Результат обновления без узла 29001
Результат обновления без узла 29001

Теперь если запустить узел 29001, то данные, которые были сохранены на узле ранее, будут отменены и обновлены версией документа с номером 2, которая присутствует в наборе реплик. Сначала на узле 29001 будут старые данные, потом на узел произойдет откат транзакций до точки синхронизации и применятся обновления, которые были сделаны в наборе реплик.

Результат восстановления узла 29001
Результат восстановления узла 29001

Потеря данных в наборе реплик никак не связана со свойствами ACID транзакции, она связана с алгоритмами репликации данных по узлам набора реплик. 

Признаком того, что MongoDB не гарантирует сохранность данных и они могут быть утеряны в случае сбоя, служит ошибка waiting for replication timed out при обновлении документа с writeConcern: {w: “majority”, j: true}.

Наш тест в том числе эмулирует ситуацию разделения распределенной системы на две несвязанные подсистемы: в одной подсистеме primary-узел, а в другой — secondary-узлы. Primary-узел после разделения еще 10 секунд может принимать запросы на запись, пока не поймет, что secondary-узлы недоступны. Secondary-узлы, наоборот, оказавшись в большинстве (majority), примерно через 12 секунд переизберут новый primary-узел и начнут принимать запросы на запись. После восстановления связи все данные, которые были изменены на старом primary-узле, будут заменены другими данными, если они менялись на новом primary-узле.

Разделенный набор реплик MongoDB
Разделенный набор реплик MongoDB

Защита от потери данных в наборе реплик

Идеальной защиты от потери данных в распределенной системе не существует, но клиент, который изменяет данные или читает их, может узнать, что есть проблема, и предпринять компенсирующие действия.

Если в момент аварии в наборе реплик работает число узлов меньше majority и еще доступен на запись primary-узел, то при обновлении с writeConcern: {w: “majority”, j: true} будет ошибка. Это сигнал, что распределенная система перешла в неопределенное состояние и данные могут быть утеряны, как в последнем тесте.

Мы сталкиваемся с проблемой неопределенного состояния базы данных даже для одного узла, например, в PostreSQL, когда при выполнении запроса на изменение данных клиент получает ошибку timeout. Такая ошибка может произойти в разных ситуациях: 

  • сервер был сразу недоступен;

  • сервер базы данных получил запрос, но не успел вернуть ответ, так как оборвалось соединение;

  • запрос слишком долго выполнялся на сервере. 

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

Аналогично при взаимодействии с распределенными системами: получение ошибки timeout в репликации данных между узлами требует выполнения дополнительных действий. Например, повторить запрос позднее, чтобы привести распределенную систему в предсказуемое состояние и гарантировать сохранность данных при сетевых сбоях.

Достаточно использовать параметр readConcern: “majority”, чтобы при чтении получать данные, которые не исчезнут из набора реплик. Этот параметр возвращает данные, находящиеся на большинстве узлов. Но такой режим чтения не гарантирует, что будут выданы самые последние данные, которые внесены в систему в момент запроса, при этом еще не успели реплицироваться на secondary-узлы. 

Если в момент запроса использовать readConcern: “linearizable”, то можно гарантировать, что мы получим данные, которые последними были внесены в систему.

Но тогда можно получить ошибку, если в наборе реплик недоступно majority число узлов.

CAP-треугольник MongoDB

Обобщим результаты тестов и сделаем выводы относительно места MongoDB в CAP-теореме, то есть определим, каким свойствам соответствует MongoDB.

В соответствии со свойствами CA неустойчивая к разделению распределенная система должна обеспечивать сильную согласованность данных и доступность.

Для получения свойств CA в наборе реплик MongoDB из трех узлов требуется указывать значения параметров:

  • readPreference: “primary”

  • readConcern: “linearizable”

  • writeConcern: {w: 3, j: true}

Перечисленные параметры обеспечивают запись и чтение только на primary-узле с ожиданием обновления данных на всех узлах. Такой режим работы избыточен по надежности и очень медленный для обновления данных, поэтому не интересен в использовании.

Более оптимальный режим работы набора реплик получается при обеспечении свойств CP. В соответствии с этими свойствами распределенная система устойчива к разделению и обеспечивает согласованность. При этом возможны следующие варианты достижения согласованности: strong consistency и eventually consistency.

Strong consistency достигается параметрами:

  • readPreference: “primary”

  • readConcern: “linearizable”

  • writeConcern: {w: “majority”, j: true}

При указанных параметрах в случае разделения системы majority часть узлов будет доступна для чтения и записи с сохранением согласованности, то есть будет обладать всеми тремя свойствами СAP-теоремы! Но меньшая часть узлов разделенной системы будет недоступна не только для записи, но и для чтения, так как невозможно обеспечить линеаризуемость получаемых данных.

Для получения eventually consistency достаточно параметров:

  • readPreference: “primaryPreferred”

  • readConcern: “majority”

  • writeConcern: {w: “majority”, j: true}

В таком случае при разделении системы majority часть узлов также будет доступна для чтения и записи с сохранением согласованности, то есть будет обладать всеми тремя свойствами СAP-теоремы. А меньшая часть узлов разделенной системы будет позволять только читать данные, которые были доступны в наборе реплик в момент разделения системы, то есть обеспечивать свойства CP.

Нужно учитывать, что достижение строгой согласованности по сравнению с согласованностью в конечном счете потребует дополнительных накладных расходов, что снижает скорость выполнения запросов.

Полностью достигнуть свойств AP (availability + partition tolerance) в том виде, который обеспечивает, например, Cassandra, набор реплик MongoDB не может. Запись изменений в базу данных всегда идет только в primary-узел, чтобы обеспечивать согласованность данных. А если он недоступен, изменения невозможны, то есть не достигается свойство availability.

Вывод: набор реплик MongoDB с небольшими оговорками можно разместить на всех сторонах CAP-треугольника.

CAP-треугольник MongoDB
CAP-треугольник MongoDB

Более полно параметры работы с набором реплик MongoDB раскрываются свойствами PACELC, которые расширяют CAP-концепцию. 

PACELC — утверждение относительно свойств, которым удовлетворяет распределенная система, когда ее узлы разделены или не разделены на группы. 

PACELC = Partitioning, Availability, Consistency, else, Latency и Consistency

Если Partitioning, то Availability или Consistency, else — Latency или Consistency. Иными словами, если есть разделение системы, то система может обладать либо свойством доступности, либо согласованности. Если разделения системы нет, то система обладает либо низкой задержкой выполнения запросов, либо свойством согласованности. 

Подробнее о PACELC можно почитать в отдельных статьях, например в статье «Нам мало CAP. Да здравствует PACELC».

Нам мало CAP. Да здравствует PACELC
Если вы когда-нибудь сталкивались с распределёнными СУБД или системами обработки данных, то слышали ...
habr.com

Распределенная система со свойствами PC + EC фокусируется на согласованности данных, что в MongoDB обеспечивается параметрами:

  • readPreference: “primary”

  • readConcern: “linearizable” / “majority”

  • writeConcern: {w: “majority”, j: true}

Распределенная система со свойствами PA + EL предполагает максимальную производительность, допуская возможную потерю данных при сбоях, что в MongoDB обеспечивается параметрами:

  • readPreference: “secondaryPreferred”

  • readConcern: “local”

  • writeConcern: {w: 1, j: false}

Распределенная система со свойствами PA + EC или PC + EL в случае набора реплик MongoDB предполагает согласованность данных с распределением нагрузки чтения по всем узлам, что обеспечивается параметрами:

  • readPreference: “secondaryPreferred”

  • readConcern: “majority”

  • writeConcern: {w: “majority”, j: true}

Согласованность в неоднородной распределенной системе

Для управления CAP-свойствами неоднородной распределенной системы можно использовать параметры, аналогичные MongoDB. Например, для распределенной системы, в которой разные узлы хранят зависимые данные, реплицируемые от узла PostrgeSQL в узел Elastic Search, потребуется добавить в интерфейсы CRUD и Search параметры управления CAP-свойствами.

Неоднородная распределенная система
Неоднородная распределенная система

В CRUD-интерфейсе для методов создания, изменения и удаления данных можно добавить параметр writeConcern с вариантами значений:

  • noWait — сразу возвращать результат выполнения метода без ожидания репликации изменений на ElasticSearch (аналогично writeConcern: {w: 1, j: true} в MongoDB);

  • searchReady — возвращать результат выполнения метода после репликации изменений на ElasticSearch (аналогично writeConcern: {w: “majority”, j: true} в MongoDB).

А в Search-интерфейс для метода поиска можно добавить параметр readConcern с вариантами значений:

  • noWait — выполнять поиск сразу без гарантии согласованности данных (аналогично readConcern: “local” в MongoDB);

  • consistencyGuarantee — выполнять поиск после репликации данных из PostgreSQL, измененных к текущему моменту, чтобы гарантировать согласованность выдаваемых данных, которые были сохранены в PostgreSQL к началу вызова метода (аналогично readConcern: “linearizable” в MongoDB).

Для обеспечения в распределенной системе свойств AP (availability + partition tolerance) потребуются параметры:

  • writeConcern: “noWait”

  • readConcern: “noWait”

Для обеспечения свойств CP (consistency + partition tolerance) потребуются параметры:

  • writeConcern: “searchReady”

  • readConcern: “consistencyGuarantee”

Указанные параметры можно расширить временем ожидания выполнения метода, по истечении которого возвращать ошибку (timeout). Дополнительно в readConcern для метода поиска можно передавать timestamp изменения данных в CRUD-интерфейсе, которого требуется дождаться в репликации на Elastic Search, чтобы позволить клиенту минимизировать время ожидания ранее выполненной CRUD-операции.

Сами PostgreSQL и Elastic Search могут быть развернуты на нескольких узлах, в которых нужно обеспечивать согласованность данных. В этом случае картинка всей системы и параметры управления CAP-свойствами будут намного сложнее.

Выводы

Подводя итог, можно сделать выводы:

  • Для набора реплик MongoDB клиент сам выбирает свойства CAP/PACELC, которые хочет получить, для каждого запроса отдельно.

  • Свойства Availability и Consistency в MongoDB регулируются настройками writeConcern, readConcern и readPreference.

  • Свойство AP (availability + partition tolerance) достижимо в MongoDB частично, только для majority части узлов.

  • Из-за инертности распределенных систем основные проблемы возникают в момент изменения состояния (по умолчанию о разделении узлов в наборе реплик MongoDB primary-узел узнает через 10 секунд).

  • При выполнении запросов к набору реплик MongoDB требуется обрабатывать ошибки изменения данных, сигнализирующие о неизвестном или несогласованном состоянии системы, и выполнять компенсирующие действия для достижения согласованности (то есть сохранности данных на длительном промежутке).

  • MongoDB полностью удовлетворяет свойствам ACID транзакций, возможная потеря данных в наборе реплик связана с некорректным выбором параметров запросов и отсутствием обработки ошибок.

MongoDB — производительная и надежная база данных с распределенным хранением информации, которая настраивается под разные задачи и разный объем обрабатываемых данных.

Статья написана по результатам доклада в сообществе «Книжный клуб.rar». Видеозапись доклада можно найти на Ютубе. 

Скрипты MongoDB для самостоятельного тестирования находятся в Github.  

Комментарии (1)


  1. Samush
    20.08.2025 19:05

    Привет, Хабр! На связи главный адвокат MongoDB Всея Руси :D

    А если серьезно, спасибо за проделанную работу и статью.

    Не понял насчет writeMajority и компенсирующих действий - ретрай изначального запроса тут подходит ? Или если запрос был +100 папугаев на счет господина А, а потом мажорити не набралось и клиент (как-то) понимает что нужны эти компенсирующие действия - в таком случае повторный запрос даст +200 в итоге или нет ?