NoSQL. Column-Oriented

13 декабрь, 2013 - 19:04Андрей Зубинский

«Я написал вам длинное письмо, потому что у меня не было времени написать короткое»
Марк Твен

В предыдущей части этого цикла статей оставлен без внимания один важный вопрос, без ответа на который всё остальное будет совершенно бессмысленным. В общем обзоре для него было слишком рано, но теперь время настало. Так что сделаем небольшое отступление, выходящее за рамки названия этой статьи.

Когда употребляется термин NoSQL, непременно за ним подразумевается Big Data. Потому что все особенности NoSQL СУБД являются в том числе и следствиями присутствия в реальности этих самых «Больших данных». И если NoSQL как бы относится к профессиональной лексике, усилиями неисчислимых журналистов «Big Data» стал уже большим чем общеупотребимым, это уже мем. Все слышали про Big Data, но... но кто может объяснить — что это и вообще сколько это? Например, в документе «Руководство архитектора по Большим Данным» от гранда мира СУБД Oracle можно найти всякое интересное, но не оценку, позволяющую определить — эти данные большие, или нет. Полная неопределённость с оценками породила то, что смело можно назвать «карго-культом Больших Данных». Причём обидное «карго-культ» здесь на удивление точно используется — слишком подозрительно многие взывают этим заклинанием к небесам, ожидая прилёт больших железных птиц, сбрасывающих денежные знаки, а также всякие упоительные предметы статусного потребления. Подобная деятельность безусловно может приносить плоды в условиях истерии, но уподобляться не будем вовсе не из эстетской брезгливости, а из простой целесообразности — там уже всё занято, и большие железные птицы просто не успевают к новым шаманам. Смеюсь, конечно.

Давайте попробуем без чрезмерного умствования определить границу, отделяющую Big Data от просто данных, которых много. Например, 10 GB данных — это просто много, или это Big Data? А 100 GB? 1 TB? Есть ли граница и где хоть примерно она сегодня проходит (обратите внимание на курсив — он подчёркивает сиюминутность попытки, завтра оценка непременно будет другой)? Здесь сразу уместно обратить внимание на хитрый цикл, упрятанный в непонятных понятиях NoSQL и Big Data — все NoSQL СУБД в том или ином смысле создавались для преодоления ограничений реляционных СУБД, и всё это делалось для обработки Big Data, масштабы которых и определяют потребность в NoSQL СУБД. Чтобы вырваться из этого цикла неопределённостей, давайте бросим взгляд на хорошие образцы современной серверной аппаратной платформы, например, на материнские платы семейства Intel S4600L*2:

NoSQL. Column-Oriented

Эти платы допускают установку до 48 модулей оперативной памяти с максимальным физическим объёмом 1,5 TB. И всё это удовольствие, по сути, вполне commodity hardware, оно получается совершенно скромной стоимости в итоге (по сравнению с эквивалентными суперкомпьютерами и машинами баз данных совсем недавнего прошлого). Наличие таких аппаратных средств, допускающих хранение и обработку в оперативной памяти порядка 1 TB данных, означает, что нижний предел действительно Больших Данных располагается существенно выше этой оценки. Да, терабайт — это уже очень немало. Но сегодня уже не цифра. Потому что до этого «немало» реальной потребности в масштабировании и прочем просто нет (если они возникают, то это больше вопрос деформации бюджета проекта, в котором расходы на проектирование и реализацию программной части масштабированного решения меньше стоимости одного сервера подобного класса). По эмпирическому правилу «20-80» и с некоторым запасом на скорость развития технологий смело можно увеличить эту оценку в 5 раз, например. И получить нижний предел Big Data — 5 TB. Всё, что больше 5 TB — это Big Data, всё, что меньше — не совсем. Конечно, это очень условно. Конечно, это настолько эмпирически, что почти «с потолка». Но уж лучше так, чем совсем никак. И многие критические ошибки использования NoSQL СУБД как раз связаны с отсутствием предварительного анализа «на пальцах» соответствия задачи понятию «Больших данных». Потому что достоинства NoSQL проявляются именно в масштабных задачах, для того они и делались.

«По теории, разницы между теорией и практикой нет. На практике она есть»
А. Эйнштейн

