24 ноября 2013 в 09:36

Моделирование данных в Cassandra 2.0 на CQL3 tutorial

Статья предназначена для людей пытающихся создать свою первую «таблицу» в БД Cassandra.

За посление несколько релизов Кассандры разработчики взяли правильный вектор направленный на простоту использования этой базы данных. Учитывая её достоинства, такие как скорость работы и отказоустойчиваость, её было сложно как администрировать, так и писать под неё. Сейчас же количество танцев с бубном, которые надо провести прежде чем запустить и начать разрабатывать, свели к минимуму — несколько комманд в bash или один .msi в Windows.
Более того, сильно облегчил жизнь разработчикам недавно обновлённый CQL (язык запросов), вытеснив бинарный и довольно сложный язык Thrift.
Лично я столкнулся с проблемой наличия отсуствия русскоязычных руководств по Кассандре. Самую, на мой взгляд, сложную тему мне бы хотелось поднять в этой статье. Как же дизайнить базу данных то?


Disclaimer

  • Статья НЕ предназначена для людей, которые впервые видят слово Cassandra.
  • Статья НЕ служит как рекламный материал той или иной технологии.
  • Статья НЕ стремится доказать что-либо кому-либо.
  • Если скорость записи/чтения не так важна, и если «100% uptime» не сильно нужен, и если у вас всего лишь несколько миллионов записей, то, вероятно, эта статья, да и вся Cassandra в целом, — не то, что вам нужно.


Ликбез


  • Cassandra (далее C*) — распределённая NoSQL БД, поэтому все решения «почему так, а не вот так» всегда принимаются с оглядкой на кластеризацию.
  • CQL — это SQL-подобный язык. Аббревиатура от Cassandra Query Language.
  • Node (нода) — инстанс C*, или java процесс в терминах операционных систем. На одной машине можно запустить несколько нод, например.
  • Основная единица хранения — строка. Строка целиком хранится на нодах, т.е. нет ситуаций когда полстроки — на одной ноде, полстроки — на другой. Строка может динамически раширяться до 2 миллиардов колонок. Это важно.
  • cqlsh — коммандная строка для CQL. Все примеры ниже выполняются именно в ней. Является частью дистрибутива C*.


Основное правило моделирования данных в C*


Кассандра создавалась как распределённая БД с упором на максимальную скорость записи и чтения. Моделировать «таблицы» нужно в зависимости от SELECT запросов вашего приложения.
В SQL мы привыкли накидать таблиц, связей между ними, и потом уже SELECT ... JOIN ... чего хотим и как хотим. Именно JOIN-ы основная проблема с произвоидтельностью в RDBMS. Их нет в CQL.

Первый пример.


У нас есть сотрудники какой-то компании. Создадим таблицу (которые на самом деле называются Column Family, но для простоты перехода с SQL на CQL используют слово table) на CQL и заполним данными:
CREATE TABLE employees (
    name text, -- уникальное имя
    age int, -- какие-то данные про человека
    role text, -- ещё какие-то данные
    PRIMARY KEY (name)); -- обязательная часть любой таблицы
INSERT INTO employees (name, age, role) VALUES ('john', 37, 'dev');
INSERT INTO employees (name, age, role) VALUES ('eric', 38, 'ceo');

Таблицы в C* обязаны иметь PRIMARY KEY. Он используется для поиска ноды, в которой хранится искомая строка.

Прочитаем данные:
SELECT * FROM employees;

Эта картинка — руками разукрашенный вывод cqlsh.


Выглядит как обычная таблица из реляционной БД. C* создаст две строки.

Внимание! Это две внутренние структуры строк, а не таблицы. Если чуть слукавить, то можно сказать, что каждая строка — это как маленькая таблица. Далее понятней.

Второй пример.


Усложняем. Добавим название компании.
CREATE TABLE employees (
  company text,
  name text,
  age int,
  role text,
  PRIMARY KEY (company,name) -- две части главного ключа: распределительный ключ company и кластерный ключ name
);
INSERT INTO employees (company, name, age, role) VALUES ('OSC', 'eric', 38, 'ceo');
INSERT INTO employees (company, name, age, role) VALUES ('OSC', 'john', 37, 'dev');
INSERT INTO employees (company, name, age, role) VALUES ('RKG', 'anya', 29, 'lead');
INSERT INTO employees (company, name, age, role) VALUES ('RKG', 'ben', 27, 'dev');
INSERT INTO employees (company, name, age, role) VALUES ('RKG', 'chan', 35, 'ops');

Прочитаем данные:
SELECT * FROM employees;


Внимание на PRIMARY KEY. Первый из параметров — company — это распределительный ключ, именно он будет использоваться для поиска ноды с этих пор. Второй ключ name — это кластерный ключ (clustering key). Он превращается в колонку. Т.е. мы данные превращаем в название колонки. Был 'eric' обычными четырмя байтами, а стал частью названия колонки.

