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

в 5:36, , рубрики: big data, cassandra, CQL, CQL3, nosql, метки: , ,

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

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

  • Статья НЕ предназначена для людей, которые впервые видят слово 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.
Моделирование данных в БД Cassandra 2.0 на CQL3

Выглядит как обычная таблица из реляционной БД. C* создаст две строки.
Моделирование данных в БД Cassandra 2.0 на CQL3
Внимание! Это две внутренние структуры строк, а не таблицы. Если чуть слукавить, то можно сказать, что каждая строка — это как маленькая таблица. Далее понятней.

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

Усложняем. Добавим название компании.

CREATE TABLE employees (
  company text,
  name text,
  age int,
  role text,
  PRIMARY KEY (company,name)
);
INSERT INTO employees (company, name, age, role) VALUES ('OSC', 'john', 37, 'dev');
INSERT INTO employees (company, name, age, role) VALUES ('OSC', 'eric', 38, 'ceo');
INSERT INTO employees (company, name, age, role) VALUES ('RKG', 'anya', 38, 'lead');
INSERT INTO employees (company, name, age, role) VALUES ('RKG', 'ben', 38, 'dev');
INSERT INTO employees (company, name, age, role) VALUES ('RKG', 'chan', 38, 'ops');

Прочитаем данные:

SELECT * FROM employees;

Моделирование данных в БД Cassandra 2.0 на CQL3
Внимание на PRIMARY KEY. Первый из параметров — company — это главный ключ, именно он будет использоваться для поиска ноды с этих пор. Второй ключ — name — превращается в колонку. Т.е. мы данные превращаем в название колонки. Был 'eric' обычными четырмя байтами, а стал частью названия колонки.

Вот так теперь выглядит внутреняя структура.
Моделирование данных в БД Cassandra 2.0 на CQL3
Как видите у нас:

  • Две компании — 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)
);
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;

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

Теперь наш главный ключ составной — (A,B).

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

  • Как видите, теперь каждая уникальная комбинация 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)
)
WITH CLUSTERING ORDER BY event_time DESC; -- reverse sort the temperature values

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

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

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

Создадим вторую таблицу, где будем хранить те же самый данные, но без учета дней.

CREATE TABLE temperature_events (
  sensor_id uuid,
  event_time timestamp,
  temperature double,
  PRIMARY KEY (sensor_id, event_time)
)
WITH CLUSTERING ORDER BY event_time DESC;

И когда будем вставляеть данные, то ограничим время жизни каждой ячейки значением 248 дней чтобы не привысить 2 млрд колонок. 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

P.S. Первая статья на хабре. Прошу указать на мои недочеты. Спасибо.

Автор: koresar

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js