Теперь можно и перейти к главной героине этой статьи. К СУБД Cassandra. Яркой представительнице класса column-oriented NoSQL СУБД. Без лишних слов: Cassandra — это распределённая, децентрализованная, эластично масштабируемая, устойчивая к сбоям column-oriented СУБД, обеспечивающая высокую доступность и настраиваемую согласованность данных. Все эти термины вскоре получат объяснение, пока же попытаюсь ответить на самый очевидный вопрос — почему для рассмотрения в этом разделе статьи выбрана именно Cassandra? Никакого «коммерческого интереса» я, само собой, не преследую (во всех смыслах — это статья ни «за» кого-то, ни «против»), так что всё просто и без конспиративных теорий — Cassandra распространяется легально бесплатно, на основе очень либеральной лицензии Apache License Version 2.0, как продукт эта система «обкатана» большим числом масштабных инсталляций, она разработана профессионалами высочайшего класса (для интересующихся подробностями — командой Джефри Хаммербахера, не знающим советую поискать, кто это) с учётом реальных требований и для исключительно большого проекта (в Facebook), ну и, наконец, использованные в ней архитектурные решения и модель данных демонстрируют «мультикультурализм» — здесь в одно целое собраны идеи разработчиков Facebook, Apache Foundation, Amazon и Google. Кроме того, Cassandra кроссплатформенная (что значит — работает везде, где есть Java-платформа), развёртывание «игрушечной» Cassandra, для изучения, практически не требует усилий, ну и, наконец, главное... — это один из немногих программных проектов с действительно достойно выбранным названием. Кассандра — дочь последнего царя Трои, Приама, и его второй жены, Гекубы, настолько прекрасная, что сам Аполлон, сражённый её красотой и обиженный её сугубо женским непостоянством (о да, эта красавица умудрилась «продинамить» самого Аполлона!), наделил её способностью предсказывать будущее, но с одним NoSQL ограничением (Аполлон был честным инженером, да) — в мгновенные и точные предсказания Кассандры никто не верил. Ну как после такой истории, скрывающейся за названием СУБД № 1 в своём классе, в первую очередь созданной для решения аналитических задач (тех же предсказаний) в области Big Data, можно ей отказать во внимании? Решительно невозможно, тем более, что в истории Кассандры уже был первый database engineer — Аякс Оилид (меньший из двух Аяксов), который, вроде как, силой добился от Кассандры того, в чём она отказала Аполлону, британский художник Соломон Джозеф Соломон посвятил этому совершенно замечательную работу «Аякс и Кассандра»:

NoSQL. Column-Oriented

Так что немного почувствуем себя маленькими Аяксами и вернёмся к прозе определений. Распределённость Cassandra означает, что система изначально создавалась для исполнения на многих машинах так, что пользователю системы всё это видится одним целым. Употребленный ранее термин «игрушечность инсталляции» в контексте принципиально распределенного характера Cassandra означает, что развернуть систему на одном компьютере очень легко, но в реальности нужно разве что для ознакомительного освоения. Децентрализованность Cassandra говорит о том, что при мультимашинном развёртывании все узлы системы, исполняющие эту СУБД, совершенно равноценны, никаких «master-slave» здесь нет, соответственно, нет и «критической точки сбоя» (single point of failure), присущей централизованным архитектурам (независимо, SQL ли они, или NoSQL). У децентрализации Cassandra есть одно косвенное, но очень важное достоинство — простота развёртывания многомашинных инсталляций. По сути, речь идёт даже не о простоте, а о почти одинаковости инсталляции Cassandra что в «системе» из одной машины, что в многомашинной. Думаю, не стоит уточнять, что это значит в реальных проектах. Эластичное масштабирование Cassandra — термин интересный. Без попыток заумных определений — это дополненное максимальным удобством и выполнением требования минимального вмешательства горизонтальное (за счёт добавления новых вычислительных узлов) и вертикальное (за счёт повышения технических характеристик узлов) масштабирование. На деле и на уровне объяснения «на пальцах» это выглядит так — вы добавляете к многомашинной инсталляции (кластеру) Cassandra новый узел с развернутой системой, кластер сам определяет этот факт, и, собственно, всё — вам нет нужды перезапускать процессы, изменять запросы, выполнять вручную или с помощью скриптов балансировку данных. Cassandra всё это сделает сама. То же самое касается и вертикального масштабирования: если вы используете commodity hardware, которое не допускает, например, замены «на лету» модулей памяти и процессоров, вы просто отключаете нужную машину, при этом кластер Cassandra невидимо перестраивается, вы выполняете механическую работу и заново включаете машину — кластер опять невидимо адаптировался. Очевидно, что косвенно эластичное масштабирование означает и высокую доступность, и устойчивость к сбоям. Однако высокая доступность не абсолютное понятие из ACID-терминологии, и в Cassandra степень «высоты доступности» уточняется настраиваемой согласованностью данных. В терминах ACID согласованность данных понимается строго и полноценно именуется «strict consistency» — это независимое ни от каких факторов переведение базы данных из одного допустимого состояния в другое в результате одной транзакции. На человеческом языке (и в смысле наблюдаемых пользователем результатов) это же можно определить так — операция чтения записи всегда возвращает то, что в неё внесла последняя «пишущая» транзакция. Если мы говорим о распределённой децентрализованной системе, строгая согласованность означает, что все изменения базы данных затрагивают одновременно все узлы, из которых «собрана» эта база данных как одно целое, и клиент, инициировавший транзакцию, получит подтверждение её завершения только после того, как базы данных всех узлов перейдут в новое состояние. Но вот тут как раз, с увеличением масштабов, с переходом от идеального мейнфрейма (достаточной производительности и требуемой ёмкости подсистемы хранения данных) к группе географически разнесенных датацентров с тысячами машин в каждом, возникает один неприятный вопрос. А именно, что такое «последняя транзакция» и что такое «одновременно»? Ответ на этот вопрос требует, очевидно, идеального согласования времени и сопровождения каждого запроса на транзакцию отметкой времени её инициации. Раз мы использовали одновременно понятия географической разнесённости и идеального согласования времени, мы неизбежно попали в область чего-то совершенно неидеального, что надо строить с учётом неидеальности мира, в первую очередь, с учётом конечной скорости распространения сигналов и непредсказуемости временных задержек в неподвластных нам подсистемах. Впрочем, это уже выходящие за рамки Cassandra нюансы, разработчики этой системы вообще отказались от попыток создания механизма идеального согласования времени (векторных часов и прочего). Но для уточнений пока рано. Пока и этого достаточно.