Вот так теперь выглядит внутреняя структура.

Как видите у нас:
  • Две компании — OSC и RKG. Здесь создалось всего две строки.
  • Зелёный eric хранит свой возраст и роль в двух ячейках. Аналогично все остальные.
  • Получается с такой структурой мы можем хранить 1 млрд сотрудников в каждой компании (строке). Помним же, что лимит количества колонок — 2 млрд?
  • Может показаться, что мы лишний раз храним одни и те же данные. Это так, но в C* такой дизайн — правильный паттерн моделирования.
  • Расширять строки — это основная фича при моделировании в С*.


Третий пример.


Ещё сложнее. Заглавная буква — название колонки. Строчная — данные.
CREATE TABLE example (
  A text,
  B text,
  C text,
  D text,
  E text,
  F text,
  PRIMARY KEY ((A,B), C, D)); -- составной распределительный ключ (A,B) и кластерные ключи (C,D)
INSERT INTO example (A, B, C, D, E, F) VALUES ('a', 'b', 'c', 'd', 'e', 'f');
INSERT INTO example (A, B, C, D, E, F) VALUES ('a', 'b', 'c', 'g', 'h', 'i');
INSERT INTO example (A, B, C, D, E, F) VALUES ('a', 'b', 'j', 'k', 'l', 'm');
INSERT INTO example (A, B, C, D, E, F) VALUES ('a', 'n', 'o', 'p', 'q', 'r');
INSERT INTO example (A, B, C, D, E, F) VALUES ('s', 't', 'u', 'v', 'w', 'x');

Прочитаем данные:
SELECT * FROM example;



Теперь наш распределительный ключ составной — (A,B). Кластерный ключ тоже составной — C, D.

Внутрення структура усложнилась. Такие данные как c, d, g, k, o, p, u, v участвуют в названии колонок наравне с E и F:


  • Как видите, теперь каждая уникальная комбинация A и B — это ключ к строке.
  • У нас всего три уникальных распределительных ключа — a:b, a:n и s:t.
  • Колонки же размножились благодаря кластерным ключам. В строке a:b у нас три уникальных комбинации — c:d, c:g, j:k — которые хранят в колонках E и F собственно данные — e и f, h и i, l и m.
  • Аналогично две другие строки.


Почему так сложно?


Это самый быстрый способ записи и хранения бесконечного количества данных в распределённой БД. C* как раз была разработана с упором на скорость записи/чтения. Вот, например, сравнение скоростей MongoDB, HBase и С*.

Пример из реальной жизни


У нас есть некие события, которые происходят 1000 раз в секунду. Например с датчиков уровня шума снимаются показатели. 10 датчиков. Каждый из них присылает данные 100 раз в секунду. У нас 3 задачи:
  1. Продолжать записывать, если сервер БД (нода) остановит свою работу.
  2. Успевать записывать 1000 новых записей в секуду несмотря ни на что.
  3. Предоставлять график любого датчика за любой день за пару-тройку миллисекунд.
  4. Предоставлять график любого датчика за любой промежуток времени как можно быстрее.


Первый и второй пункты — легко.

Нам нужно установить несколько нод, сделать каждую автономной. Может даже вынести одну из них в облако.

Третий пункт — основная хитрость.

Мы будем хранить данные одного дня в одной строке.
CREATE TABLE temperature_events_by_day (
  day text, -- Text of the following format: 'YYYY-MM-DD'
  sensor_id uuid,
  event_time timestamp,
  temperature double,
  PRIMARY KEY ((day,sensor_id), event_time) -- составной распред. ключ (day,sensor_id) и кластерный ключ (event_time)
)
WITH CLUSTERING ORDER BY event_time DESC; -- обратная сортировка записываемых данных

Так как распределительным ключом является уникальная комбинация день+датчик, то данные за один день будут храниться для каждого датчика в отдельной строке. Благодаря обратной сортировке внутри строки мы получаем самые важные для нас данные (последние) «на кончике пальцев».
Так как поиск распределительного ключа (дня) — очень быстрая операция в С*, то третий пункт можно считать выполненным.

Четвертый пункт

Конечно, мы можем сделать поиск дня/дней, а внутри дня уже сравнивать timestamp. Но дней может быть очень много.
У нас ведь всего 10 датчиков. Нельзя ли этим воспользоваться? Можно, если представить, что один датчик — одна строка. В этом случае С* закеширует в памяти местоположение всех десяти строк на диске.

