Наверх ▲

Почему не стоит использовать MongoDB?

Сергей Туленцев Сергей Туленцев Разработчик популярных приложений для социальных сетей, где имел возможность испытать MongoDB под реальной нагрузкой.

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

MapReduce

Начнем с MapReduce. Как известно, MongoDB относится к классу так называемых NoSQL-решений. Соответственно, в нем нет SQL и соответствующих возможностей по обработке данных. Есть свой язык запросов с какими-то возможностями. Если этих возможностей не хватает, предлагается использовать MapReduce. 

Вообще это мощный механизм, многие его используют. Но конкретно в этой реализации есть два фатальных недостатка. Первый – он медленный, очень медленный. Второй – он однопоточный. 

Оба эти недостатка обусловлены тем, что функции мы пишем на JavaScript, и выполняется это тоже внутри встроенного JavaScript-движка. Термин «однопоточный» в данном случае означает то, что в каждый конкретный момент исполняется только один экземпляр JavaScript-кода. Но экземпляров, запущенных MapReduce, может быть несколько. Чтобы понять, почему это медленно и не очень подходит для RealTime-запросов, подробнее рассмотрим алгоритм работы.

Маппинг

Сначала у нас идет стадия маппинга, мы читаем входную коллекцию. Для этого мы берем блокировку чтения (англ. read lock) и отпускаем каждые 100 документов для того, чтобы дать другим операциям возможность выполниться. 

Далее, как мы считали эту пачку документов, мы для каждого документа выполняем "map". Для этого мы берем блокировку JavaScript и преобразуем документ из Bison в JSON. Если функция "map" у нас выполнила лимит, и нам нужно записать что-то во временную коллекцию, мы пишем это во временную коллекцию. Для этого мы берем блокировку записи (англ. write lock). 

Стадия "Reduce"

Итак, когда-нибудь это закончится, и наступит стадия "Reduce". Здесь мы делаем то же самое, только берем временную коллекцию в качестве входной и функцию "reduce" как функцию, которую надо выполнить над документом. Однако результаты, которые возвратит эта функция, мы не записываем никуда. Мы пока накапливаем их в памяти.

Потом наступает момент пост-обработки, и мы их все разом записываем в коллекцию, которую мы указали как выходную. Для этого мы берем блокировку записи и записываем все "пачкой", а не каждый документ по отдельности. Все эти мелкие блокировки и JavaScript, а также преобразование из Bison в JSON не очень положительно влияют на быстродействие. 

Я это тестировал на таблице в миллион документов. Одни и те же результаты посчитал встроенным языком, встроенными средствами и MapReduce. MapReduce был в 4 раза медленнее при условии, что это был единственный запрос в системе, а другие запросы не выполнялись.

Memory Mapped Files: плюсы и минусы

Memory Mapped Files – это механизм современных операционных систем, который позволяет установить прямое соответствие между файлом на диске и областью памяти. После этого приложение может работать напрямую с памятью, читать и записывать в нее. Система позаботится о сбросе изменившихся участков на диск. В приложении в результате увеличивается быстродействие, потому что доступ к памяти на порядки быстрее, чем системный вызов.

Но есть и недостатки. Недостаток ровно тот же – управление памятью на системе. Мы не контролируем, какие страницы у нас находятся в памяти, а какие вытеснены на диск.

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

Допустим, что у нас есть система, она как-то работает. Кто-то запускает массивный MapReduce на большой коллекции, и он начинает обрабатывать эту коллекцию, "поднимать" все в память, вытесняя индексы. Уже сама эта операция будет небыстро работать, потому что, в основном, происходит чтение с диска. Остальные запросы у нас тоже начнут тормозить. Система может очень долго восстанавливаться после такого запроса.

Если бы мы управляли памятью сами, то теоретически мы могли бы лучше знать, какие области у нас «горячие», какие не надо выгружать или, например, ограничить размер буфера для таких операций полного сканирования. 

Следующий недостаток – это «дыры» в файлах. Когда мы удаляем какой-то документ, он, на самом деле, конечно, физически не удаляется. Он помечается как «удаленный» и будет совсем удален при следующей дефрагментации коллекции. Но до тех пор он будет лежать на диске и занимать место.

Представим себе такой экстремальный случай, что мы удалили большинство документов, но равномерно (4 из каждых пяти). Мы так это дело и оставили, пытаемся посчитать какую-то агрегатную статистику. В результате, чтобы загрузить 5 документов, нам потребуется загрузить в 5 раз больше страниц. Если раньше они умещались, скажем, на одной странице, то теперь на каждой странице будет лежать один реальный документ и 4 фантомных. Это лишняя нагрузка на диск и расход памяти. Конечно, после таких массивных операций надо делать «контакт».

