5 ноября 2009 в 15:16

MongoDB — варим хороший кофе

MongoDB
Введение
Друзья, в первую очередь хочу поблагодарить вас за высокую оценку моего труда, это приятно, и мотивирует меня продолжать. Итак, почему надо покупать наших слонов я думаю вы уже поняли из первой статьи, кто-то уже скачал и попробовал на вкус, а кто-то только собирается. Как бы там ни было, начнем.
Сегодня мы поставим MongoDB, ниже рассмотрим свежеиспеченный ХабраЛоггер и пошпионим за главной страницей Хабра в реальном времени :-)

Засыпаем кофе и заливаем кипяток
Сначала, выбираем себе релиз по душе и качаем — Downloads.
Вы можете выбрать любой релиз, я использую 1.1.1. Если не боитесь падений, ставьте последний — 1.1.2 (вовсе не обещаю что будет падать, просто это возможно).
Если для вашей ОС есть сборка, советую ставить сборку, если же нет, придётся собрать из исходников. Простейшая установка сборки хорошо дана тут — Quickstart. При сборке из исходников придется работать со Scons, и под FreeBSD возникнет ошибка «shell/dbshell.cpp:77: error: 'sigrelse' was not declared in this scope» — просто закомментируйте эту строчку.
Итак, будем считать что поставили. Если планируется репликация, советую запускать `mongod --master`, с этим ключом MongoDB ведёт лог операций (oplog). Поиграться можно уже сейчас с помощью `mongo test` (test — название БД):
> db.habratest.save({abra: "foo", ka: true, da: 123, bra: {foo: "bar"}}); # коллекция создана и в нее вставлен ряд.
> db.habratest.find({ka: true}); # ищем ряды где ka == true, поиск будет без индекса
{"_id" : ObjectId( "4af18c86977fd21033ca67f8") , "abra" : "foo" , "ka" : true , "da" : 123 , "bra" : {"foo ar"}}

А теперь давайте создадим индекс — вернее удостоверимся в его наличии:
> db.habratest.ensureIndex({"da": 1});
true
> db.habratest.find({da: 123}); # поиск ведется уже по индексу.
{"_id" : ObjectId( "4af18c86977fd21033ca67f8") , "abra" : "foo" , "ka" : true , "da" : 123 , "bra" : {"foo" : "bar"}}
> db.habratest.update({da: 123},{"$set": {pi: 3.14159265}}); # атомарная операция задания свойства
> db.habratest.find({da: 123});
{"_id" : ObjectId( "4af18c86977fd21033ca67f8") , "abra" : "foo" , "ka" : true , "da" : 123 , "bra" : {"foo" : "bar"} , "pi" : 3.14159265}
> db.habratest.update({da: 123},{"$set": {"bra.pi": 3.14159265}}); # задание вложенного свойства
> db.habratest.find();
{"_id" : ObjectId( "4af190176ca438316825ddef") , "abra" : "foo" , "ka" : true , "da" : 123 , "pi" : 3.14159265 , "bra" : {"foo" : "bar" , "pi" : 3.14159265}}
> db.habratest.remove({ka: true});
> db.habratest.find();
> db.habratest.drop();
{"nIndexesWas" : 2 , "msg" : "all indexes deleted for collection" , "ns" : "test.habratest" , "ok" : 1}

Свойство _id генерируется клиентом автоматически, и присуще каждому объекту, при желании, ID можно формировать самостоятельно, вкладывая в него полезные данные.
Думаю, всё понятно. За пикантными подробностями работы функций советую обратиться к документации.
Теперь давайте подключим драйвер к нашему средству разработки, список доступных драйверов — mongodb.org/display/DOCS/Drivers. В статье я буду ориентироваться на PHP, однако и с любым другим языком всё будет вполне аналогично. В PHP драйвер представлен расширением 'mongo' в PECL.

Пьем первую чашку
Я подготовил небольшое приложение-пример — ХабраЛоггер.
Думаю, из кода и комментариев к нему всё понятно, если нет, я с радостью отвечу на вопросы. Возможно вы заметили, что в начале скрипта пара классов наследуется от оригиналов, это не обязательно и лишь дает синтаксический сахар в виде $mongo->mydb->mycollection. Время выполнения мотивирует (0.411 миллисекунды).

Наверняка кто-то из вас заметил операцию group() в куске кода:
$stat['hostsHours'] = $db->hosts->group(
array('hour' => true) // keys
,array('count' => 0) // initial object
,'function (obj, prev) {++prev.count;}' // reduce function
,array('url' => $url) // condition
);