А нам для дальнейшего рассказа понадобится знание так называемой CAP-гипотезы (подтверждённой даже теоретически, но всё же не являющейся полноценной, в математическом смысле, теоремой). Эта подтверждённая гипотеза утверждает, что в общем случае невозможно создать распределённую СУБД, одновременно гарантирующую и полноценную согласованность (когда на все одновременные одинаковые запросы система выдаёт одинаковый ответ при наличии одновременных обновлений её состояния), и полноценную доступность (система обеспечивает обслуживание запросов всем и всегда), и полноценную толерантность к разделению (многомашинная реализация системы продолжает выполнять свои функции при наличии сбоев узлов или в собственной сети, соединяющей узловые компьютеры системы). Из трёх перечисленных характеристик, согласно гипотезе CAP, в любой СУБД можно обеспечить только две. Есть и более жесткие гипотезы в тех же терминах, утверждающие, например, что вообще невозможно создать одновременно обеспечивающую доступность и согласованность распределенную СУБД, но они доказательной теоретической базы не имеют (некоторые строятся на различных толкованиях последствий от сбоев в узлах и сбоев в сети, мы в эти нюансы вдаваться не будем). Иными словами, идеальная в CAP-смысле СУБД находится в единственной точке пересечения всех трёх кругов на следующем рисунке, и, так как точка — абстракция, ещё и сугубо математическая, то и идеальной CAP СУБД в реальности места нет.

NoSQL. Column-Oriented

CAP-диаграмма приведена здесь не для красоты, три образованных пересечениями кругов лепестка в ней используются для дополнительной классификации всего NoSQL разнообразия. Рассматриваемая нами Cassandra, благодаря специфическим проектным решениям, занимает в CAP-диаграмме выделенное красным овалом положение — её разработчики приняли решение, что система должна обеспечивать доступность и толерантность к разделению, это так называемая AP-система (Availability-Partition-tolerant). Естественно, «согласованность вообще», идеальная согласованность для такого класса СУБД — нечто, достижимое только при определённых ограничениях на архитектуру распределённой системы в целом, этот параметр задают в самом начале проектирования всей системы на основе Cassandra, и он определяет очень много в последующем проектировании, потому и использованные в Cassandra проектные решения обеспечивают «настройку» этого параметра, и при правильном подходе к проектированию даже в сложных случаях эта СУБД обеспечивает (богатый практический опыт показывают, что действительно обеспечивает) время, при котором проявляется несогласованность данных, на уровне тысячных долей процента от общего времени работы системы. Этот нюанс — ещё один критериальный для тех, кто выбирает СУБД «под задачу» — если ваша задача допускает такое, то ставьте плюсик Cassandra, как потенциальному претенденту.

Мы добрались до той точки, в которой совершенно невозможно объяснять, например, некоторые нюансы Cassandra как AP-системы без ознакомления с её моделью данных. Поэтому мы продвинемся немного вперёд, чтобы потом вернуться к некоторым уточнениям (увы, по-другому не получится). И здесь нам придётся в который раз столкнуться с условностью всех определений в мире NoSQL — я не виноват, не я придумывал устоявшуюся терминологию.

«И следы вокруг натоптаны, как бы собачьи, но в ботинках, и самое интересное, что следы эти уходят вниз, под землю, и там теряются»
Неизвестный культовый автор «из интернета»

