Введение
Коллеги, при разработке приложений, мы каждый день сталкиваемся с потребностью в гибком хранении информации (обновлении, поиске по ней, и т.д.). Класс продуктов, которые решают этот круг задач, как все мы знаем — Базы данных. Но что это такое в нашем понимании? У многих «база данных» твердо ассоциируется с MySQL, таблицами и SQL-запросами. И это устраивает до определенного момента. Действительно, реляционные базы данных дают массу преимуществ в работе: поскольку данные имеют сильную связанность, не нужно контролировать целостность базы данных. Используя простой под-запрос можно выбрать количество комментариев к каждому посту в блоге. Используя JOIN нетрудно делать сложные связанные выборки и получать данные сразу о нескольких сущностях.
Масштабировали, масштабировали, да не вымасштабировали
Однако, когда вдруг понимаешь, что одного сервера перестает хватать, и хочется раскидать базу на несколько физических машин, то первое что предлагается — master-slave репликация, при которой запись идёт на одну машину, а чтение — с нескольких. Но при достаточно большом количестве изменений в скором времени master только и будет делать что раздавать логи, и придётся прибегнуть к изощренной настройке, чтобы каждый узел имел один шефствующий сервер (master) и мог иметь несколько подчиненных серверов (slave). Данная схема весьма сложна в реализации и содержит single point of failure (т.е. при выходе из строя одного сервера, все его подчиненные стремительно превращаются в динозавров). Опять же, все эти ухищрения позволят увеличить лишь количество чтений. А если для внесения изменений в состояние базы данных не хватает мощности одного сервера, то приходится распределять нагрузку, храня разные таблицы на разных серверах. Но тогда потеряется связанность. Или приходится разбивать таблицу на несколько частей, храня их в разных местах, согласно заданному закону (например по ID), однако это унесет в могилу прелести JOIN. Чем дальше мы пытаемся масштабировать реляционные базы данных, тем большим удобством мы за это платим. Например, при использовании master-master, мы заплатим auto increment'ами.
Спасут ли нас MemcacheDB и Redis?
Подобные key-value решения существуют довольно давно. На мой взгляд, MemcacheDB это поделка, паразитирующая на добром имени замечательного продукта: данные там абсолютно не связаны между собой, мы можем лишь проводить операции над значением зная ключ. Я потратил уйму времени на написание инструментария, позволяющего выгодно работать с key-value БД вроде MemcacheDB, и это даже работает, однако пришел к тому что простые насущные задачи решаются настолько криво, что невольно думаешь в сторону реляционки: например, нету даже элементарной реализации времени жизни объекта (TTL), т.е. нельзя взять и удалить сессии старше месяца. Это не доставляет.
Авторы Redis пошли чуть дальше в своих извращенных фантазиях и сделали атомарные списки и set'ы в ключах, однако не сильно продвинулись в облегчении жизни программисту.
Более того, всё еще приходится вручную обслуживать кеш ключей, т.е. сохранять нужные ключи в Memcached и брать их из него, что создает уйму проблем с синхронизацией. При этом также отсутствует атомарность операций: возникает гонка между получением объекта и записью в кеш, CAS демотивирует нас своей производительностью.
Что делать если хотим каталог товаров по типу Яндекс.Маркета?
Допустим, мы хотим сделать каталог, в котором осуществляется поиск по таким параметрам как цена, рейтинг, кратность зума у фотоаппарата и количество передач у велосипеда. Допустим, мы используем реляционную базу данных типа MySQL. Что мы должны сделать? Мы должны либо создать таблицу goods, в которой будет как кратность зума, так и количество передач для велосипеда, и к примеру, показатель гибкости щетины зубной щетки (в этом случае мы упремся в лимит полей для таблицы, или потеряем на дисковом пространстве, скорости, и удобстве), либо мы должны завести табличку вида good_id, key, value и делать страшные JOIN'ы для выборки и поиска, не заикаясь о масштабировании.
Еще можно натравить на это дело Sphinx или что-нибудь похлеще, однако это скорее всего будет выглядеть как забивание гвоздей ноутбуком.
У реляционок налицо серьезная родовая травма.
MongoDB говорите?
MongoDB — резкая как понос объектно-документарная база данных. Идеологически это некий симбиоз между привычной реляционной БД и key-value хранилищем, и на мой взгляд, весьма удачный. С одной стороны, она позволяет делать очень быстрые операции над объектом, зная его идентификатор, а с другой, предоставляет мощнейший инструмент для сложных взаимодействий.
Коллекция — именованное множество объектов, при этом один объект принадлежит лишь одной коллекции.
Объект — совокупность свойств, включая уникальный идентификатор _id.
Свойство — совокупность названия и соответствующего ему типа и значения.
Типы свойств — строка, целое число, число с плавающей точкой, массив, объект, бинарная строка, байт, символ, дата, boolean, null.
Поддерживаются операции выборки (count, group, MapReduce...), вставки, изменения и удаления. Связей между объектами нет, объекты могут лишь хранить другие объекты в свойствах. Поддерживаются как уникальные, так и композитные индексы. Индексы можно накладывать на свойства вложенных объектов.
Поддерживается репликация (даже подразумевается), реализован fail-over.
Реализован MapReduce и шардинг.
Поскольку объекты могут иметь произвольный набор свойств, для каталога достаточно создать коллекцию goods и складывать туда объекты. При этом поиск будет вестись по индексам.
Резкость MongoDB ярко выражена на insert'ах, они происходят ну очень быстро. Кстати, приятно что формат хранения и формат передачи объектов по сети один и тот же, так что для выборки какого-то объекта надо всего лишь найти его позицию по индексу и вернуть клиенту кусок файла определенной длины — никакой абстракции над storage engine.
В качестве уникального идентификатора используется не auto-increment'ное поле, а 12-байтное уникальное число, генерируемое на клиенте. Таким образом, во-первых нет проблемы с синхронизацией реплик, т.е. можно независимо делать вставки на две разные машины, и конфликта не возникнет. Во-вторых, не будет ерунды с переполнением целого числа, ну и после пересоздания базы данных, поисковики не будут адресовать на новые статьи по старым ссылкам.
MongoDB + Memcached? Покруче чем Бонни и Клайд!
С базой данных более-менее разобрались, теперь подумаем как нам всё это дело кешировать, ведь нам же хочется отдавать горячие пирожки со скоростью Memcached!
Во-первых, кеширование самих объектов. Как многие успели догадаться, или узнать из опыта — в большинстве случаев операция выборки объекта по ID является самой частой. Например, выборка объекта пользователя, выборки этого поста из базы Хабрахабра (аминь), и т. д.
Как мы уже выяснили выше, перекладывать сей труд на приложение неразумно, т.к. мы должны были бы нагородить огород распределенных блокировок. Пойдем другим путем, напишем асинхронное приложение, которое подключится к MongoDB и, прикинувшись slave'ом, будет получать лог изменений, и сбрасывать изменения в Memcached (если объект содержит ключ в свойстве _key). Поскольку приложение асинхронно, это будет происходить быстро, однако гонки возникать не будет. Написать несложно, моя реализация тут. Более того, я еще присобачил туда отправку изменений серверу событий, так что мне стоит только изменить объект откуда угодно (да хоть из консоли), как он тут же будет передан всем подписанным на него клиентам.
Кеширование запросов и девалидация кеша должна работать несколько иначе. Существует несколько подходов:
- Кешировать на определенное время. Если запрос один и тот же или их не так много, то можно обновлять кеш не чаще раза в секунду — это уже значительно снизит нагрузку.
- Удалять кеш по требованию приложения. Подход довольно муторный, но имеющий право на жизнь.
- Использовать сервис блокировок для того, чтобы не делать одну работу два раза. Однозначно — это добро.
Минусы с которыми придется столкнуться.
- MongoDB — продукт довольно молодой, и в нем встречаются баги (бывает Segmentation fault, core dumped), появляются новые фичи, и т.д. Я использую его в продакшене, но с осторожностью. Однако, несомненным плюсом является высокий темп разработки (проект пишут не только волонтеры, но и компания людей на полной занятости), так что вы вполне можете рассчитывать на быстрый багфикс, помощь в решении ваших проблем и реализацию ваших идей (если конечно они хорошие). Также доступна коммерческая поддержка.
- Накладные расходы на хранение названий свойств.
- По-умолчанию максимальный размер объекта — 4 мегабайта.
- На 32-битных машинах, максимальный размер одной базы данных — 2 гигабайта.
Finita la comedia!
Страничка проекта — MongoDB.Найденный бенчмарк MongoDB vs MySQL.
Эта статья рискует положить начало циклу о MongoDB. В следующий раз я поподробнее расскажу о шардинге, MapReduce, и приведу живой пример.
Друзья, спасибо за внимание! Буду рад конструктивным комментариям.
Опубликовано продолжение: «MongoDB — варим хороший кофе».