Во второй версии MongoDB, которая вышла недели 2-3 назад, появилась команда «контакт», которая позволяет дефрагментировать отдельную коллекцию без необходимости делать это для всей базы данных. Но такая операция по-прежнему блокирует все остальные операции, поэтому лучше производить ее на ведомом сервере (англ. slave). 

Еще один недостаток, связанный с памятью, напрямую не связан с технологией MMF. В MongoDB нельзя ограничить размер используемой памяти. Специалисты хостинговой компании "Selectel" утверждают, что MondoDB читает объем доступной оперативки при старте и не проверяет потом это значение. Соответственно, это создает проблемы при запуске MongoDB в "облачной" среде, где объем доступной оперативки может как повышаться, так и понижаться.

Мне захотелось проверить эту информацию. Я задал этот вопрос на официальном форуме, но никто из "10gen" мне не ответил. Поэтому предлагаю поверить ребятам из "Selectel".

Одна из самых больших проблем MondoDB – это блокировки и гранулярность блокировок. В моем личном списке недостатков у них вообще первое место. В MondoDB принята модель «1 писатель и много читателей». Причем «писатель» один не на документ или коллекцию, как во многих других БД, и даже не на базу данных. Он один на сервер. Сколько бы баз данных у нас на сервере ни было, только в одну коллекцию в данный момент может прийти запись. 

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

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

Так вот, в версии 1.6 жутко тормозило это удаление. Оно могло висеть часа 3 и блокировало все остальные операции. Я достал всех разработчиков по этому поводу. Они что-то там подкрутили, и перестало тормозить.

Потом вышла версия 1.8, в которой это перестало тормозить. Но миграции продолжали зависать. Выяснилось, что во время миграции чанков происходит вызов некоей процедуры setShardVersion, которая, видимо, изменяет что-то в файлах конфигурации на серверах. Она вызывается несколько раз за миграцию. Она тоже повисала очень легко минут на 20. Иногда отрабатывает, иногда повисает. Устранялось это перезапуском сервера, потому что ждать, пока отвиснет, очень не хотелось.

Запросы и оптимизация

В настоящее время MongoDB может использовать только один индекс при исполнении запросов, даже если всего этих индексов несколько. Соответственно, это отметает всякие продвинутые оптимизации вроде слияния индексов (англ. index merge). При этом индекс не всегда угадывается правильно с точки зрения разработчика. Разработчик думает, что должен использовать один индекс, а используется другой. Почему? Потому что MongoDB выбирает этот план эмпирически.

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

Шардинг

В MongoDB, как во многих других системах, шарды равноправны. Данные на них распределяются равномерно. Однажды, еще будучи излишне оптимистичным по поводу возможностей MongoDB, я добавил новый шард в кластер. Все бы было ничего, но этот шард был в 3 раза меньше текущих машин, которые у меня были. Шард добавился, данные начали раскидываться, я пошел спать. Утром проснулся – данные перераспределились, и все тормозит. Работает, но тормозит, очень тяжело отвечает. На этот шард распределилась треть данных. Было два шарда, я добавил еще один, соответственно, везде стало по трети. Я наивно  думал, что на него положится одна седьмая. Но нет, все начало тормозить, и тормозило еще два дня, пока я изымал этот шард из кластера.

Ошибка, конечно, была моя. Но, кажется, было бы неплохо уметь задавать вес шарда, потому что не всегда удается достать одинаковые машины. В файле конфигурации задавать вес: этот может хранить X данных, этот – 2X, этот – 0,3X. Тикет насчет этого я отправил, но пока ничего непонятно. 

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

Включать его для маленьких коллекций себе дороже, и лучше не стоит. Если у нас много небольших коллекций, то это может создать серьезные проблемы. Например, мы храним какие-то данные, логируем и группируем их по дням. Один день – одна коллекция. Для таких коллекций включать шардинг лучше не стоит, но и на одном шарде они все не поместятся. Для таких случаев советую либо не использовать кластер MongoDB и шардить все вручную, либо применять более традиционные средства.

Мониторинг

Здесь просто. Для каждого приложения должен быть организован мониторинг. Я пользуюсь сервисом, который называется New Relic RPM. Он мониторит приложения, производительность, собирает статистику, возникающие ошибки и много чего другого. В частности, он мониторит работу с базой данных. Потом в панели администратора можно посмотреть, какие запросы к базе данных пользуются популярностью, какую нагрузку создают, среднее время выполнения – много чего. 

Если какой-то запрос к базе данных у нас занимает неприлично большое количество времени, по нему можно посмотреть дополнительную информацию. Какие конкретно запросы к базе данных были отправлены, сколько времени заняли, можно посмотреть конкретные подставленные туда параметры, план запроса – все, что в голову придет.