Создадим вторую таблицу, где будем хранить те же самые данные, но без учета дней.
CREATE TABLE temperature_events (
  sensor_id uuid,
  event_time timestamp,
  temperature double,
  PRIMARY KEY (sensor_id, event_time) -- распределительный ключ (sensor_id) и кластерный ключ (event_time)
)
WITH CLUSTERING ORDER BY event_time DESC; -- обратная сортировка записываемых данных


И когда будем вставляеть данные, то ограничим время жизни каждой ячейки чтобы не привысить 2 млрд колонок. У нас каждый датчик даёт не более 100 показаний в секунду. Отсюда:
2**31 / (24 часа * 60 мин * 60 сек * 100 событий/сек) = 2147483648 / (24 * 60 * 60 * 100) = 248.55 дней
Надо сделать, чтобы через 248 дней самые старые данные тихо и незаметно самоудалялись.
INSERT INTO temperature_events (sensor_id, event_time, temperature) 
VALUES ('12341234-1234-1234-123412', 2535726623061, 36.6)
TTL 21427200; -- 248 days in seconds


В коде приложения нужно будет поставить условие, что если запрашиваемые данные выходят за границы последних 248 дней, то используем таблицу temperature_events_by_day, если нет — temperature_events. Поиск по последней будет на несколько миллисекунд быстрее.

«Что за бред! Зачем вторую таблицу?» — подумаете вы. Повторюсь: в БД С* хранить одно и то же значение по нескольку раз — это норма, правильная модель. Выигрыши следующие:
  • Запись данных во вторую таблицу быстрее, чем в первую. Кассандре не придётся искать ноду(-ы) в которую бы сложить новое значение. Она будет знать заранее.
  • Чтение данных тоже очень быстрое. Например в разы превосходит обычную индексированную, нормированную SQL БД.


Источники


Рекомендую к просмотру именно в этом порядке.
  1. Вебинар — Understanding How CQL3 Maps to Cassandra's Internal Data Structure.
  2. Вебинар — The Data Model is Dead, Long Live the Data Model
  3. Вебинар — Become a Super Modeler
  4. Вебинар — The World's Next Top Data Model
  5. Полная документация по CQL3 — Cassandra Query Language (CQL) v3.1.1


Следующая статья цикла.


UPD: Исправление терминологии. Заменил слова «главный ключ» на «распределительный ключ» в нужных местах. Добавил кое-где понятие «кластерный ключ».
Василий Боровяк @koresar
карма
28,0
рейтинг 0,0
Похожие публикации
Самое читаемое Разработка