Итак, Cassandra называют column-oriented СУБД. И NoSQL-фильтр всем позволяет без сомнения утверждать, что это NoSQL система. Фильтр в этом случае не даёт сбоя. Но на этом область внятного заканчивается. И начинаются в каком-то смысле сюрпризы. Потому что модель данных Cassandra весьма нетривиальна и заставляет попотеть даже опытных специалистов в базах данных. К тому же внятных описаний её не так уж и много (вообще, документация большинства NoSQL систем — отдельный предмет для молчания со стиснутыми зубами).

Рассказ о модели данных Cassandra мы начнём с того, о чём надо один раз узнать и потом... забыть. С аналогий в терминах из миров реляционных СУБД и Cassandra. Дело в том, что они часто используются для внесения путаницы и увеличения энтропии, так что совсем проигнорировать их нельзя:

NoSQL. Column-Oriented

Эти аналогии, как всякие аналогии вообще, с грехом пополам применимы для приобретения набора заблуждений о Cassandra теми, кто имеет опыт работы с реляционными СУБД, но совершенно непригодны для реального использования, особенно для «переноса» готовой реляционной модели в NoSQL мир этой системы. В общем, если они вам помогли немного дезориентироваться, сейчас станет понятно, почему они совершенно бесполезны и вам следует как можно скорее их забыть (да и вообще всё забыть, это мир NoSQL). В дальнейшем мы, чтобы окончательно разрушить все иллюзии относительно возможных аналогий, начнём движение по модели данных Cassandra снизу вверх, от самых базовых конструкций к тем, которые определяют высокоуровневую функциональность этой системы.

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

NoSQL. Column-Oriented

Сразу уточним, что ассоциативные массивы с упорядочиванием по значению ключей в терминах NoSQL, в частности, соответствуют key-value storage (порядок от меньшего к большим значениям обозначен стрелкой):

NoSQL. Column-Oriented

Введение упорядоченности ключей даёт возможность с очень хорошей скоростью выполнять итеративные операции над множеством элементов ассоциативного массива, сохраняя, естественно, исключительно высокую скорость доступа к отдельным значениям по ключам.

В терминах и в основе модели данных Cassandra лежит именно эта структура данных — ассоциативный массив (или key-value storage), только каждый отдельный ключ именуется Column (или Column key, эти понятия традиционно взаимозаменяемы). В роли Column key может выступать всё, что представимо массивом байтов и позволяет реализовать отношение порядка (это очень важная деталь, многое усложняющая, потому что ключи столбцов в Cassandra сами могут являться по совместительству и «контейнерами» для данных, что означает — в Cassandra возможны и используются как бы key-value пары, но без value, потому что ключ сам является и данными, реализация же механизмов упорядочивания для контейнерных данных в ключах находится в области ответственности программиста). Существенным отличием от «дублета» «ключ-значение» в модели данных Cassandra является дополнительная для каждой пары «отметка времени последнего обновления» (timestamp), причём временная шкала для неё используется не универсальная (идеальные часы), а клиента, который эту пару обновлял, и вся ответственность за формирование timestamp переложена на клиента, и, что особенно важно — не следует относиться к этой отметке как к чему-то полезному вне мира собственно Cassandra, это информация сугубо для системы и сугубо в системных целях, к прикладной области эта отметка не имеет никакого отношения, но мы её для полноты картины всё-таки отобразим:

NoSQL. Column-Oriented

К дополнительным «временным» характеристикам в Cassandra следует отнести и необязательный указываемый программистом «срок годности столбца», названный фотографической аббревиатурой TTL (Time To Live). «Испортившиеся» столбцы с утратившими актуальность данными автоматически удаляются при исчерпании «срока годности», что полностью освобождает программиста от необходимости периодического сканирования базы данных для поиска неактуального. Две вещи, которые не стоит забывать о TTL — они есть, и могут указываться только для «столбца».

Итак, «столбец» (Column) модели данных Cassandra — это элемент ассоциативного массива, дополненный до «триплета» «ключ столбца — значение — отметка времени» или даже до четвёрки «ключ столбца — значение — отметка времени — TTL». Но это далеко не всё, это только начало (иначе Cassandra была бы просто key-value storage, а не column-oriented СУБД).

Ассоциативный массив как абстрактный тип данных — структура очень гибкая, и никто не запрещает реализовать «ассоциативный массив ассоциативных массивов», логически сгруппировав множества столбцов в группы (суперстолбцы), например так:

NoSQL. Column-Oriented

Мы почти добрались до модели данных одной записи в Cassandra (почему почти — будет чуть дальше, это ещё не всё). Она — упорядоченный ассоциативный массив упорядоченных ассоциативных массивов, ключами в которых могут выступать универсальные контейнеры «для байтов», сами хранящие данные. Ассоциативные массивы позволяют изменять размеры записи «в ширину», добавляя сколько нужно «столбцов» (сугубо для иллюстрации — Cassandra допускает порядка двух миллиардов «столбцов» в одной записи), отношения порядка обеспечивают быстрое выполнение итеративных операций. Теперь осталась самая мелочь — назвать эту конструкцию «строкой» (row) и все эти «строки» ... записать в очередной ассоциативный массив с собственными ключами «строк»:

NoSQL. Column-Oriented

Эта «весёлая картинка», по сути, и есть основа высокоуровневой модели данных Cassandra. Главной особенностью её реализации в Cassandra является принципиально «разреженный» характер на всех уровнях вложения этих ассоциативных массивов — если данных в них нет, они занимают 0 байтов.

На некотором выдуманном псевдоязыке программирования, где есть тип данных «упорядоченный ассоциативный массив» SortedMap { ключ, данные }, изображенное на предыдущем рисунке можно записать примерно так (надеюсь, что в этой записи теперь всё понятно без лишнего многословия, включая и термины Cassandra для «семейств» столбцов и суперстолбцов):

NoSQL. Column-Oriented

Само собой разумеется, что такую структуру данных можно без особых усилий реализовать на любом хорошем языке «быстрого программирования», в некоторые языки такое вообще встроено как штатное (например, в R), и, больше того, если вернуться к началу статьи и вспомнить оценку «больших данных» — когда приходится иметь дело со сравнительно небольшими данными, вместо эпической битвы с продуктом масштаба Cassandra лучше использовать именно языковые конструкции, позволяющие на недорогой современной рабочей станции с объёмом памяти в районе 16-64 GB без усилий «промолачивать» многогигабайтные информационные массивы.

Но подождите расслабляться, это ещё далеко не всё о модели данных Cassandra. Например, на уровне, где мы пока находимся, есть ещё «композитные ключи столбцов». Не в самой Cassandra как программной системе. Именно в модели данных. Вообще, это такой прекрасный раздел эзотерических знаний о Cassandra, о котором в презентациях самого разработчика системы говорится дословно так: «ветераны Cassandra могут использовать передовые техники, например, композитные ключи, которые в действительности нигде не документированы, хотя кое-где кое-кем всё-таки как-то описаны, ищите с помощью Google». Ну да, ветеранам уже ничего не страшно, а мы можем даже позволить себе повеселиться — в ходе «боевого» применения Cassandra в Twitter эмпирическим путём было сделано открытие — если в модели данных используются суперстолбцы (ассоциативные массивы ассоциативных массивов), то, оказывается, система не может эффективно искать по ключам (индексировать) во вложенных ассоциативных массивах (если быть более точным — раньше вообще не могла, теперь не может эффективно). Поразительным фактом кажется это эмпирическое открытие — система из нескольких десятков мегабайт исходных текстов, а фундаментальные нюансы реализации её познаваемы только эмпирическим путём. Здравствуй, прекрасный мир agile разработки. Впрочем, теперь мы знаем, что с «суперстолбцами» надо быть поаккуратнее, плодить внутри них множество «столбцов» — не является рекомендованной практикой. А для повышения эффективности можно использовать композитные ключи — так как в Cassandra ключом может быть в общем случае массив байтов, программист может выбрать какое-то ему нравящееся и подходящее (то есть, не встречающееся в самих ключах) значение в качестве разделителя и «собрать» в этот массив сразу несколько ключей. Естественно, разбираться с этой композитной конструкцией программисту придется самому.

Теперь, когда фундаментальное более-менее понятно, настало время подняться в этой системе абстракций выше. Тем более, что ничего особенно пугающего уже не будет. То, что мы называли «строкой» (row), в максимальной конфигурации ассоциативный массив ассоциативных массивов ассоциативных массивов (ну, извините), — в Cassandra упрятали в ещё один ассоциативный массив, тоже упорядоченный. И назвали его для усиления энтропии Column Family, несмотря на то, что в принятых в системе терминах он состоит из «строк», rows. Я умышленно обращаю внимание читателя на совершенно дикую условность всей этой странной терминологии. Кажется, долгое засилье реляционных СУБД сыграло одну действительно нехорошую роль — переставленная «на SQL рельсы» система образования сформировала море таких специалистов по базам данных, для которых приемлем только жёстко детерминированный табличный мир, и разработчики NoSQL систем вынуждены «заигрывать» с этой аудиторией и использовать привычные ей термины в самых вычурных значениях, от чего никому лучше в итоге не становится.