В первом аргументе задаются ключи (поля) для группировки — GROUP BY hour
В втором — исходное состояние объекта, которое будет перед первой итерацией.
В третьем — функция, которая применяется для редукции (уменьшения) множества.
Ну а в четвертом — фильтр исходя из которого будет сформировано множество для уменьшения.
SQL'ный аналог сего действа — SELECT hour, COUNT(*) count FROM hosts GROUP BY hour

Алгоритм ХабраЛоггера довольно прост, лог пишется в коллекцию hits и hosts, hosts содержит уникальный индекс по IP и дню, таким образом туда попадают лишь уникалы в рамках суток, а при выводе графика проводится несложная группировка.
Подчеркиваю, это лишь пример, и конечно же, более оптимальным решением было бы ротировать лог и проводить агрегацию раз в n минут. Но оптимизацию мы рассмотрим в следующий раз.

Кластеризация? Я ждал этого вопроса!
Для начала определимся с терминами:
Sharding — дробление базы данных на несколько шардов.
Shard — один или несколько равнозначных серверов, которые хранят одну и ту же часть данных.
Config-server — сервер, хранящий мета-информацию в первую очередь о том на каком shard'е какой chunk лежит.
Chunk — диапазон документов по индексу, например коллекцию hosts можно разбивать по индекс на свойстве url.
mongos — демон который принимает запросы от клиентов, взаимодействует с нужными shard'ами и config-серверами, и передает готовый ответ клиенту.

Выглядит это вот так:


Не стану вдаваться в пересказ документации, и в описание поведения mongos на каждом типе запроса. Шардинг подробно описан в официальный англоязычной документации — Sharding Introduction.
Скажу лишь — папаша Google использует сходную схему.

Тушим свет, спускаем воду
На сегодня всё. Учтите, что тема следующей статьи будет по заявкам радиослушателей. Могу рассказать о GridFS (файловой системе на MongoDB) и рассмотреть другие примеры.
Спасибо за внимание, друзья! Жду ваших отзывов.

P.S. статистику посещений этой статьи можно посмотреть тут, а статистику другой статьи, которая шпионится с ночи — тут.
UPD: Шпионим за главной страницей Хабра в реальном времени :-)
+45
9014
168

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

+1
boston, #
Руки чешутся попробовать, MongoDB с каждым обзором всё привлекательнее кажется.
+4
paradoxs, #
Раз пошла такая пьянка, писать посты про объектные базы, то я скоро напишу про db4o. Уверен она тоже Вам понравится.
0
TravisBickle, #
Идеология совершенно другая, db4o ведь встраиваемая, как например BDB. Так что целевые аудитории этих продуктов сильно разнятся.
0
paradoxs, #
1. Идеология хоть и другая, но тоже не реляционная
2. Она не только встраиваемая. На основе их документации мы сделали вполне себе настоящий сервер db4o. Конечно с «серверностью» дела там обстоят не очень гладко, но всё решаемо.
3. Аудитории разные, но это же не значит, что о ней не нужно знать
0
paradoxs, #
И кстати сервер делается не так уж и сложно
+1
remal, #
Каковы минусы вешать индексы на все подряд? Насколько это замедлит процесс вставки?

Какова производительность операции group?
+1
TravisBickle, #
Не сильно замедляет, но не измерял. Производительность довольно высокая, хотя злоупотреблять конечно не стоит, надо эту операцию над куском лога делать один раз и запоминать готовые данные, но я ради простоты примера не стал этого делать, иначе бы сложно было для понимания. Производительность зависит от всего — от reduce-функции, от индексов, от железа… посмотрите сами на своем железе и задаче.
0
NeOn4eG, #
img src root.loopback.su/habralogger/ в шапке
ждите нового графика…
0
TravisBickle, #
Если Вы правильно вставили код, то график уже есть, поскольку после редактирования вы сразу попадаете на страницу с постом и график создается ;-)
+1
Olif, #
echo 'Ты такой умный да? Никада однопиксельных картинок прозрачных невидел да? Нас тут тысячи, а ты нас не видишь!';

веселая строчка)
+1
TravisBickle, #
Это для тех кто копирует адрес картинки и открывает в броузере )
0
FTM, #
Насколько быстро бд работает по сравнению с другими?

