mongodb

Совсем недавно вышла новая версия популярной NoSQL базы данных MongoDb. Что же нового принесла нам эта версия, случился ли прорыв и как новая версия сможет повлиять на текущую разработку в проектах, где используют NoSQL подход. Обо всех изменениях читайте под катом.

Сделана ставка на WiredTiger.

WiredTiger движок появился в версии 3.0 и сразу взорвал сообщество пользователей Mongo. Наконец-то была реализована блокировка на уровне документа. Для приложений с Heavy Wright подходом это оказалось существенным плюсом. Однако разработчики посоветовали явно указывать эту опцию при запуске mongo таким образом

mongod --storageEngine wiredTiger

А движком по умолчанию оставался старый добрый MMAP. Теперь же с выходом новой версии WiredTiger является движком по умолчанию. Для тех же, кто хочет вернуться к старому доброму mmap нужно будет указать это явно

mongod --storageEngine mmapv1

Либо для любителей конфига эта настройка будет выглядеть так

storage:
 engine: mmapv1

Улучшение отказоустойчивости.

Другим существенным изменением стало улучшение механизма отказоустойчивости. За что мне нравится монго, так это за ее безболезненную настройку шардирования и репликации из коробки. Если ваш сервер перестает справляться с нагрузкой, то в течение получаса можно развернуть несколько штук дополнительных шардов и распределить нагрузку по всем. Это просто здорово!

В новой версии реплики будут по умолчанию использовать protocolVersion : 1. Появилась также новая настройка

electionTimeoutMillis – это время в миллисекундах, в течение которого происходит выбор нового мастер узла. По-умолчанию, это значение равно 10000 (т.е. 10 секунд).

Новая версия убирает ограничение на использование 3х инстансов mongod для config серверов. Config сервера – это инстансы mongod, которые хранят в себе информацию по шардам.
В более ранних версиях количество инстансов в реплике ограничивалось двенадцатью штуками. Недавно это число расширили до 50.

Теперь config сервера по-умолчанию объединяются в реплику таким образом

mongod --configsvr --replSet configReplSet --port <port> --dbpath <path>

Для файла конфигурации это будет выглядеть следующим образом

sharding:
  clusterRole: configsvr
replication:
  replSetName: configReplSet
net:
  port: &lt;port&gt;
storage:
  dbpath: &lt;path&gt;

После этого нужно будет соединиться с одним из config сервером и выполнить

rs.initiate( {
 _id: "configReplSet",
 configsvr: true,
 members: [
 { _id: 0, host: "<host1>:<port1>" },
 { _id: 1, host: "<host2>:<port2>" },
 { _id: 2, host: "<host3>:<port3>" }
 ]
 } )

После этого запускаем mongos и указываем нашу реплику, состояющую из нескольких серверов

mongos --configdb configReplSet/<cfgsvr1:port1>,<cfgsvr2:port2>,<cfgsvr3:port3>

Ну а дальше все как обычно. Добавляем шарды в наш кластер, включаем шардирование и выбираем коллекцию для шардирования.

Нововведением также стала настройка readConcern.

Но для ее работы необходима поддержка драйверами версии 3.2. Что это за настройка? Многие уже наверняка знают про writeConcern, которая гарантирует, что данные будут записаны на n узлов, т.е. фактически эта настройка позволяет управлять консистентностью наших данных. ReadConcern же как видно из названия служит для того, чтобы гарантированно получить самые актуальные данные на момент чтения.
Есть два режима:
local – режим по умолчанию. Мы получаем самые актуальные данные, но при этом нет гарантии, что эти данные были записаны на большинство узлов.
majority – в этом режиме запрос гарантированно вернет данные, которые были записаны на большинство узлов.
ReadConcern доступна для следующих операций:

find
aggregate
distinct
count
parallelCollectionScan
geoNear
geoSearch

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

Частичные индексы.

Теперь можно создавать индексы только по тем документам, которые удовлетворяют определенному условию. Такие индексы естественно требуют меньше ресурсов (места), но если ваш запрос не удовлетворяет условию выборки, использованной в индексе, то ожидаемо получите sequential scan всей коллекции. Создать такой частичный индекс можно следующей командой:

db.restaurants.createIndex(
 { cuisine: 1, name: 1 },
 { partialFilterExpression: { rating: { $gt: 5 } } }
)

Такой индекс проиндексирует только те документы, у которых рейтинг выше 5.
PartialFilterExpression может использовать следующие операторы:

$eq - равенство
$exists: true - существование
$gt, $gte, $lt, $lte - >, >=, <, <=
$type - тип
$and - условие "и"

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

db.restaurants.createIndex(
 { cuisine: 1, name: 1 },
 { partialFilterExpression: { rating: { $gt: 5 } } }
 )

то следующий запрос будет использовать частичный индекс:

db.restaurants.find( { cuisine: "Italian", rating: { $gte: 8 } } )

т.к. тут выражение

$gte: 8

входит в множество, определенное в выражении partialFilterExpression.
А вот такие запросы

db.restaurants.find( { cuisine: "Italian", rating: { $lt: 8 } } )
db.restaurants.find( { cuisine: "Italian" } )

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

Сравнение со sparse индексом.

Вообще говоря частичные индексы являются надмножеством sparse индексов, поэтому предпочтительнее использовать именно partial.

Sparse индексы определяют какие документы нужно индексировать основываясь только на существовании конкретного поля в документе. Ту же самую логику можно выразить через частичные индексы таким образом

db.contacts.createIndex(
 { name: 1 },
 { partialFilterExpression: { name: { $exists: true } } }
 )

Таким образом частичные индексы поддерживают те же запросы, что и sparse индексы. Однако с помощью частичных индексов можно указывать выражения для фильтрации по полям, которые не определяются с помощью обычного индексируемого ключа. Например, следующая операция создает частичный индекс по полю name и выражение фильтрации по полю email:

db.contacts.createIndex(
 { name: 1 },
 { partialFilterExpression: { email: { $exists: true } } }
 )

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

Например, такой запрос вполне подойдет:

db.contacts.find( { name: "xyz", email: { $regex: /.org$/ } } )

А вот этот индекс использовать уже не будет:

db.contacts.find( { name: "xyz", email: { $exists: false } }

Надеюсь, общая идея использования частичных индексов понятна.

Валидация документов.

Теперь появилась возможность валидировать документы при вставке и обновлении. Правила определяются для каждой коллекции. Вот что доступно для нас:

  • validator – это та опция, которая определяет правила валидации
  • validationLevel – уровень, на сколько строго mongo будет следовать правилам валидации
  • validationAction – определяет действие, которое должна предпринять mongo, если правило будет нарушено.

Рассмотрим на примере, как всем эти пользоваться.

У нас есть коллекция контакты (contacts), мы для нее определяем правила валидации. Для нас важно следующее:

  • поле phone является строкой
  • поле email соответствует регулярному выражению
  • поле status принимает два значения – Unknown или Incomplete
db.createCollection( "contacts", {
  validator: {
  $or: [
    {phone: {$type: "string"}},
    {email: {$regex: /@mongodb\.com$/} },
    {status: {$in: ["Unknown", "Incomplete"] }}
  ]}
} )

Теперь когда у нас есть валидатор, то такая операция не пройдет

db.contacts.insert( { name: "xyz", status: "A" } )

Этот код вернет ошибку в WriteResult:

WriteResult({
  "nInserted" : 0,
  "writeError" : {
    "code" : 121,
    "errmsg" : "Document failed validation"
  }
})

Добавление валидации в существующую коллекцию.

Для того чтобы добавить правила валидации в текущую коллекцию используйте команду collMod с опцией validator:

db.runCommand( {
 collMod: "contacts",
 validator: { $or: [ { phone: { $exists: true } }, { email: { $exists: true } } ] }
 } )

Также можно просмотреть текущие правила валидации для коллекции при помощи команды db.getCollectionInfos( ) :

db.getCollectionInfos( { name: "contacts" } )

Метод вернет следующий массив:

[
  {
    "name" : "contacts",
    "options" : {
      "validator" : {
        "$or" : [
          {
            "phone" : {
              "$exists" : true
            }
          },
          {
            "email" : {
              "$exists" : true
            }
          }
        ]
      }
    }
  }
]

Текущие ограничения.

Нельзя указывать валидаторы для коллекций в базах admin, local, config.

Обход валидации.

При помощи опции bypassDocumentValidation можно обходить валидацию в следующих командах:

MongoDB позволяет определить как строго будут восприниматься операции изменения при нарушении правил валидации. За это отвечает параметр validationLevel:

  • off – отключает валидацию для вставок и обновлений
  • strict – уровень по умолчанию. Валидация включена для вставок и обновлений
  • moderate – правила валидации не применяются при обновлении текущих невалидных документов. При изменении валидных документов или добавлении новых правила работать будут

Также MongoDB определяет какие действия предпринять в случае нарушения правил валидации. За это отвечает параметр validationAction:

  • error – в случае нарушения правил операция записи завершится с ошибкой
  • warn – в случае нарушения правил операция записи пройдет, но будет выдано предупреждение

В новой версии также добавлены улучшения для механизма Aggregation Framework.

Появились новые этапы при агрегации:

ЭтапОписаниеСинтакс
$sampleПроизвольно выбирает N документов{ $sample: { size: <positive integer> } }
$indexStatsСтатистика использования индеса{ $indexStats: { } }
$lookupВыполняет левое внешнее объединение с другой коллекцией
{
   $lookup:
     {
       from: <collection to join>,
       localField: <fieldA>,
       foreignField: <fieldB>,
       as: <output array field>
     }
}

Да, теперь в mongo появился join! База двигается по пути реляционных баз данных. Тут, наверно, будет уместно вспомнить перевод термина NoSQL как Not Yet SQL.

Наверно, оператор $lookup все же оправдан в текущих реалиях, так как данные реляционны в большинстве случаев.

В mongo есть ограничение на 16mb для одного документа, поэтому слишком много туда не получится вместить, и приходится сохранять некие идентификаторы в коллекциях, чтобы потом вытащить по ним полноценные документы из других коллекций. Теперь же можно воспользоваться оператором $lookup, который все это сделает за нас.

Появились также новые вычисляемые значения для этапа $group.

ЗначениеОписаниеСинтаксис
$stdDevSampВычисляет среднее отклонение части выборки{ $stdDevSamp: <array> }
$stdDevPopВычисляет среднее отклонение всей выборки{ $stdDevPop: <array> }

Появились новые арифметические операторы:

ОператорОписаниеСинтаксис
$sqrtВычисляет квадратный корень{ $sqrt: <number> }
$absВычисляет значение по модулю{ $abs: <number> }
$logВычисляет логарифм числа{ $log: [ <number>, <base> ] }
$log10Вычисляет логарифм по основанию 10{ $log10: <number> }
$lnВычисляет натуральный логарифм{ $ln: <number> }
$powВозводит число в степень{ $pow: [ <number>, <exponent> ] }
$expВозводит число Эйлера в степень{ exp: <number> }
$truncОбрезает число до целого{ $trunc: <number> }
$ceilОкругляет до ближайшего целого в большую сторону{ $ceil: <number> }
$floorОкругляет до ближайшего целого в меньшую сторону{ floor: <number> }

Появились новые операторы по работе с массивами:

ОператорОписаниеСинтаксис
$sliceВозвращает срез массива

{ $slice: [ <array>, <n> ] }

or

{ $slice: [ <array>, <position>, <n> ] }

$arrayElemAtВозвращает элемент по указанному индексу{ $arrayElemAt: [ <array>, <idx> ] }
$concatArraysОбъединяет массивы
{
  $concatArrays: [ <array1>, <array2>, ... ]
}
$isArrayОпределяет является ли операнд массивом{ $isArray: [ <expression> ] }
$filterВыбирает элементы массивы по фильтру
{
  $filter:
    {
      input: <array>,
      as: <string>,
      cond: <expression>
    }
}

Теперь такие операторы как:

также доступны для этапа проекции $project.

Оптимизация.

Большая работа была проделана по оптимизации различных выборок. Например, если условие $match содержит ключ шардирования, то выборка будет производиться только на определенном шарде. Если операции агрегации не требуется выполнения на первичном узле кластера, она будет выполнена на других узлах и результат будет объединен в дальнейшем. Это позволяет разгрузить первичный узел. Например, операции $out и $lookup требуют выполнения на первичном узле кластера.

Утилиты mongodump, mongorestore теперь поддерживают streaming. Можно, например, сделать дамп базы, тут же ее сжать и отправить по сети на удаленный сервер.

Движок с поддержкой шифрования.

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

Сначала создается системный ключ, он хранится отдельно от данных. Потом создаются ключи для баз данных, которыми шифруются сами данные. Потом эти ключи шифруются системным ключом.

Текстовый поиск.

В версии 3.2 появилась 3-я версия текстовых индексов.

Оператор $text теперь поддерживает:

  • чувствительность к регистру при помощи новой опции $caseSensitive
  • диакритический поиск – это поиск слов с похожими буквами, такими как é, ê, и e. Используется новая опция $diacriticSensitive

В Enterprise версии также добавились некоторые языки. Например, китайский и арабский.

Из других улучшений.

Добавились операторы

Версия 3.2 теперь использует движок JavaScript SpiderMonkey. Для обеспечения консистентности с операциями CRUD в драйверах были добавлены следующие операции:

Новое APIОписание
db.collection.bulkWrite()

Эквивалент инициализатору операций Bulk(), использующий методы для добавления операций и выполняющий операции посредством Bulk.execute().

db.collection.deleteMany()Эквивалент db.collection.remove().
db.collection.deleteOne()

Эквивалент db.collection.remove() с опцией justOne установленной в true; т.е.

db.collection.remove( <query>, true ) или db.collection.remove( <query>, { justOne:true } ).

db.collection.findOneAndDelete()Эквивалент db.collection.findAndModify() с опцией remove в true.
db.collection.findOneAndReplace()Эквивалент db.collection.findAndModify() с опцией update указывающей на заменяемый документ.
db.collection.findOneAndUpdate()Эквивалент db.collection.findAndModify() с опцией update указывающей на документ, который определяет операции изменения посредством операторов обновления.
db.collection.insertMany()Эквивалент db.collection.insert() с массивом документов в качестве параметра
db.collection.insertOne()Эквивалент db.collection.insert() с единственным документом в качестве параметра
db.collection.replaceOne()Эквивалент db.collection.update( <query>,<update> ) указывающий на заменяемый документ посредством параметра <update>
db.collection.updateMany()Эквивалент db.collection.update( <query>,<update>, { multi: true, ... }) то же, что предыдущее, только с опцией multi: true
db.collection.updateOne()Эквивалент db.collection.update( <query>,<update> ) но изменяет только один документ по условию

Появилась новая команда fsyncLock для базы. Она предотвращает изменение данных. Может быть полезно при создании бэкапов. Также появился новый механизм диагностирования данных в коллекции, который записывает статистику сервера в указанные промежутки времени. По умолчанию, этот интервал равен одной секунде. В файловой системе сервер создает новую директорию diagnostic.data.

Ну вот, пожалуй, и все основные изменения в новой версии MongoDB 3.2. В целом релиз считаю очень удачным. После выхода третьей версии mongo радует все больше и больше. Круг задач, которые решает эта база постепенно расширяется и все больше компаний с каждым годом обращают внимание на этот продукт.

Что еще можно почитать:

Document Validation and what dynamic schema means

Leave a Reply

One thought on “MongoDB 3.2”
  1. db.createCollection( “contacts”, {
    validator: { $or:
    [
    { phone: { $type: “string” } },
    { email: { $regex: /@mongodb\.com$/ } },
    { status: { $in: [ “Unknown”, “Incomplete” ] } }
    ]
    }
    } )

    В этом примере, достаточно, чтобы ТОЛЬКО одно из правил (любое!) выполнялось, тогда, запись будет вставлена.
    Если $or заменить на $and – обязательно все поля должны присутствовать.

    Если не все поля обязательны – можно внутри $or комбинировать различные комбинации $and, получается два в степени N комбинаций внутри $or, где N – число необязательных полей.

    Есть ли более простое создание валидатора с любым количеством необязательных полей?

Leave a Reply

Your email address will not be published. Required fields are marked *