И, наконец, самый высокий уровень абстракции. Четырехуровневое вложение ассоциативных массивов «пакуется» в контейнер (тоже ассоциативный массив) пятого уровня, называемый «пространством ключей» — Keyspace. В дополнение к потенциально множественным Column Families (одно пространство ключей может содержать одну или много Column Families, ограничений здесь нет, единственное существенное ограничение — проектирование всей этой иерархии, формальных методов для него нет, да и реальных «лучших практик» тоже, это строго область эмпирических знаний) этот массив содержит важнейшие описатели распределённого построения базы данных. И мы, продвигаясь по модели данных снизу вверх, наконец дошли до возможности «отката» к уточнению настраиваемой доступности данных Cassandra. Пора совершить ещё один кульбит (Cassandra — это сплошные кульбиты, увы), и взглянуть на всё это с самого высокого уровня, чтобы потом прийти к одной точке фундаментального понимания основ, дальше которых совершенно невозможно продвинуться что в рамках статьи, что в рамках книги среднего объёма.

С высоты птичьего полёта многомашинная инсталляция Cassandra, именуемая кластером (cluster) — некоторое количество машин. Кластер — хранилище «пространств ключей». Физически в этом хранилище каждое семейство столбцов (Column Family) хранится в отдельном файле, что в сочетании со свободой формирования семейств столбцов означает — все низкоуровневые нюансы доступа непременно скажутся на высокоуровневых показателях работы системы, если под свободой формирования семейства столбцов понимать «что хочу — то и отношу к Column Family, потому что это просто сделать».

Вообще, этому очевидному соображению следует уделить особое внимание – как уже должно быть понятно, если бы речь шла о сугубо «in memory» СУБД (работающей исключительно с расположенными в оперативной памяти хитрыми структурами данных), всё было бы намного проще. Но когда идёт речь о хранении данных на долговременных носителях, для которых имитация прямой адресации отдельных элементов в данных – очень дорогое удовольствие в смысле времени, всё становится исключительно сложным. Это особенность практически всех NoSQL СУБД, о ней нельзя забывать.

Так как «семейство столбцов» (Column Family) на деле является множеством строк (rows), имеющих свои ключи, которые позволят находить нужные «строки» и уже из них выбирать по ключам «столбцов» данные, то и основная логика кластера Cassandra строится вокруг использования системой этих ключей. В частности, механизмы системы обеспечивают отображение «строк» на множество целочисленных значений (в диапазоне от 0 до 2127–1), причём эти механизмы могут сохранять в отображении порядок (а могут не сохранять — это задаётся разработчиком БД).

На этом единственно допустимом для статьи погружении в конкретику — указании масштабов пространства ключей, мы остановимся в падении в дебри деталей, опять поднимемся высоко-высоко и попробуем посмотреть теперь на всё в целом. Потому что пытаться рассмотреть что-то в такой сложной системе, как Cassandra, ограничивая поле зрения только, например, областью видимого программисту — заведомо заводить себя в тупик. Это не означает, что ничего вообще при таком подходе сделать нельзя, безусловно можно что-то и сделать, и даже добиться работоспособности сделанного. Только потом, как показывает практика, можно делать ещё и «внезапные открытия», не обязательно приятные, история эксплуатации Cassandra демонстрирует нам презабавнейшие факты (об одном таком будет дальше) из области познания больших систем эмпирическим путём. Поэтому сразу попробуем взглянуть на Cassandra с высоты «птичьего полёта» и даже выше.

С самой большой высоты, то есть, с минимальной степень детализации, архитектура распределённой системы, использующей Cassanda, сложностью не пугает. Это кластер (Cassandra Cluster, в терминах системы) — множество узлов (nodes):

NoSQL. Column-Oriented

В этом «сложном» рисунке есть два важных нюанса. Во-первых, узлы (nodes) умышленно изображены разными. По идее, никаких особых требований к гомогенности аппаратных средств и системного ПО, из которых выстроен Cassandra Cluster, нет. В идеале и на уровне принципиальных требований. Несмотря на это, само понятие «гетерогенный кластер» в мире Cassandra не отменено. Оно просто приобрело специфический смысл. Критерием, определяющим гетерогенность, является «неодинаковость» ключевых ресурсов узлов — в первую очередь числа процессорных ядер и объёмов оперативной памяти. Ярко выделенное курсивом в этом абзаце «в идеале» означает, что в реальности всё обстоит несколько не так. И здравый смысл никто не отменяет, поэтому, если вам когда-либо вздумается построить Cassandra Cluster, постарайтесь сделать его гомогенным и максимально уменьшить отличия в аппаратных средствах и системном ПО узлов. Мои личные попытки собрать игрушечный (но неигрушечно гетерогенный) кластер по принципу «слепить из того, что есть» выявили много интереснейших нюансов-головоломок, с которыми лучше категорически не связываться при создании реальной (а не учебно-потешной) системы.