Впечатляет, кстати, Ваш труд.
+1
TravisBickle, #
Затормозить или разогнать можно что угодно, вопрос лишь в приспособленности архитектуры к шардингу и работе без затыков. На мой взгляд, у MongoDB хорошая гибкая архитектура, которая в то же делает прозрачной работу с СУБД, и не позволяет программисту особо сильно накосячить. С другой стороны, в ней отсутствует транзакционная модель, даже не знаю хорошо это или плохо.
Конкретно узнать насколько можно лишь на реальном железе и реальных тестах, попробуйте если интересно, это вовсе несложно.
Спасибо, я рад.
0
Iskin, #
Даёшь GridFS в след. статье :)
+2
TravisBickle, #
Намёк понял :-) Будет.
Что ещё? Достаточно ли сказано о масштабировании?
0
Iskin, #
Статья немного плохо структурирована, что опасно для Веба. Про мастабирование лучше отдельную статью написать с How To и графиками :).
0
aleks_raiden, #
раскажите подробнее о репликациях и вообще подходе их к этому.

Также у 10gen есть еще пара проектов, например, свой сервер приложений (с которого компания и начинала пока не переключилась на базу)
0
Snick, #
про масштабирование сказано мало.
Нужен реальный пример и workaround'ы
0
sergey_k, #
По поводу _id: как указано, его можно формировать самому. По полю _id индекс есть всегда. Если системное значение _id для вас некритично и есть потребность в индексе по какому-либо одному или нескольким полям (комбинация которых будет уникальна) — можете формировать id сами по нужным правилам. На этом можно сэкономить один индекс на отдельно взятом поле или группе полей, а также быстро адресоваться к конкретной записи по заранее известному значени.
Например для получения объекта { _id: «1234_4456»; a: 1234; b: 4456 }, мы используем $collection->find( array( "_id" => «1234_4456» ) ), что, теоретически, должно быть быстрее, чем $collection->find( array( «a» => 1234, «b» => 4456 ) ).
НЛО прилетело и опубликовало эту надпись здесь
0
TravisBickle, #
Manual — на английском.
Что имеется в виду под интеллектуальным поиском? Что такое сетевая БД?
НЛО прилетело и опубликовало эту надпись здесь
0
TravisBickle, #
Достаточно хранить коллекцию связей видаи id -> [id,weight], и повесить индекс на id.
НЛО прилетело и опубликовало эту надпись здесь
0
Scala, #
А где-нибудь есть сравнение другими KV-хранилищами? Меня в частности интересуют Java-проекты Cassandra(Facebook) & Voldemort(LinkedIn), Hadoop(Apache).
0
aleks_raiden, #
Hadoop разве KV?
0
Scala, #
Сам я с ним не работал, но HBase в какой-то степени я думаю можно к ним отнести.
0
eugyn, #
С Tokyo Cabinet и Tokyo Tyrant наверное еще интересней.

Еще бы бенчмарки в сравнении.
+5
TravisBickle, #
Идея.
У меня давно есть идея распределенной отказоустойчивой платформу как у Google или круче, на которой и поисковик можно написать, и армию ботов поднять не проблема.
Скажите, была бы вам интересна статья об этом? HOWTO повторить Google, с некоторой долей теории, исследований и рассуждений.
0
remal, #
Да. Особо интересно как работает MapReduce на таких объемах данных.
+1
nod, #
Отлично пишите!

Многое есть в оф. документации.
НО ваш личный опыт работы с MONGO ниоткуда не почерпнешь!

— С удовольствием бы прочитал про масштабируемость и отказоустойчивость в формате «как мы это поднимали у себя».

— Интересно узнать про то, сталкивались ли вы с падением шарда, перераспределением данных между шардами (например решили убрать один шард и надо данные распределить по оставшимся), добавление нового шарда.

— Где-то прочитал, что при записи в Mongo комп перегрузился и база упала. На восстановление ушло 30 минут. Как от таких случаев вы защищаетесь на своем продакшене?
0
TravisBickle, #
Спасибо.

— С удовольствием бы прочитал про масштабируемость и отказоустойчивость в формате «как мы это поднимали у себя».

С удовольствием напишу.

— Интересно узнать про то, сталкивались ли вы с падением шарда, перераспределением данных между шардами (например решили убрать один шард и надо данные распределить по оставшимся), добавление нового шарда.

Вы видно перепутали сервер и шард. Если шард полностью упал, то данные уже не спасти (только если из резервных копий), если упал сервер в шарде (их рекомендуется 3 на шард), то всё работает как работало, как только добавляется новый сервер, он без проблем синхронизируется и вступает в бой с клиентами.

— Где-то прочитал, что при записи в Mongo комп перегрузился и база упала. На восстановление ушло 30 минут. Как от таких случаев вы защищаетесь на своем продакшене?

