Мой первый опыт использования MongoDB

24 октября 2012

Большинство программистов (кроме тех, кто вообще не следит за новостями) наверняка что-то слышали о MongoDB, но никогда не пользовалось этой СУБД. Давайте же выясним, что умеет MongoDB, а что не умеет, а также, вооруженные Perl и Mojolicious, напишем простую сокращалку ссылок, использующую MongoDB.

Теоретическая часть

Что в первую очередь следует знать о MongoDB?

  • MongoDB — это документо-ориентированная СУБД. Данные в MongoDB хранятся в документах, которые объединяются в коллекции. Каждый документ представляет собой JSON-подобную структуру. Проведя аналогию с реляционными СУБД, можно сказать, что коллекциям соответствуют таблицы, а документам — строки в таблицах. Максимальный размер документа в MongoDB 2.x составляет 16 Мб (в более ранних версиях — лишь 4 Мб);
  • В отличие от РСУБД MongoDB не требует какого-либо описания схемы базы данных — она может постепенно меняться по мере развития приложения, что есть удобно;
  • Поддерживаются индексы, в том числе по массивам и вложенным документам, а также геопространственные индексы. Поддерживаются уникальные и составные индексы;
  • Также MongoDB есть атомарные операции, compare-and-swap, курсоры, запись без подтверждения и даже MapReduce (но я бы не спешил переходить с Hadoop на MongoDB);
  • Размер коллекции в MongoDB может быть ограничен числом документов или мегабайтами. Если коллекция слишком разрастется, старые документы будут удалены. Эта возможность может пригодится, если вы собираетесь хранить в MongoDB какие-то временные данные;
  • Интерфейс MongoDB сильно напоминают работу с DBIx::Class. Типа получаем ORM «из коробки». В запросах могут использоваться функции на языке JavaScript;
  • В MongoDB поддерживается журналирование, а также асинхронная репликация двух видов — master-slave репликация и наборы реплик. Разработчики MongoDB рекомендуют использовать последние. Набор реплик представляет собой тот же master-slave, но в случае падения мастера среди реплик автоматически выбирается новый мастер. После возобновления своей работы бывший мастер становится репликой;
  • Пожалуй, самая значительная особенность MongoDB заключается в том, что документы могут быть автоматически сегментированы по нескольким наборам реплик. Сегментирование производится по диапазону; чтобы отнести документ к конкретному диапазону, используется сегментный ключ (shard key). Данные распределяются между наборами реплик так, чтобы каждый набор содержал примерно одинаковый объем данных. Если кластер перестает справляться с нагрузкой, можно просто добавить в него еще один набор реплик — перераспределение данных произойдет автоматически;
  • В документах MongoDB можно хранить бинарные данные — картинки, mp3 и так далее. Однако для данных размером более 1 Мб рекомендуется использовать GridFS. GridFS — это соглашение о хранении файлов произвольного размера в MongoDB, поддерживаемое всеми официальными драйверами. У меня уже чешутся руки написать свой RapidShare, а у вас?
  • MongoDB используют GitHub, SourceForge, Foursquare, Bit.ly, About.me, MTV, CNN, New York Times, Forbes, Disney, EA и многие другие;

Однако за все хорошее приходится платить. В MongoDB атомарность операций гарантируется только на уровне отдельных документов. Например, если сервер с MongoDB будет обесточен во время обновления коллекции документов, часть документов окажется обновлена, а часть — нет. В MongoDB нет join’ов. Приходится выполнять их вручную. При этом следует учитывать, что пока вы делаете join, состояние базы данных изменяется. Также в MongoDB нет транзакций, но можно написать свой двухфазный коммит.

Есть множество других тонких моментов. Например, при обновлении документа в коллекции, ограниченной по размеру, документ не может увеличиваться. Еще MongoDB не позволяет выполнять неоптимизированную сортировку документов (когда производится выборка большого объема данных, а у вас нет подходящего индекса). А еще для работы с файлами MongoDB использует mmap. В связи с этим MongoDB обычно использует больше места на диске, чем другие СУБД. Также, если вы хотите работать с объемами данных, превышающими 4 Гб, вам понадобятся 64-х битные сервера.

Вы спросите, как вообще можно для чего-то использовать такую СУБД, если она не поддерживает даже транзакции? В действительности, в этом нет ничего страшного. Наверняка половина веб-сайтов до сих пор хранят свои данные в MyISAM. И ничего, живем ведь как-то. Скажем, если вы решили написать на MongoDB движок форума, то при удалении темы должны сначала удалить все ответы в этой теме и только потом саму тему. Кроме того, никто же не запрещает вам использовать MongoDB, например, совместно с PostgreSQL.

Пример — простая сокращалка ссылок

В Debian/Ubuntu установка MongoDB производится очень просто:

sudo apt-get install mongodb

Во FreeBSD — чуть сложнее:

pkg_add -r mongodb
/usr/local/etc/rc.d/mongod onestart

Лично я во время установки MongoDB под FreeBSD столкнулся с такой ошибкой:

Creating user 'mongodb' with uid '922'.
pw: user 'mongodb' already exists

Если вы вдруг тоже с ней повстречаетесь, просто выполните после установки следующую команду:

pwd_mkdb -p /etc/master.passwd

