Надо понимать, что NoSQL- решения не обязательно означают замену и полный отказ от РСУБД. Как обычно, инструмент должен выбираться под задачу, а не наоборот. Когда говорят о NoSQL, обычно перечисляют следующие достоинства.
Масштабируемость. Горизонтальное масштабирование существующих традиционных СУБД обычно является трудоемкой, дорогостоящей и эффективной только до определенного уровня задачей. В то же время многие NoSQL-решения проектировались исходя из необходимости масштабироваться горизонтально и делать это «на лету». Поэтому эта процедура обычно проще и прозрачнее в NoSQL, чем в РСУБД.
Производительность БД на одном узле, а не в кластере также является немаловажным параметром. Для многих задач такие свойства традиционных СУБД, как транзакционность, изолированность изменений, надежность в пределах одного узла или даже сама реляционная модель, не всегда нужны в полном объеме. Поэтому отказ от этих свойств (всех или некоторых) позволяет NoSQL иногда добиваться большей производительности на одном узле, чем традиционным решениям.
Надежная работа в условиях, когда отказ железа или сетевая недоступность – обычное дело, является одним из свойств многих решений NoSQL. Основной способ ее обеспечения – это репликация. Сама по себе репликация отнюдь не является уникальной особенностью NoSQL, но здесь, как и при масштабировании, важную роль играют эффективность и легкость внесения изменений в существующую инсталляцию. Переход БД к работе в режиме репликации – это простая задача для большинства NoSQL-решений.
Простота разработки и администрирования – также важный аргумент в пользу NoSQL-технологий. Целый ряд задач, связанных с масштабированием и репликацией, представляющих значительную сложность и требующих обширной специальной экспертизы на традиционных СУБД, у NoSQL занимает считанные минуты. Задачи установки и настройки, само использование NoSQL-решений обычно существенно проще и менее трудоемки, чем в случае с РСУБД. Поэтому NoSQL-системы стали очевидным выбором для многих стартапов, где скорость разработки и внедрения является ключевым фактором.
NoSQL-решения не обязательно означают замену и полный отказ от РСУБД. Как обычно, инструмент должен выбираться под задачу, а не наоборот
Специализированная модель данных. Реляционная модель не обязательно является самым подходящим способом представления данных для всех задач. При разработке приложений уже давно стало нормой использование специальных «прослоек», отображающих реляционную модель на модель данных приложения, и наоборот. Это увеличивает накладные расходы и усложняет систему в целом. NoSQL предлагает широкий спектр моделей данных и их реализаций, остается лишь выбрать оптимальную для конкретной задачи модель: данные в виде «документов» из наборов полей, записей «ключ–значение», графов и т.д.
CAP-теорема, или Почему нельзя получить всё и сразу CAP-теорема гласит, что невозможно построить такую распределенную БД, которая бы одновременно обладала следующими тремя свойствами: Consistency (непротиворечивость), Availa-bility (доступность), Partition Tolerance (устойчивость к разделению). Consistency подразумевает, что данные всегда должны быть непротиворечивыми, в какой бы момент времени мы ни запросили БД. Availability означает, что все данные доступны в любой момент времени. Partition Tolerance предполагает, что система продолжает работу при выходе из строя одного или нескольких распределенных узлов. В качестве иллюстрации теоремы обычно приводят задачку «сервиса напоминаний».
Допустим, мы решили организовать новый полезный сервис – телефонные напоминания. Любой человек может позвонить вам по телефону, попросить запомнить что-либо, а затем перезвонить снова и попросить напомнить ему то, о чем он говорил при первом звонке. Например, запомнить номер рейса самолета, код от камеры хранения, список покупок и т.д.
Задачка выглядит несложной: сажаем оператора на телефон, даем ему блокнот и ручку. Оператор принимает запросы на запоминание и записывает их в журнал. Соответственно, если получает запрос на напоминание – ищет в журнале запись абонента и зачитывает её.
Первая проблема возникает, когда один человек перестает справляться с потоком звонков. Решаем её в лоб – сажаем второго человека за второй телефон и выдаем ему еще один блокнот и еще одну ручку.
Какое-то время все работает замечательно, но в один печальный день начинают поступать жалобы от пользователей, заявки которых приняли, но выполнить (напомнить им сказанное) не смогли. Причина находится легко: первый звонок с запросом «запомните» пришел к одному оператору, а второй с запросом «напомните мне то, о чем я просил» – ко второму. Это проблема Consistency – данные у операторов противоречивы. Быстро «патчим» проблему: когда одного оператора просят что-либо записать, он до того, как положит трубку, передает информацию другому, чтобы тот тоже ее записал. Теперь уже не важно, к какому из операторов пришел запрос – одинаковые данные есть у обоих.
Возникает следующая проблема: один из операторов не пришел на работу. Это проблема Availability: оператор не может принимать звонки, пока другой отсутствует, т.к. не может передать ему свои данные. Пытаемся решить и ее: если оператор, которому нужно передать данные, отсутствует на рабочем месте, отправляем ему e-mail. На следующий день тот прочитает почту и допишет в свой журнал недостающие данные.
И снова проблема: что если оба оператора на работе, но по какой-то причине не уведомляют друг друга о принятых звонках? В этом случае мы либо продолжаем принимать звонки и рискуем не обработать запросы корректно, либо приостанавливаем работу до тех пор, пока оба оператора не договорятся. Это проблема Partition Tolerance: разделенная система не может продолжать корректную работу, не жертвуя при этом доступностью (не принимаем звонки вообще) или непротиворечивостью (принимаем, но можем отвечать не то, что пользователи ожидают).
В 2002 году CAP-теорема была формально доказана в MIT (Massachusetts Institute of Technology). Таким образом, решения проблемы CAP фокусируются на различных комбинациях двух из трех фигурирующих в CAP-теореме характеристик. NoSQL-системы также не могут обойти ограничение этой теоремы (хотя и пытаются).
Характерные черты NoSQL
Перечислим основные признаки современных NoSQL-решений.
Map/Reduce – это подход к обработке больших объемов данных, который состоит из двух фаз: Map – предварительная обработка входных данных и Reduce – обработка тем или иным способом выборки, полученной на стадии Map. Map/Reduce не является свойством исключительно NoSQL-решений. Аналогичным образом можно работать с большими объемами данных и в РСУБД. Но большинство NoSQL-систем (за исключением, может быть, самых простых key-value хранилищ) реализуют Map/Reduce в том или ином виде.
Преимущества такого подхода очевидны при соблюдении двух условий. Первое – фаза Map реализована так, что её можно запускать параллельно на нескольких узлах. Второе – Reduce позволяет принимать на вход не только результат выполнения Map, но и применения предыдущего Reduce.
В качестве простого примера: допустим у нас есть большая база данных покупателей, распределенная по нескольким узлам, и перед нами стоит задача посчитать среднюю сумму покупок в зависимости от возраста. Тогда в качестве Map будет функция, возвращающая для каждой записи БД структуру вида «возраст сумма покупки количество записей», где при выполнении первой фазы «количество записей» будет всегда равно единице. В качестве Reduce будет функция, складывающая «суммы покупок» и «количество записей» с группировкой по «возрасту». Таким образом, мы получим возможность запустить Map на нескольких узлах параллельно и получить предварительную выборку. Далее мы запускаем Reduce на нескольких узлах и подводим промежуточные результаты, а по итогам – тот же самый Reduce по финальному набору данных, собранному из промежуточных.
Sharding (шардинг) – горизонтальное партиционирование1 – как и Map/Reduce, не является эксклюзивным свойством NoSQL, но большинство таких решений включает простой механизм партиционирования (в том числе автоматического). Например: шардинг данных пользователей по их географическому местоположению (случай, когда мы заранее выбираем ключ партиционирования) или автоматическое партиционирование по синтетическому идентификатору (когда стоит задача равномерно распределить данные по партициям).
Репликация – один из основных способов обеспечения надежности в NoSQL-решениях. Ее алгоритмы могут быть как простыми (копия всех данных на отдельном узле), так и сложными (частичные копии на некоторых узлах системы с задаваемым процентом перекрытия). Так же, как и партиционирование, настройка репликации (в том числе «на живую») в большинстве NoSQL-решений – более легкая задача, чем в традиционных РСУБД.
Выбор из многообразия NoSQL-систем
Обычно выбор того или иного NoSQL-решения начинается с выбора модели данных. Некоторые решения поддерживают несколько разных моделей, но обычно выделяют следующие семейства.
Документо-ориентированные СУБД. Они хранят данные в виде коллекций документов, состоящих из набора полей. Этот набор может различаться в документах одной коллекции благодаря «бессхемности» таких СУБД. В некоторых реализациях допускаются вложенные документы и сложные типы значений полей (массивы, ссылки и т.п.). Идеальный вариант их применения – это хранение более-менее независимых документов, не требующих поддержания ссылочной целостности между ними или коллекциями (форумы или социальные сети, каталоги товаров или изделий).
Также они хорошо подходят для работы с данными нестрогой структуры. Такого рода данные часто встречаются при решении задач логирования и сбора статистики: существует множество типов событий, относящихся к одной категории, но с различными атрибутами. В традиционных СУБД хранение такого рода данных осуществляют двумя способами. Либо записывают основные параметры событий в одну таблицу, а дополнительные поля – в множество связанных таблиц, либо осуществляют сериализацию дополнительных полей в строки, бинарные данные и т.п. Такой подход сильно усложняет логику приложения и затрудняет дальнейшую работу с данными.
Документо-ориентированные БД мало подходят для задач, где требуются ad-hoc сложные запросы: зачастую они не могут реализовать вложенные выборки или выборки по нескольким индексам. Дело в том, что в таких СУБД обычно отсутствуют связи между документами, или они есть, но не форсируются (связи могут становиться невалидными, и отслеживание их целостности возлагается на приложение).
Nosql-системы – такие же противоречивые решения, как и традиционные реляционные СУБД. Ведь, как известно, не бывает плохих инструментов – бывают инструменты, подходящие или не подходящие под конкретные задачи
Самые популярные представители этого семейства – MongoDB и CouchDB. Обе поддерживают репликацию и шардинг, причем MongoDB – шардинг с автоматической миграцией данных при добавлении или отключении «шардов»1 из кластера. Кроме того, оба решения реализуют Map/Reduce. CouchDB использует его в качестве замены сложным ad-hoc запросам: СУБД создает «отображения» (view), данные которых вычисляются операциями Map/Reduce и автоматически обновляются при изменении. MongoDB также поддерживает достаточно сложные запросы. Для этого в решении реализован собственный, не похожий на SQL, язык запросов.
Основное отличие между MongoDB и CouchDB – это разный подход к конкурентным изменениям данных. MongoDB не реализует транзакционность2 и осуществляет лишь простейшие атомарные операции над объектами. CouchDB же реализует управление конкурентным доступом по модели Multi-Version Concurrency Control – с помощью мультиверсионности. Таким образом, в CouchDB изменение документов одним пользователем не видно другим до момента «фиксации» (commit).
Хранилища типа «ключ–значение» (key-value, KV). Такие БД хранят данные в виде пар ключ–значение. В некоторых случаях значениями могут быть массивы, списки, множества (наборы уникальных значений) и т.п. Обычно они реализуют минимальный набор операций (установить, прочитать значение и др.).
Типичное применение этих решений – кэширование данных для повышения общей производительности приложения (например, результатов запросов к более сложным системам) и счетчики. Иногда их применяют в качестве промежуточного звена для систем логирования или сбора статистики.
При этом если задача чуть сложнее, модели «ключ–значение» для ее решения может быть недостаточно, либо потребуется реализовывать множество операций вручную на уровне приложения. Также некоторые хранилища не позволяют держать на одном узле кластера больший объем данных, чем размер оперативной памяти. У них отсутствует функция выгрузки «не поместившихся» данных на диск.
Широкораспространенные представители семейства KV – Redis и Riak. Redis – простая и высокопроизводительная реализация KV-хранилища, написанная на чистом C. Объем данных в нем не может превышать объем оперативной памяти: от поддержки выгрузки «лишней» информации на диск разработчики отказались некоторое время назад. Задачи, которые решают с помощью Redis, обычно требуют максимальной отзывчивости и производительности от хранилища.
Для обеспечения надежности в рамках нескольких узлов Redis использует репликацию, а одного узла – журналирование операций с диском. Помимо обычных для KV-хранилищ команд (установить, прочитать значение), Redis реализует большой набор дополнительных – операции над списками, строками и т.п.
Riak – более сложное решение. Оно в отличие от Redis, обеспечивает работу с данными большего, чем оперативное запоминающее устройство, объема. Riak поддерживает кластеризацию, причем с автоматическим реплицированием. Данные в таком кластере поделены на партиции одинакового размера, причем каждый узел хранит не только свои, но и некоторое количество партиций «соседа». Riak также поддерживает Map/Reduce. Функции Map и Reduce предлагается реализовать на языках JavaScript или Erlang (на нем написан сам Riak).
Колоночные СУБД. В отличие от традиционных, колоночные СУБД хранят данные в виде последовательности столбцов, а не строк. Благодаря этому достигаются некоторые преимущества в хранении и обработке больших объемов информации.
Типичные задачи, которые решают с помощью колоночных СУБД, – те, для которых скорость (многопоточной) записи обычно важнее скорости чтения (хранение и архивирование данных, логирование и сбор статистики). Подобные СУБД лучше подходят для агрегации больших объемов данных, чем для онлайн-обработки сложных запросов. Один из самых известных представителей этого семейства – Apache Cassandra. Cassandra представляет собой гибрид KV и колоночных СУБД и обладает практически линейной масштабируемостью. Добавление новых узлов происходит без простоя или прерывания работы приложений. Для обеспечения надежности в Cassandra используется автоматическая репликация. Сбойные узлы восстанавливаются также без простоя.
Граф-ориентированные СУБД. Такие СУБД эффективно хранят данные, представленные в виде графа: с вершинами (узлами) и ребрами (связями между ними). Идеальны для хранения отношений между множеством сущностей и анализа их взаимосвязей (например, социальный граф, зависимости между компонентами систем и т.п.).
Особой известности графориентированные СУБД не получили, в основном из-за узкого круга решаемых задач, но нам отдельно хочется отметить СУБД Neo4j. Сама СУБД реализована на языке Java и может работать как в выделенном в отдельное приложение, так и во встроенном режимах. Neo4j реализует полный спектр операций с данными в виде графа: создание, изменение, поиск по графу и т.п. Решение поддерживает шардинг и репликацию.
Заключение
Популярность NoSQL-решений растет, и связано это в первую очередь с новыми задачами по обработке ранее не мыслимых объемов данных, с новыми требованиями по доступности и масштабируемости. Несмотря на то что многие NoSQL-системы кажутся (или пропагандируются в таком качестве) «серебряной пулей», на самом деле это такие же противоречивые решения, как и традиционные реляционные СУБД. Ведь, как известно, не бывает плохих инструментов – бывают инструменты, подходящие или не подходящие под конкретные задачи.
Сноски
1 Партиционирование (секционирование) – это разделение набора данных на отдельные части. Применяется для упрощения управления ими, повышения производительности и надежности. Горизонтальное партиционирование – разделение данных по строкам, когда одна часть записей хранится в одной партиции, а другая – в другой. Существует также вертикальное партиционирование – разделение данных по столбцам.
2 «Шарды» – узлы кластера, содержащие партиции данных.
3 В данном случае транзакционность – это выполнение набора операций по принципу «всё или ничего» (либо все операции в наборе будут выполнены успешно, либо происходит откат к предыдущему состоянию БД).