С MySQL бывало и похуже, это нормально. Во-первых сервер не может просто так перезагрузиться — по крайней мере за год работы этого продакшена ни разу не было, во-вторых, два или три сервера уж точно одновременно не навернуться, но если это произошло — восстанавливать и вперед.
НЛО прилетело и опубликовало эту надпись здесь
0
TravisBickle, #
Есть. `mongo [dbname]`, а большего и не надо. Правда.
Хотя я слышал, что какие-то энтузиасты делают проект для просмотра и редактирования коллекций через веб-интерфейс, но мне даже смотреть не захотелось.
0
jiexaspb, #
Интересно.
А сортировку он поддерживает?
Возможно ли на нем сделать подобное:
ORDER BY RAND()*hosts DESC LIMIT 5

то есть выводить 5 случайных документов, но вероятность показа будет больше у тех, у кого hosts больше.
0
TravisBickle, #
Сортировку поддерживает замечательно.
> ORDER BY RAND()*hosts DESC LIMIT 5
Нет. И это огромный плюс… MySQL любит делать фишки удобные для использования, но которые при этом кладут на производительность. Данная конструкция в MySQL вызовет фулскан всей таблицы и положит сервер на немаленькой табличке. Ведь чтобы отсортировать результат и взять сверху 5 элементов, необходимо каждый ряд в таблице проверить и положить в память результат, потом сделать сортировку пузырьком в памяти, а затем уже отпилить маленький кусочек и послать ответ клиенту.
Но есть и правильные пути. Например, задайте индекс по свойству hosts и делайте запросы hosts > min && hosts < max. Где min и max там ограничители выбираемой группы, которые задаются псевдослучайно с задаваемой вероятностью. Еще можно задать какое-то числовое свойство группы (в ней скажем 20 объектов), и выбирать запросом (k IN list) 5 таких групп, а затем выбирать для каждой группы случайный элемент (уже на клиенте). Согласен, может быть не очень красиво, за то это будет хорошо работать и на миллиарде рядов, а вот RAND() загнется куда раньше.
Да, придётся думать, но без этого никуда.
+1
symbix, #
>А сортировку он поддерживает? Возможно ли на нем сделать подобное: ORDER BY RAND()*hosts DESC LIMIT 5

А этот пистолет стреляет? А ногу у меня получится себе отстрелить?
0
jiexaspb, #
Не стоит так все воспринимать.

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

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

Для таких задач что нужно использовать?
Поэтому я и поинтересовался насчет Mongo, вдруг она подойдет.
0
TravisBickle, #
Думаю, для таких как и для любых других нужно использовать в первую очередь разум, Mongo конечно подойдет.

Кстати, в первой задаче (про функцию RAND): индекс на hosts, и find().sort(«hosts»:1).limit(rnd(),1) нужное количество раз. rnd() — Ваша хитрая схема с вероятностями.

Вторая задача (про баннерную систему): думаю я бы инкрементил клики в Memcached, и скидывал бы изменения в MongoDB раз в минуту, например. А выборку баннера сделал бы по индексу само собой.
0
jiexaspb, #
Спасибо!
Очень поверхностно пока знаком с Mongo.
Как я понял rnd это моя фунцкция?

А возможно подобной схемой вытаскивать сразу 5-7 баннеров одним махом?
0
TravisBickle, #
Я тоже пока поверхностно :-)
Да, это Ваша функция которая выполняется в программе и возвращает количество для skip'а.
Неа, нельзя, но это ничего.
0
impersona, #
А с Cassandra Вы не сравнивали?
Тема очень интересная, особенно практические комментарии и примеры.
Буду рад видеть продолжение :-)
0
TravisBickle, #
Нет, не сравнивал. Меня и Mongo вполне удоволетворяет :-)
0
oag, #
А почему не CouchDB? Скорость?
Мне кажется с каучуком по-удобнее — готовый rest-сервис и прозрачность работы из JS кода с клиента… Плюс есть подозрение что при больших базах каучук будет много надежнее.
0
TravisBickle, #
Я прочёл идеологию, меня не вставило. CouchDB решает другие задачи.
0
oag, #
Прошу развить ответ.
0
benone, #
Не подскажете по вопросу ссылок:
Как я понял, ссылок на объекты в MongoDB нет. Как нам поступать на примере тех же комментариев, когда надо вытащить логин каждого автора коммента (разумеется, логин может изменяться, а привязка идет по id).

Спасибо!
0
TravisBickle, #
Вторым $in-запросом по коллекции пользователей. Ссылки там изначально были, но потом от них отказались. Однако можно сделать свою хранимую процедуру на Javascript.
0
benone, #
Вот интересно как раз насколько это быстрее чем join на mysql
0
wmw85, #
Не подскажите где написано, от них отказались? На mongodb.org/display/DOCS/DB+Ref и php.net не нашел.
Когда будут следующая статья о Mongo? Наверняка нашлись новые минусы.
Спасибо.
0
stas_agarkov, #
расскажите про файловую систему на монго

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