Статьи

Почему TokuMX изменил формат оплога MongoDB для операций

За несколько постов, я объяснил  те различия между TokuMX репликацией и репликацией MongoDB, и почему они совершенно несовместимы. В этом (запоздалом) посте я объясню одно последнее отличие: формат оплога для операций. В частности, TokuMX и MongoDB регистрируют обновления и удаляют по-разному.

Предположим, у нас есть коллекция foo со следующим элементом:

rs0:PRIMARY> db.foo.find()
{ "_id" : 0, "a" : 0, "b" : 0 }

Мы выполняем следующее обновление:

rs0:PRIMARY> db.foo.update({_id:0}, {$inc : {a : 1}})

В TokuMX запись операции выглядит следующим образом:

{
    "op" : "ur",
    "ns" : "test.foo",
    "pk" : {
           "" : 0
    },
    "o" : {
           "_id" : 0,
           "a" : 0,
           "b" : 0
    },
    "m" : {
           "$inc" : {
                   "a" : 1
           }
    }
}

В MongoDB операция выглядит следующим образом:

{
       "ts" : Timestamp(1401739915, 1),
       "h" : NumberLong("-8954662209753934860"),
       "v" : 2,
       "op" : "u",
       "ns" : "test.foo",
       "o2" : {
               "_id" : 0
       },
       "o" : {
               "$set" : {
                       "a" : 1
               }
       }
}

Хотя есть несколько отличий, я хочу обратить внимание на поля, которые определяют обновление. В записи оплог TokuMX они обозначены как «o» и «m». «Pk» также используется, но давайте не будем обращать на это внимание, так как это излишне усложняет историю. В MongoDB это «o2» и «o». В обоих случаях первое поле определяет предварительное изображение документа, а второе поле определяет модификацию, которая должна быть сделана. Для обоих полей TokuMX и MongoDB обрабатывают эти поля по-разному. Ниже я объясню почему.

Давайте сначала обратимся к предварительному изображению. MongoDB включает только поле _id в запись оплог. На вторичном сервере для выполнения обновления MongoDB выполняет следующие действия:

  • Используйте поле _id, чтобы выполнить поиск, чтобы найти весь документ перед изображением.
  • Выполните обновление документа на месте, если это возможно, в противном случае, переместив документ в новое место.
  • Обновите вторичные индексы, если необходимо.

Обратите внимание, что для MongoDB получение документа является требованием для обновления документа, поскольку обновление может быть выполнено на месте. Поэтому, если все предварительное изображение было записано в оплог, а не только в поле _id, MongoDB ничего не получит. Это просто свойство их хранилища на основе B-дерева и кучи .

TokuMX, с другой стороны, регистрирует полный предварительный образ документа. Мы делаем это, потому что, если мы знаем полный предварительный образ и необходимые модификации, мы можем выполнить обновление без извлечения документа. Полное предварительное изображение вместе с изменениями, которые необходимо внести, определяют, какие изменения произойдут в первичном ключе (в котором хранится основная копия документа) и во всех связанных вторичных ключах. С помощью этой информации мы можем использовать индексацию Fractal Tree , отправляя сообщения по деревьям, которые будут выполнять обслуживание индекса без предварительного поиска. Таким образом, добавляя полный предварительный образ в оплог, вторичные устройства TokuMX избегают ненужного поиска, что означает избегание ненужного ввода-вывода.

Thanks to this, TokuMX secondaries perform significantly less I/O than primaries. All the hidden I/Os required to lookup documents for updates and deletes are not performed on secondaries. For this reason, we think TokuMX secondaries do a wonderful job at scaling reads across a replica set. To show this, we ran the following experiment. On both MongoDB and TokuMX, we ran sysbench on a replica set such that the primary was utilizing 100% of available I/O. Note that some of the I/O was used for performing queries, which are not replicated. We proceeded to measure the I/O utilization of secondaries as they were replicating off I/O bound primaries. Below is graph showing our results.

You’ll see that the TokuMX secondary is using significantly less I/O to keep up with the primary.

A downside to this current design choice is that large documents induce more network bandwidth over replica sets. But that is because, at the moment, we are encoding the full pre-image of the entire document. What we ought to be doing, and what we will do in the future, is encode just the pre-image of fields affected by the update.

Now let’s address the post-image. You’ll notice that the user issued a $inc update, but MongoDB changed it to a $set within its oplog. The reason (I believe) is that MongoDB needs the oplog to be idempotent, as mentioned here. $set is an idempotent operation, $inc is not. As I’ve mentioned in the past, we don’t want idempotency to be a requirement for oplog entries, because it hinders our ability to innovate. Now, I want to give an example.

Some day, we’d like to change the internal implementation of more updates to not require a read before performing the write. Mark Callaghan describes these writes here. He mentions increment and decrement as examples, but really any type of update can be fast, provided the query uses the primary key and secondary indexes are not modified. In fact, if any user is interested in this feature, drop us a line at support@tokutek.com and tell us about it. However, if the oplog must be idempotent, as MongoDB’s is, I see no way to optimize non-idempotent update operations such as increment and decrement. As far as I can tell, MongoDB’s requirement of changing a $inc operation to a $set in the oplog forces the storage engine to perform a read before applying the modification, in order to find what value we will be setting the field “a” to.

So, when I said forcing TokuMX’s oplog entries to be idempotent would limit our ability to innovate, a prime example is making more updates really fast (in the future).