Во-вторых, Node 2 и Node K изображены явно несколько отличающимися от всех прочих. Это тоже сделано умышленно. Не то, чтобы в Cassandra Cluster «все узлы равны, но некоторые равнее», но непременно должны быть определены специфические узлы, играющие роль даже не координаторов, а «информированных обо всём обитателей» — seeds в терминах Cassandra. Эта специфическая роль необходима и исполняется только в одном случае — при подключении нового узла к кластеру. Именно к seeds-узлам новый узел и обращается за сведениями об «обитателях кластера». Короче говоря, это те самые старушки на лавочке под подъездом — всё знающие о всех жильцах и никак не влияющие на «жизнь подъезда» в целом. Наличие seeds — обязательное условие работоспособности кластера, и документация Cassandra жёстко требует (дословный перевод): «Каждый узел кластера должен располагать одинаковым списком seed-узлов, представленным в виде разделенного запятыми списка их IP-адресов» (для уточнения «где искать дальше» — этот список является частью конфигурационного файла cassandra.yaml). Если речь идёт о нескольких «подъездах» и их «старушках», то есть, о кластерах из разных локальных сетей (например, в географически разнесенных датацентрах), в списке seeds обязательно должны присутствовать хотя бы адреса одного seed-узла из каждой сети.

Узлы Cassandra Cluster находят друг друга в кластере и связываются между собой с помощью специально созданного для этой цели протокола Gossip из одноимённого класса gossip-протоколов (сплетничают, короче). При первом подключении узла к кластеру он «расспрашивает старушек» — seed-узлы, после этой процедуры переходит к «знакомству с обитателями» и раз в секунду обменивается информацией о кластере и себе максимум с тремя узлами кластера. Очевидно, что при такой способе распространения «сплетен» через некоторое время все узлы кластера «узнают всё обо всём».

Мы пока не будем говорить о модели данных Cassandra, попробуем обойтись без неё, и уяснить смысл и назначение одного из ключевых механизмов системы — «разметчика» (patrtitioner). И для этого нам придется сделать полезный экскурс в очень интересный и сравнительно новый (первая работа датируется 1997 годом) алгоритмический раздел «согласованного хеширования» (consistent hashing). Это тем более полезный экскурс потому что «согласованное хеширование» используется в том или ином виде практически везде, где нужен механизм, позволяющий распределять какое-то количество ресурсов по какому-то количеству исполнителей так, чтобы распределение получалось равномерным, а сам механизм не требовал изменений при изменении числа исполнителей. Так что начнём с согласованного хеширования.

Предположим, что у нас есть некоторое довольно большое множество случайных чисел с равномерным распределением { xi } и есть несколько, например, — N, «коробок» (вёдер, чего угодно), в которые мы хотим «разложить» числа множества так, чтобы они мало что все поместились, так ещё и в примерно равных количествах в каждой из «коробки». Само собой подразумевается, что N несоизмеримо меньше мощности множества чисел. Самый простой способ решения такой задачи — использование операции «остаток от деления на N», обозначаемой как mod N. Каждое число множества мы «отправляем» в «коробку» с номером, равным остатку от деления этого числа на количество «коробок». Легко проверить «на бумажке» что произойдёт при применении такой операции для неслучайной последовательности чисел от 0 до 99. Но в 2013 году это моветон, посему воспользуемся онлайновой «бумажкой» и посмотрим наглядно — открываем интерпретатор языка Python и вводим «сложную» программу, раскладывающую по четырём «коробкам» (с номерами от 0 до 3) последовательные числа от 0 до 99 (я не буду дешифрировать запись, пользуйтесь ей как калькулятором) и показывающая, в «коробку» с каким номером попадает какое число последовательности:

print(list(map(lambda yy: yy%4, list(range(100)))))

Которая как раз и применяет операцию «остаток от деления на N» (почти в общепризнанном синтаксисе, не только в Python — «%») к элементам списка из 100 чисел от 0 до 99. И наблюдаем результат:

NoSQL. Column-Oriented

Из которого видим, что последовательные числа «раскладываются» по «коробкам» с номерами от 0 до 3 по, так сказать, «кольцевому принципу», потому что последовательность номеров «коробок», в которые числа «отправляются», повторяется. Слово «кольцо» выделено здесь не случайно, в документации Cassandra (и многих распределённых NoSQL СУБД) «виртуальная кольцевая топология» будет присутствовать непременно, и в основе её лежит именно этот математический приём, а не что-то специфическое «компьютерное». Операция «остаток от деления на N» в современных процессорах дешёвая в смысле вычислительных ресурсов и времени, что особенно приятно, поэтому подобная простая схема «раскладывания по коробкам» выглядит очень привлекательной — для любого числа мы очень быстро можем вычислить, в какой «коробке» оно находится или должно находиться. Что есть замечательно, если число «коробок» неизменно. Но что будет, если мы изменим N? Например, сделаем N равным шести. Давайте «скормим» второй вариант нашего калькулятора:

print(list(map(lambda yy: yy%6, list(range(100)))))

Результат очевиден:

NoSQL. Column-Oriented

