В моем последнем посте я описываю различия между записью оплог TokuMX и записью опроса MongoDB. Одна из причин, почему записи так различны, заключается в том, что TokuMX поддерживает транзакции с несколькими выписками и документами . В этой статье я хочу подробнее рассказать о том, почему транзакции с несколькими операторами вызывают изменения в оплоге, и объяснить, как мы изменили репликацию для поддержки произвольно больших транзакций.
Возьмите следующее утверждение MongoDB:
db.foo.insert( [ { _id : 1 , a : 10 }, { _id : 2 , a : 20 } ] );
Внутри сервера MongoDB этот оператор не является транзакционным. Вставка каждого документа является транзакционной, а сама инструкция — нет. Это означает, что допустимым возможным результатом является то, что первый документ вставляется, а второй — нет, или что запрос может получить первый документ, но не второй. С TokuMX, с другой стороны, весь оператор является транзакционным, что означает, что либо весь оператор применяется, либо ничего не применяется. Между ними нет. Поэтому ни один запрос, который должен захватить оба документа, не может забрать первый документ и пропустить второй.
Такое поведение влияет на оплог. С MongoDB, поскольку оператор не является транзакционным, запись двух вставок в оплог по отдельности — это нормально. Если вторичный сервер реплицирует первый документ, но не второй, это все равно согласуется с нормальным поведением MongoDB. Следовательно, формат оплога MongoDB с одной операцией на запись оплога является действительным. С TokuMX мы не можем позволить себе такое поведение. Нам нужно, чтобы содержимое транзакции было захвачено вместе в оплоге, чтобы вторичный сервер точно знал, какие операции должны быть применены в транзакции при воспроизведении событий. Если транзакция атомарна на первичном, она должна быть атомарна и на вторичном. Поэтому мы пришли к выводу, что для правильной поддержки многодокументных транзакций с репликацией, как минимум,нам пришлось расширить и, возможно, изменить формат оплога (хотя, если честно, для этого было много причин, как было указано в моем последнем посте).
Мы видели два варианта сделать это:
- Запишите все операции для транзакции в одной записи оплог.
- Для каждой операции имейте запись оплога, но связывайте их с идентификатором транзакции.
Первый вариант показался более эффективным. Запись одной большой записи в оплог, содержащий все операции, выполняется быстрее, чем запись множества небольших записей, по одной для каждой операции. Таким образом, для «нормальных» транзакций среднего размера решение было несложным. Первый вариант лучше.
То, что сделало решение интересным, разработало способ обработки очень больших транзакций. Хотя это не обязательно рекомендуется, пользователи могут создавать произвольно большие транзакции, содержащие гигабайты данных оплогов. Мы должны быть в состоянии справиться с этим делом правильно. Первый вариант требует ведения списка операций, выполненных в памяти, и последующей записи их в оплог за одну запись до совершения транзакции. При больших транзакциях этот список в памяти может быть произвольно большим, что приводит к нехватке памяти. Итак, без изменений первый вариант непригоден для использования. Поскольку второй вариант не требует списка в памяти, второй вариант можно использовать.
Что сделало второй вариант все еще непривлекательным с поведенческой точки зрения, так это следующее. Если мы начнем записывать данные, которые еще не были зафиксированы в операционном журнале во время выполнения транзакции, то вторичные серверы, которые следят за операционным журналом, не смогут выйти за пределы незафиксированных данных. Итак, предположим, у нас есть набор реплик, в котором все вторичные серверы обновлены. Если у нас есть большая транзакция, которая занимает один час, и мы регистрируем операцию в начале транзакции, тогда вторичные серверы остановятся на этот час, ожидая завершения транзакции. Не записывая в журнал операций до тех пор, пока мы не будем готовы к фиксации, все транзакции, которые завершаются, пока большая транзакция является действующей, могут все еще реплицироваться.
В результате мы действительно хотели найти способ заставить вариант 1 работать. Именно так мы и создали коллекцию oplog.refs, о которой я рассказывал в своем последнем посте. Когда операции транзакции начинают занимать слишком много места (по умолчанию 1 МБ), мы начинаем записывать операции в коллекцию oplog.refs в пакетном режиме. Если транзакция прерывается, то вся эта письменная информация также прерывается и исчезает. Если транзакция должна быть зафиксирована, мы записываем запись в коллекцию oplog.rs, в которой хранится ссылка на коллекцию oplog.refs. Вторичные агенты, которые следуют за оплогом, знают, что, если запись оплога имеет такую ссылку, соответствующие записи в oplog.refs (а их может быть много) также должны быть скопированы и воспроизведены.
Это уменьшает нагрузку на память при ведении списка в памяти. Мы просто периодически сбрасываем список на диск через коллекцию. Теперь для крупных транзакций вторичные серверы будут остановлены в момент совершения транзакции, а не в момент ее начала. Это значительно лучше. (Напомним, что для TokuMX крупные транзакции записи могут все еще занимать нетривиальное количество времени для фиксации. Поэтому будьте осторожны. Иногда они необходимы, но старайтесь не использовать очень большие транзакции в наборе реплик.)
Вот почему в оплоге TokuMX «обычные» транзакции имеют массив «ops», который содержит операции вместо отдельных «op». Вот почему большие транзакции не имеют поля «ops», а вместо этого имеют поле «ref», которое является OID. Вот почему существует коллекция oplog.refs.
В заключение, я надеюсь, что некоторые из этих изменений в оплоге, которые мы сделали, имеют смысл В следующих постах я объясню причины других изменений.