Pull to refresh

Всё, что вы хотели знать о MongoDB, но боялись спросить

mongodb logo
MongoDB — молодая, но довольно перспективная NoSQL база данных. На хабре уже было несколько вводных статей, плюс на сайте MongoDB есть неплохой интерактивный туториал, поэтому я не буду объяснять базовые вещи. Лучше поделюсь накопленным опытом в использовании этой БД. Для начала постараюсь развеять несколько распространённых мифов.


MongoDB нереально быстр! Я провел бенчмарк и сам в этом убедился


Это не совсем так. Никто не может двигаться быстрее скорости света, сохранять данные быстрее винчестера и считать быстрее процессора. Любая база данных рано или поздно упирается во ввод/вывод, и MongoDB не исключение. Почему же MongoDB всё время выигрывает в самопальных бенчмарках?
Причина проста — авторы этих бенчмарков не желают разобраться в том, как же работает MongoDB. Дело в том, что по умолчанию MongoDB работает в режиме «fire-and-forget». Это означает, что, в отличие от большинства БД, выполнение программы не блокируется на время выполнения запроса. Т.е. при вставке документа, мы просто выплёвываем данные в сокет, и сразу же продолжаем работу, вне зависимости от того, успешно ли произошла вставка. Под хорошей нагрузкой, вполне возможна следующая ситуация: вставляем новый документ, после этого пытаемся извлечь его… и ничего не получаем. При этом не происходит никакой ошибки. Более того, даже если после вставки мы смогли успешно извлечь этот документ, нет никакой гарантии что мы сможем повторить это после внезапной перезагрузки сервера. По возможности, MongoDB старается держать все данные в оперативной памяти, и сбрасывает изменённые данные на диск каждые 60 секунд. Т.е. при перезагрузке сервера, мы вполне можем потерять данные за последнюю минуту. К счастью, MongoDB позволяет управлять как синхронизацей с диском, так и блокировкой на время выполнения запроса. Но в бенчмарках этим обычно никто пользуется.

MongoDB ест всю доступную память и не отдаёт её обратно!


Опять-таки не совсем верное утверждение. Поскольку MongoDB использует memory-mapped файлы для работы с диском, операционная система сама решает сколько ему нужно выделить памяти. Действительно, обычно это будет почти вся доступная память, но на самом деле большая её часть используется под кэш. Естественно, при необходимости, эта память будет освобождена. И всё же нужно понимать, что MongoDB, как и любая другая БД, любит работать на отдельном сервере (а лучше хотя бы двух, с учётом репликации). Если вы запускаете его на сервере, где одновременно работает MySQL, Apache, nginx, куча демонов и fastcgi процессов — вполне вероятно, что рано или поздно проснётся oom-killer, который наверняка первым делом решит убить именно процесс mongod. Если вы всё-таки решили запускать MongoDB на одном сервере — убедитесь что ему хватит памяти хотя бы под working set и индексы. Иначе вас ждут жуткие тормоза.

MongoDB ненадёжен. При нештатной перезагрузке сервера можно потерять все данные!


Это не так. Текущая версия (1.6.x) действительно не поддерживает single server durability. Если с сервером, на котором работает MongoDB, что-то случится — вы гарантированно потеряете всё, что было записано после последнего fsync (но не более того). Но задумайтесь, разве вас спасёт postgres или MySQL/InnoDB, с его надёжностью и транзакциями, когда у вас умирает винчестер? Один сервер, вне зависимости от используемой базы данных, не может гарантировать сохранность ваших данных. Если вам действительно нужна какая-то гарантия надёжности — вы просто делаете репликацию. И MongoDB тут не исключение. Конечно, single server durability — это приятный бонус, который, пусть и не гарантирует надёжность, но уж точно не повредит (а в случае отключения электричества во всём датацентре, хотя бы сократит даунтайм, т.к. не нужно проверять целостность базы данных). Поэтому, начиная со следующей версии (1.8.х, релизными являются чётные версии), в MongoDB будет поддержка single server durability, путём ведения лога транзакций (запускайте mongod с параметром --journal). В настоящий момент уже доступен release candidate, поэтому споры о надёжности MongoDB пора прекращать.

В MongoDB есть MapReduce. Это же супер быстрый фреймворк от Гугла!


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

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

Индексы


Да, в MongoDB есть индексы! Как ни странно, многие их не используют, и совершенно напрасно. Индексы в MongoDB практически полностью аналогичны стандартным индексам из распространённых РСУБД — это самые обычные B-Tree. Это означает, что имея составной индекс по полям (A, B, C) — его можно использовать для выборки по (A), (A, B) или (A, B, C). Для выборки по (A, C) он может быть использован только частично (по полю A), а для выборки по (B, C) он вообще бесполезен. Также учитывайте, что MongoDB всегда использует максимум один индекс на запрос, поэтому имея отдельные индексы по полям A и B, в выборке по (A, B) может быть использован только A или B, но никак не оба сразу.

fsync/getLastError