Комментарии (25)

  • +4
    Ещё!
    • 0
      Без проблем. Скажите какая часть Кассандры вас интересует? Хотя оговорюсь, что сам в ней знаю не так уж много.
      • 0
        Да мне, в принципе, все интересно) Рассказывайте о том, что хорошо знаете :)
        • 0
          ok :)
      • 0
        Расскажите, затронули ли изменения в 2.x счётчики? Работал только с 1.2.6, там счётчики периодически факапили.
        С INSERT'ами всё понятно, а что с SELECT'ами, можно ли использовать часть первичного ключа для SELECT? Куда делся или что стало с ordering key, возможно ли теперь задавать составной ordering key?
        • +1
          Счетчики как таковые не изменились, по появилиcь Lightweight transactions и Atomic batches. Не то чтобы это убрало все проблемы счетчиков, но сделало их более надёжным инструментом. Затрону чуток счетчиков в след. статье — №2 — которая уже написана, но ждёт последних правок.

          В следующей после следующей статье (№3 по счету) я подробно рассмотрю SELECT, что можно делать, что нельзя и всё такое. Сейчас как раз пишу. Краткий ответ на ваш вопрос — да, с сильным замедлением производительности, но тогда у меня подозрение, что ваша модель могла бы быть лучше.

          Ordering key никуда не девался, смотрите чуть выше примеры.

          Можно создавать составной ordering key:
          CREATE TABLE test1 (name text, id1 int, id2 int, PRIMARY KEY (name, id1, id2)) 
          WITH CLUSTERING ORDER BY (id1 DESC, id2 DESC);
          
          • 0
            Т.е. SELECT так же как и раньше, когда нужно было указывать ALLOW FILTERING?

            Да, возможно, что и могла бы быть лучше. У меня хранятся полученные СМС сообщения, с timestamp DESC. Скажем, когда мне нужно получить эти сообщения за какой-то период, приходится использовать ALLOW FILTERING. Никакого основного ключа для кластеризации тут выделить не получается. Тут, скорее, нужна очередь, и Cassandra не очень с таким заданием справляется.

            Было бы забавно увидеть что-то типа Cassandra Cookbook, с рецептами что и как лучше делать для типовых задач. Понятно, что для бложика C* подходит слабо.
            • +1
              Похоже я не понял вопроса. Перефразируй. Или просто дождись завтра — я уже написал третью статью про SELECT-ы. Там покрыто всё, что можно было покрыть про слово WHERE.

              Что является распределительным (partition) ключом в твой таблице с СМС? Вообще, показал бы уже CREATE таблицs.
              Ты думал что будет, если наберётся 2 млрд смс? (У средних опсосов набирается за год или меньше.)

              Cookbook для С* может быть и хорошая идея, но такого понятия, как «типовые решения» здесь не cуществует. У всех разные задачи. Даже чуть-чуть отличающаяся задача приводит к совершенно к другим таблицам.

              Кстати, вторая статья уже опубликована. habrahabr.ru/post/204026/
              • 0
                Синтетический там int, пробовал дату (без времени), но только усложняет логику приложения:
                CREATE TABLE messages (
                    constant int,
                    timestamp timestamp,
                    body varchar,
                    sender varchar,
                    recipient varchar,
                    weekday int,
                    PRIMARY KEY (constant, timestamp)
                )
                    WITH CLUSTERING ORDER BY (timestamp ASC);
                


                Вообще расчёт на 100к в секунду в пике, это получается куда больше. Данные эти нужны, но пакетным обработчиком партиями выбираются и кладутся в другое место. То есть тут можно и TTL применить, у меня в INSERT'ах стоит месяц.
                • 0
                  Усложнение приложение — неизбеждая часть, если хочется скорости. Поэтому рекомендую усложнять.

                  Как я писал в статье, вам нужно сначала определиться с теми SELECT-ами, которые планируете делать, и только потом уже моделировать таблицы. В связи с чем вопрос: какие select-ы по этой таблице вам нужно делать?
              • 0
                Ага, понял о чём речь в вопросе про 2 млрд смс. Придётся переделывать схему.
  • 0
    Очень неплохо, спасибо!
  • +1
    бинарный и довольно сложный язык Thrift.


    Thrift скорее rpc протокол, чем язык.
    • +1
      Да, я знаю. Здесь, на самом деле, я довольно много неправильных терминов называл. Это ради простоты. Новичков не хотелось сильно нагружать.
  • 0
    Интересно. Но единственное что не понятно, почему в Касандре пытаются применить ОЧЕНЬ похожий на реляционный TSQL язык CQL.
    У NOSQL свой путь и, имхо, не надо работу с ней делать похожим на всем угодный TSQL ;) Это приводит к тому что многие пытаются думать реляционно в nosql средах.
    • 0
      Урезанный SQL гораздо проще воспринимается чем thift api.
    • 0
      С одной стороны всё верно, люди начинают применять SQL модель к Кассандре.
      Но с другой стороны переход с SQL на CQL сильно упрощается (thrift имеет высокий порог входа).
      Люди продвигающие CQL утверждают, что Кассандра стала более популярной именно благодаря CQL.
  • 0
    Уххх ты как интересно! Мне как sql-щику интересно исключительно. Запросы, запросы покажите. Пример с датчиками прекрасен, но он только один. Как я понял, джойнов не бывает (в этом вроде бы и смысл) — а что бывает? Ограничения? Сортировки? Группировки?

    Не знаю, получилось у меня внятно высказаться или нет. На всякий случай — у меня и в мыслях нет доказывать, что «Cassandra — это неправильный дизайн данных». Я убеждён, что это *правильный* дизайн, но мне совершенно не знакомый, и хочу узнать больше про эту часть реальности.

    В общем, спасибо за статью, пишите ещё. Пошёл смотреть вебинары :).
    • 0
      После просмотра вебинаров вопросы отпадут сами собой. :) Там куча примеров.
      Но следующая статья уже запланирована. Надеюсь родить через недельку.
  • 0
    А порядок в составном ключе и кластерном влияет на алгоритм распределения?
    Т.е если ( (A,B), C, D) и ( (B, A), D, C) с точки зрения запроса и их порядков и возможностей понятно, а вот будет ли идентичен алгоритм заполнения?
    • 0
      Порядок очень влияет. Грубо говоря, дерево объектов будет иметь вершину А в первом случае, и B во втором. Гляньте третью статью цикла. Там будет понятно почему так.
  • 0
    Если у нас к примеру 5-6 распределительных ключей и 5-6 класстерных, и еще 4-5 обычных колонок.
    Сильно ли пострадает производительность записи и чтения? На сколько процентов примерно
    • 0
      :) Не знаю. Я пока не гуру. Перфоманс таких таблиц не замерял. :)
  • 0
    Атомарность и изоляция отсутствуют как и в большинстве NoSQL?
    • 0
      Изоляция присутствует на уровне строки, но не БД в целом. Она же распределённая. :)
      Атомарность есть — Lightweight Transactions — это микротранзакции.
      Учтите, что бонус С* не в автомарности, а в распределённости. Для больших проектов на сотни нод С* — идеальное решение.

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