«Кольцевой принцип», конечно, не изменился. Но кое-что изменилось. «Коробок» стало больше, и в каждую из них теперь за один проход по «кольцу» номеров коробок от 0 до 5 попадает другое количество чисел. Этот прискорбный для практики факт означает — если числа всё подаются и подаются нашему механизму раскладывания их по «коробкам», и сначала у нас есть всего 4 коробки, а потом мы добавляем ещё две, — всё пропало. Изменив алгоритм «раскладывания по коробкам» для шести коробок, мы не можем правильно определить в какой «коробке» находятся числа, «положенные» в неё тогда, когда «коробок» было четыре. Надо всё раскладывать заново, чтобы восстановить порядок.

Собственно говоря, способ устранения этого эффекта — основа основ большинства распределённых NoSQL СУБД. В них вместо использованной нами демонстрационной последовательности чисел на вход алгоритма «раскладывания по коробкам» подаются ключи записей базы данных (в Cassandra — ключи «строк», row keys, об этом будет дальше), преобразованные в числа с помощью вычисления какой-либо хеш-функции, а «коробками» являются настоящие коробки серверов кластера. Которые самим принципом определения номера коробки-сервера для заданного числа и как бы соединяются в виртуальное кольцо. А возможность изменения числа коробок-серверов без необходимости полного «перекладывания всего с места на место» обеспечивается алгоритмом, который и называется «согласованным хешированием» (consistent hashing). Я не ставил перед собой задачи вдаваться в детали реализации разных вариаций согласованного хеширования (это сделано в достатке без моих усилий), но и не показать откуда берётся «виртуальное кольцо» и потребность в согласованном хешировании не мог себе позволить.

Теперь возвращаемся к Cassandra, к механизму «разметчика» (partitioner). Собственно, его назначение мы уже определили — он как раз реализует в том числе и алгоритм согласованного хеширования (и для этого алгоритма в терминах Cassandra называется Random Partitioner) и тем самым формирует виртуальное кольцо и определяет его «разметку», то есть, — в какой из ящиков-серверов положить данные с определённом ключом, или в каком их искать. И обеспечивает эффективное соблюдение закономерности «ключ — номер сервера» при изменении числа серверов в кластере. А для ещё большего снижения зависимости от физических характеристик реального вычислительного кластера (в первую очередь — от используемого числа узлов), Cassandra предлагает концепцию «виртуального узла». И поддерживает её программными средствами. Один физический сервер-узел кластера может содержать несколько «виртуальных узлов», и механизм разметчика, естественно, будет это учитывать. И такой подход позволяет добавлять к кластеру, например, негомогенные (в смысле характеристик) узлы, или в прежних терминах — «коробки», например, «увеличенного объёма». В них очевидно можно разместить больше записей, но принцип согласованного хеширования по логике предусматривает «коробки» одинаковой ёмкости, и возникает противоречие. Для устранения которого и используется представление узла набором «виртуальных коробок», емкость которых в кластере одинакова. И если к кластеру Cassandra добавить новый реальный сервер, но с увеличенным числом «виртуальных узлов» («коробок»), можно инициировать автоматическое перестроение системы, в ходе которого прочие узлы из списков своих «коробок» просто вычеркнут то, что теперь к ним не относится (а новый узел, само собой, получит все, что было у прочих узлов).

Итак, мы осмотрели почти всё фундаментальное в ключевых идеях Cassandra (и заодно не только её, так что все кажущиеся «лишними» отступления на деле таковыми не являются). Осталось, пожалуй, одно. Репликация. Которая определяется создателем кластера и задаётся значением «коэффициента репликации» (в терминах Cassandra — Replication Factor, но при дословном переводе получается очень уж биологический термин). Своим значением этот коэффициент говорит о том, сколько узлов кластера Cassandra хранят одну и ту же запись базы данных. То есть, раз мы знаем, что записи характеризуются ключами, которые рассчитываются хеш-функциями, и «раскладываются» Partitioner’ом по виртуальным «коробкам», которых в физических серверах может по несколько, мы неявно пришли к выводу, что и репликация в Cassandra — тоже механизм, работающий на том же уровне абстракции «виртуального кольца». Несмотря на такой «виртуализированный» характер, репликация в Cassandra позволяет отображать сегменты виртуального кольца даже на... физические серверы непременно в разных стойках (это может быть целесообразно, например, если к различным стойкам подведено электропитание от разных источников). Вот так, на стыке чистой математической абстракции и жёсткой конкретики реальности обеспечивается надёжная работа итогового решения.

Это, конечно, далеко не всё о Cassandra даже с высоты птичьего полёта. Но пришлось остановиться, потому что статья начала разрастаться до размеров книги, которую никто не будет читать. Тем более, что книг, которых никто не читает, написано очень много.