Как я уже упоминал, по умолчанию MongoDB синхронизирует память с диском раз в минуту. Конечно, частоту синхронизации можно настроить в mongodb.conf, но гораздо благоразумнее делать её после каждого запроса, который изменяет важные данные. Для этого просто выполните
db.runCommand({fsync:1}) в коллекции admin. Эта команда работает синхронно, т.е. программа будет заблокирована до тех пор, пока запрос не выполнится. Таким образом вы можете гарантировать, что ваши изменения сразу сохранятся на диске, а не только в памяти.
Если же вы хотите сделать синхронной любую команду, но не хотите при этом нагружать диск fsync'ами — вам поможет getlasterror. Вызов db.runCommand({getlasterror:1}) после любой записи данных, блокирует выполнение до тех пор, пока ваш запрос не отработает до конца.
Помимо того, что getlasterror позволяет сделать любую команду синхронной, он также может принимать несколько параметров. Вызов db.runCommand({getlasterror:1, fsync:true}) завершится только после синхронизации с диском. А команда db.runCommand({getlasterror:1, w:3, wtimeout:3000}) завершится только после того, как данные запишутся минимум на 3 сервера, либо выдаст ошибку через 3 секунды, если за это время данные не успели записаться (подробнее о том, что такое W, можно почитать по ссылке в конце статьи).
Обычно вам не придётся вызывать getlasterror вручную — большинство клиентов позволяют управлять синхронностью операций записи, путём передачи дополнительных параметров. Например такой код на питоне db.test.insert({"key":"value"}, safe=True, w=2) будет выполнен синхронно и завершится только после записи минимум на 2 сервера (safe=True вызывает getlasterror, а все параметры после safe будут переданы getlasterror).

Не совсем обычные числа


Поскольку MongoDB использует мозилловский движок SpiderMonkey, вполне логично ожидать от него (движка) поведения, аналогичного Firefox. Например массивы гарантированно будут иметь метод forEach. Но с числами ситуация немного отличается. В зависимости от используемого клиента, и от разрядности операционной системы, числа могут записываться либо как обычный number, либо как специальный объект NumberLong. Обычно это не имеет значения — запрос db.test.find({"value1":42,"value2":42}) отработает правильно, вне зависимости от того, в каком виде хранятся value1 и value2. Но если вы извлекаете эти значения, допустим, в функции map (например вы хотите обработать все объекты, у которых value1 и value2 равны) — помните о том, что число может оказаться не примитивным типом number, а объектом NumberLong. Т.е. вполне возможен следующий результат:
this.value1 == 42 // true
this.value2 == 42 // true
this.value1 == this.value2 // false

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

MapReduce и красивый код


Наверняка читая документацию по MapReduce, в используемом вами клиенте, вы видели такие примеры кода:
# Ruby
@@reduce_count = "function(key, values) { " +
"var sum = 0; " +
"values.forEach(function(f) { " +
" sum += f.count; " +
"}); " +
"return {count: sum};" +
"};"
# Python
map = Code("function () {"
            "  this.tags.forEach(function(z) {"
            "    emit(z, 1);"
            "  });"
            "}")


Хотя такой код более-менее понятен, и вполне допустим для небольших примеров, он обладает кучей очевидных недостатков:
  1. Его неудобно писать — все эти кавычки, конкатенации и ручные отступы очень раздражают
  2. Его неудобно читать — этот код не будет подсвечиваться вашим любимым редактором, да и изобилие кавычек не помогает читаемости
  3. Его сложно отлаживать — вы не можете просто взять и запустить этот код в консоли mongo
  4. Держать джаваскриптовый код внутри исходников какого-нибудь питона или джавы — не очень-то красиво

Очевидно, что джаваскриптовый код должен лежать внутри .js файлов. Поэтому я рекомендую выносить весь js-код в какой-нибудь отдельный файл. Для запуска этого кода в консоли mongo — выполняем команду load(path_to_file). Для использования его в своих приложениях — считываем весь файл и делаем eval его содержимому. Проблемы решены.

Server-side Javascript


Читать файл и выполнять eval каждый раз может показаться избыточным. В таком случае можно хранить код на сервере — для этого предназначена специальная коллекция system.js. Использовать её можно следующим образом:
db.system.js.save({_id: "sqr", value: function(num){return num * num}})

Теперь у вас есть глобальная функция sqr, которая хранится на сервере и возвращает квадрат числа. Точно также вы можете сохранить свои map, reduce, и функцию которая вызывает db.mycollection.mapReduce. И тогда запуск MapReduce в вашем коде будет выглядеть приблизительно так:
db.eval("runMyMapReduce()")

А в случае использования питона и PyMongo, можно сделать еще красивее:
db.system_js.runMyMapReduce()

system_js в PyMongo — вспомогательный класс, позволяющий прозрачно использовать серверный джаваскрипт. Посмотрите на следующий код:
>>> db.system_js.getTomorrow = "function(date){date.setDate(date.getDate() + 1); return date}"
>>> datetime.datetime.now()
datetime.datetime(2011312204243692182)
>>> db.system_js.getTomorrow(datetime.datetime.now())
datetime.datetime(2011313194252732000)

Выглядит так, словно мы вызываем обычную питоновскую функцию, которая получает datetime в качестве аргумента, и возвращает datetime в качестве результата. Вся прелесть заключается в том, что код этой функции написан на js, а не на питоне, и работает он с джаваскриптовым Date.

Полезные ссылки

  1. Критика MongoDB от разработчика CouchDB (в комментариях ответы разработчиков MongoDB)
  2. Почему (пока) нет single server durability
  3. Очень хороший FAQ по MongoDB
  4. Кратко о CAP, Eventual Consistency, BASE vs ACID, NRW
  5. Автоматизация хранения кода на сервере
Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.