Так вот, ничего этого для MongoDB не существует. Как объяснили специалисты New Relic, это обусловлено ее курсорной моделью работы. Впрочем, какие-то крупицы информации собрать можно. Ну, на уровне, что «во время этого запроса к веб-серверу у класса user 3 раза был вызван метод find». Инструментируется конкретный фреймворк доступа к данным, а не работа с базой на более низком уровне. Для тех, кто привык к подобной отчетности, это может стать серьезным недостатком. Для меня это является серьезным недостатком, но почему-то я пока еще использую MongoDB.

Второй пункт. На самом деле, это не недостаток. На прошлой неделе в четверг компания "10gen" анонсировала свой сервис мониторинга MongoDB (сокращенно MMS). Это бесплатный сервис. 

Скачивается агент, устанавливается на сервер, настраиваются хосты и снимается статистика по MongoDB. То же самое, что вы могли смотреть через Mongostat, что-то выцеплять в логах. Теперь это централизованно собирается и отправляется на сервис, где это можно смотреть в виде красивых графиков. Если на машине стоит Munin, то он будет брать данные еще из него.

Собственно, вкратце все, что я хотел вам рассказать. Спасибо за внимание. Вопросы, если есть.

Вопросы и ответы

Вопрос из зала: Можно вопрос про New Relic?

Сергей Туленцев: Да, конечно. 

Вопрос из зала: Если вам так хотелось в New Relic увидеть MongoDB, то почему не взять и не дописать? Там 20 строчек. По примеру Memcached – два дня работы.

Сергей Туленцев: В том-то и дело, что такая инструментация уже написана. Да, строк там 10, но она инструментирует конкретный маппер (Mongo1, Mongo mapper и все, по-моему). Конкретно драйвер она не инструментирует. Разработчики из New Relic (я просто забыл, что уже к ним обращался, и написал им еще раз: «Почему у вас нет инструментации?») объяснили, что можно это сделать, но это будет очень затратно с точки зрения производительности или памяти. Это будет тормозить клиентские приложения, чего очень не хочется.  

Реплика из зала: Я просто удивлюсь, если это будет затратней, чем инспектирование Memcached. Для Memcached они написали, тут абсолютно аналогичная ситуация. 

Сергей Туленцев: Нет, а что Memcached? Там ключ-значение: запрос – ответ. Ответ приходит сразу. В реляционных БД, в принципе, то же самое. В MongoDB – курсоры. Вы отправили запрос, получили «пачку» данных, потом эти данные можете долго обрабатывать. Сразу собрать по этому запросу полную статистику, видимо, получается очень неудобно. 

Вопрос из зала: По ощущениям – на какой объем данных можно замахиваться с MongoDB, а на какой страшно?

Сергей Туленцев: Я тут рассказывал пару историй. У меня была база гигабайт на 500. В принципе, она держала неплохие, средние нагрузки. По здешним меркам, наверное, смешные. Тысяч 7 запросов в минуту. Но такие держала. 

Вопрос из зала: 7 тысяч – это со скольких серверов?

Сергей Туленцев: Два шарда.

Вопрос из зала: Насколько я знаю, по умолчанию MongoDB не кидает ответы на записи в базах. По-моему, это большая проблема.

Сергей Туленцев: Если вам нужны ответы, подтверждение того, что выполнен fsync, это можно запросить при отправке команды insert.

Вопрос из зала: Учитывая высказанные недостатки, в каких сценариях стоит использовать MongoDB, в каких не стоит?

Сергей Туленцев: Я ее сейчас использую в сценарии, где количество данных не очень быстро растет, но происходит очень много мелких обновлений, по сути, в «горячие» данные. На диск они не пишутся. Они постоянно обновляются в памяти и потихоньку «скидываются» на диск. Все это довольно быстро работает. Выдает тысяч 30 запросов в секунду.

Вопрос из зала: Спасибо за то, что поделились с нами вашей болью. Почему вы до сих пор используете MongoDB?

Сергей Туленцев: Подкупает отсутствие схемы и быстрота разработки. При каждом новом проекте возникает ситуация, что опять будет тормозить, что-нибудь еще. Но как только представлю себе, что надо будет писать миграции для классов, продумывать заранее структуру… Спасибо!

Комментарии

Нет ни одного комментария

Только пользователи могут оставлять комментарии

Возможно, вам будет интересно:

Георгий Баркан

Георгий Баркан

Руководитель разработки технологической стратегии развития пользовательских продуктов «Лаборатории Касперского».

Рассказ об управлении продуктом с точки зрения успеха этого процесса.

Муслим Меджлумов

Муслим Меджлумов

Начальник отдела безопасности сети компании ОАО "РТКомм.РУ".

Рассказ о такой услуге, как защита от DDоS, под кодовым названием «анализ интернет-трафика».

Александр Калугин

Александр Калугин

Project Director в компании Mercury Development Russia. Докладчик профильных конференций SEF.BY, ReqLabs, SECR.

Рассказ о практическом опыте создания двух отдаленных офисов и распределенной команды на их основе.