Поздравляю, установка MongoDB завершена! Это, конечно, не кластер с наборами реплик и автоматическим сегментированием, но в качестве тестового окружения самое то. Помимо самого сервера MongoDB (mongod) у нас в распоряжении есть ряд полезных утилит, среди которых следует отметить mongodump, mongorestere и mongo. Первые две, очевидно, предназначены для резервного копирования и восстановления из резервной копии. Последняя является оболочкой для работы с mongod. А еще, если зайти на localhost:28017, то можно увидеть веб-интерфейс к mongod с логами, информацией о нагрузке и тп.

Теперь давайте запустим на локалхосте сокращалку ссылок, использующую MongoDB. Исходный код сокращалки вы можете найти на гитхабе. Заметьте, что к моменту, когда вы будете читать эту заметку, код в репозитории может отличаться от кода, приводимого далее.

git clone git://github.com/afiskon/mongo_shortener.git
cd mongo_shortener
sudo ./INSTALLDEPS.sh # устанавливаем зависимости
prove -rl ./t # прогоняем тесты
starman --port 3000 ./script/mongo_shortener

Теперь, если зайти на locahost:3000, то можно будет попробовать сокращалку в действии:

Сокращалка ссылок, использующая MongoDB

Напоминаю, что я программист, а не дизайнер, и делать красивые сайты не умею :) Теперь попробуем разобраться, как же сокращалка работает. Для начала откроем файл lib/MongoShortener/Database.pm:

package MongoShortener::Database;

use strict;
use warnings;
use MongoDB;

my $db;

sub getHandle {
  unless( defined $db ) {
    $db = MongoDB::Connection->new(
      host => 'mongodb://localhost:27017',
    )->get_database('MongoShortener');

    $db->urls->ensure_index({ code => 1 }, { unique => 1});
  }

  return $db;
}

1;

Теперь мы знаем название базы данных. Еще мы видим, что после установки каждого нового соединения в коллекции urls происходит создание уникального индекса по полю code. Если индекса еще нет, он будет создан, если же он уже есть, ничего не произойдет. Насколько я понимаю, создавать индексы прямо в коде, чтобы не напрягать этим админов — обычная практика при использовании MongoDB.

Также следует обратить внимание на файл lib/MongoShortener/Main.pm:

sub _create_short_url {
  my ($self, $url) = @_;
  my $db = MongoShortener::Database::getHandle();
  my $code = undef;
  for (1..5) {
    $code = 1 + int rand(2**40 - 1);
    try {
      $db->urls->insert({ code => $code, url => $url }, { safe => 1 });
    } catch {
      $code = undef;
    };
    last if defined $code;
  }
  die 'CODE_GEN_FAILED' unless defined $code;

  my $short_url = encode_base64url(pack('Q', $code));
  $short_url =~ s/A+$//;

  return HOME_URL().$short_url;
}

sub _resolve_short_url {
  my ($self, $short_url) = @_;
  $short_url .= 'A' x (11 - length $short_url);
  my $code = unpack('Q', decode_base64url($short_url));

  my $db = MongoShortener::Database::getHandle();
  my $doc = $db->urls->find_one({ code => $code });
  return defined $doc ? $doc->{url} : undef;
}

Как видите, в коллекции urls хранятся документы, содержащие поля code и url. Первое из них представляет собой случайное число от 1 до 240 − 1. Поскольку по этому полю построен уникальный индекс, если мы попытаемся создать два документа с одинаковыми code, будет брошено исключение. Поэтому производится до пяти попыток создания нового документа. В поле url, очевидно, хранится сокращаемая ссылка.

Обратите внимание на второй аргумент метода insert. По умолчанию запись в MongoDB производится без подтверждения. То есть, мы просто даем команду «создай/обнови/удали документ такой-то» и не дожидаемся ответа. По понятным причинам в данном случае нам критически важно дождаться подтверждения от сервера. Что и достигается путем передачи вторым аргументом { safe => 1 } методу insert.

Можете немного поэкспериментировать с MongoDB, поработав с ней через оболочку mongo:

$ mongo MongoShortener
MongoDB shell version: 2.0.6
connecting to: MongoShortener

Смотрим список коллекций в базе:

> show collections

Создать новый документ (попробуйте создать два документа с одинаковым code):

> db.urls.insert({ code: 123, url: "http://eax.me/" });

Просмотреть список документов (в поле _id всегда хранится уникальный ID документа):

> db.urls.find();

Создание индекса:

> db.urls.ensureIndex({ code: 1 }, { unique: true });

Просмотр списка индексов:

> db.urls.getIndexes();

Удаление индекса (перезапустите сокращалку и посмотрите, создастся ли он снова):

> db.urls.dropIndex({ code: 1 });

Обновление документа:

> db.urls.update({ code: 123 }, { url: "http://example.ru/" });

Удаление документа:

> db.urls.remove({ code: 123 });

Выход из оболочки:

> exit

Можете в качестве упражнения добавить в сокращалку подсчет числа переходов по ссылкам (подсказка — используйте атомарную операцию инкремента).

Дополнительные материалы

В качестве источников дополнительной информации по MongoDB я бы рекомендовал следующие:

А что вы скажите о MongoDB? Уже пробовали использовать эту СУБД в реальных проектах? Как впечатления?

Метки: , .


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