Pull to refresh

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

NoSQLBig Data
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


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


UPD: Исправление терминологии. Заменил слова «главный ключ» на «распределительный ключ» в нужных местах. Добавил кое-где понятие «кластерный ключ».
Tags:CQLCQL3cassandra
Hubs: NoSQL Big Data
Total votes 20: ↑19 and ↓1+18
Views88K

Top of the last 24 hours