Статья предназначена для людей пытающихся создать свою первую «таблицу» в БД 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.
Выглядит как обычная таблица из реляционной БД. C* создаст две строки.
Внимание! Это две внутренние структуры строк, а не таблицы. Если чуть слукавить, то можно сказать, что каждая строка — это как маленькая таблица. Далее понятней.
Второй пример.
Усложняем. Добавим название компании.
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;
Внимание на PRIMARY KEY. Первый из параметров — company
— это главный ключ, именно он будет использоваться для поиска ноды с этих пор. Второй ключ — name
— превращается в колонку. Т.е. мы данные превращаем в название колонки. Был '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)
);
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, 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 задачи:
- Продолжать записывать, если сервер БД (нода) остановит свою работу.
- Успевать записывать 1000 новых записей в секуду несмотря ни на что.
- Предоставлять график любого датчика за любой день за пару-тройку миллисекунд.
- Предоставлять график любого датчика за любой промежуток времени как можно быстрее.
Первый и второй пункты — легко.
Нам нужно установить несколько нод, сделать каждую автономной. Может даже вынести одну из них в облако.
Третий пункт — основная хитрость.
Мы будем хранить данные одного дня в одной строке.
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 БД.
Источники
Рекомендую к просмотру именно в этом порядке.
- Вебинар — Understanding How CQL3 Maps to Cassandra's Internal Data Structure.
- Вебинар — The Data Model is Dead, Long Live the Data Model
- Вебинар — Become a Super Modeler
- Вебинар — The World's Next Top Data Model
- Полная документация по CQL3 — Cassandra Query Language (CQL) v3.1.1
P.S. Первая статья на хабре. Прошу указать на мои недочеты. Спасибо.
Автор: koresar