«Кошелек или деньги: сложный выбор между памятью и процессором» Алексеенко Иг...
Aleksey Yeschenko "Моделирование данных с помощью CQL3". Выступление на Cassandra Conf 2013
1. Моделирование данных с CQL3:
типичные паттерны и анти-паттерны
Aleksey Yeschenko
Apache Cassandra Committer, Engineer @DataStax
@AYeschenko
2. Кто я такой
• Backend Ruby web-dev (давно и неправда)
• Telecom Erlang dev (недавно и правда)
• Принуждают использовать Java+Python (cqlsh) в DataStax с июля
2012
• Apache Cassandra committer с ноября 2012
• @AYeschenko
3. Когда использовать C*
• Необходима Active-Active репликация между несколькими датацентрами
• Необходим (около) 100% аптайм
• Данные больше не умещаются в PostgreSQL/MySQL/etc.
• PostgreSQL/MySQL/etc. больше не справляются с объёмом записи
• Модель Cassandra изначально идеально подходит для
приложения
• Нет необходимости в ad-hoc запросах
• Любое другое решение стоит $$$$$
* 1 и более пунктов из списка == true
7. Как хранятся данные + терминология
• Partition ключ и множество ячеек (до 2 миллиардов)
• Хэш partition ключа определяет ноды, к которым принадлежит
partition
• Быстрое чтение по partition ключу
• На диске и в памяти, ячейки всегда отсортированы по имени
• Компаратор для сортировки задаётся пользователем
1
Имя ячейки
2 Миллиарда
...
Имя ячейки
Значение ячейки
Значение ячейки
Timestamp
Timestamp
TTL
TTL
Partition ключ
8. Идеальная нагрузка
• Запись доминирует
• Перезапись/удаление ячеек отсутствуют или не очень часты
• Чтение по ключу + (имени ячейки или небольшим отрезкам
ячеек в порядке компаратора)
9. Идеальная нагрузка
• Логи
• Данные с сенсоров
• Финансовые данные (ticks)
• Ads - клики и отображения, аналитика
• Необязательно именно временные ряды (в порядке timestamp);
любые данные, естественно упорядоченные по
последовательному значению
• Нужны последние N значений
11. Паттерн #1: временной ряд
CREATE TABLE temperature (
weatherstation_id text,
partition ключ
event_time timestamp,
кластеринговая колонка
temp int,
PRIMARY KEY (weatherstation_id, event_time)
) WITH CLUSTERING ORDER BY (event_time DESC);
составной ключ
обратный порядок
сортировки ячеек
• Каждая станция регулярно собирает показания
• Один partition == одна станция
• Одно показание == одна ячейка (здесь две, технически)
• Потенциально очень широкий partition
12. Паттерн #1: временной ряд
CREATE TABLE temperature (
weatherstation_id text,
partition ключ
event_time timestamp,
кластеринговая колонка
temp int,
PRIMARY KEY (weatherstation_id, event_time)
составной ключ
обратный порядок
сортировки ячеек
) WITH CLUSTERING ORDER BY (event_time DESC);
SVO
weatherstation_id
1386475781 1386475781:temp 1386475780 1386475780:temp 1386475779 1386475779:temp …
*
-6
*
event_time
-6
*
temp
-5
…
* маркеры CQL3 ряда
13. Паттерн #1: временной ряд
CREATE TABLE temperature (
weatherstation_id text,
date text,
составной partition ключ
event_time timestamp,
temp int,
PRIMARY KEY ((weatherstation_id, date), event_time)
) WITH COMPACT STORAGE AND CLUSTERING ORDER BY (event_time DESC);
• Один partition для пары {weatherstation_id, date}
• Отсутствие оверхеда маркера ряда и имени колонки
14. Паттерн #1: временной ряд
CREATE TABLE temperature (
weatherstation_id text,
date text,
составной partition ключ
event_time timestamp,
temp int,
PRIMARY KEY ((weatherstation_id, date), event_time)
) WITH COMPACT STORAGE AND CLUSTERING ORDER BY (event_time DESC);
SVO:2013-12-09
1386475781
1386475780
1386475779
…
-6
-6
-5
…
date
weatherstation_id
timestamp
temp
15. Анти-паттерн #1: неуместное использование
встроенных индексов
• RDBMS опыт с индексацией НЕ переносится в C*
16. Анти-паттерн #1: неуместное использование
встроенных индексов
• RDBMS опыт с индексацией НЕ переносится в C*
• Встроенные индексы в C* != RDBMS индексы
17. Анти-паттерн #1: неуместное использование
встроенных индексов
• RDBMS опыт с индексацией НЕ переносится в C*
• Встроенные индексы в C* != RDBMS индексы
• Встроенные индексы в C* != RDBMS индексы
18. Анти-паттерн #1: неуместное использование
встроенных индексов
• RDBMS опыт с индексацией НЕ переносится в C*
• Встроенные индексы в C* != RDBMS индексы
• Встроенные индексы в C* != RDBMS индексы
• Встроенные индексы в C* != RDBMS индексы
19. Анти-паттерн #1: неуместное использование
встроенных индексов
• RDBMS опыт с индексацией НЕ переносится в C*
• Встроенные индексы в C* != RDBMS индексы
• Встроенные индексы в C* != RDBMS индексы
• Встроенные индексы в C* != RDBMS индексы
• Для удобства, не для улучшения производительности
• Достаточно ограничены в плане удобства
• Следует избегать, по большей части
20. Анти-паттерн #1: неуместное использование
встроенных индексов
cqlsh:nope> CREATE TABLE users (
... username varchar,
... email varchar,
... fullname varchar,
... last_login timestamp,
... PRIMARY KEY (username)
... );
!
cqlsh:nope> INSERT INTO users (username, email, fullname) VALUES ('ay',
'aleksey@datastax.com', 'Aleksey Yeschenko');
!
cqlsh:nope> SELECT * FROM users WHERE email = 'aleksey@datastax.com';
Bad Request: No indexed columns present in by-columns clause with Equal operator
22. Анти-паттерн #1: неуместное использование
встроенных индексов
cqlsh:nope> CREATE INDEX ON users (email);
!
cqlsh:nope> SELECT * FROM users WHERE email = 'aleksey@datastax.com';
username | email
| fullname
| last_login
----------+----------------------+-------------------+------------
ay | aleksey@datastax.com | Aleksey Yeschenko |
!
(1 rows)
null
23. Анти-паттерн #1: неуместное использование
встроенных индексов
• При высокой и средней кардинальности колонки решается
‘ручным’ вторичным индексом
• Отдельная таблица для обратного лукапа
25. Анти-паттерн #1: неуместное использование
встроенных индексов
cqlsh:yep> BEGIN BATCH
... INSERT INTO users (username, email, fullname) VALUES ('ay', 'aleksey@datastax.com', 'Aleksey
Yeschenko');
... INSERT INTO users_by_email_idx (email, username) VALUES ('aleksey@datastax.com', 'ay');
... APPLY BATCH;
!
cqlsh:yep> SELECT username FROM users_by_email_idx WHERE email = 'aleksey@datastax.com';
username
----------
ay
!
cqlsh:yep> SELECT * FROM users WHERE username = 'ay';
username | email
| fullname
| last_login
----------+----------------------+-------------------+------------
ay | aleksey@datastax.com | Aleksey Yeschenko |
null
26. Анти-паттерн #1: неуместное использование
встроенных индексов
• При средней/низкой кардинальности свой, отдельный набор
проблем
• Главная - большое количество random i/o
• Решается правильной моделью/денормализацией вместо
использования встроенных индексов
27. Анти-паттерн #1: неуместное использование
встроенных индексов
• Q: Когда вообще следует использовать встроенные индексы?
• A: Для поиска рядов с 1+ кластеринговыми колонками внутри
определённого partition (зная partition ключ)
28. Анти-паттерн #1: неуместное использование
встроенных индексов
CREATE TABLE example (
partition_key varchar,
clustering_col0 varchar,
clustering_col1 varchar,
val varchar
);
!
CREATE INDEX ON example (val);
!
SELECT * FROM example WHERE partition_key = ‘some_pk’ AND val = ‘some_val’;
29. Анти-паттерн #1: неуместное использование
встроенных индексов
• Всё вышесказанное распространяется на индексы коллекций в
2.1
• Обращайтесь со встроенными индексами как с expert-level
feature
30. Паттерн #2: денормализация
• Отношения без реляционности
• Без использования foreign key
CREATE TABLE users (!
username varchar,!
firstname varchar,!
lastname varchar,!
email varchar, !
PRIMARY KEY(username)!
);
• Users 1-M Videos
Users
username firstname
lastname email
tcodd
rboyce
Edgar
Raymond
Codd
Boyce
videoid
99051fe9
videoname
My funny cat
b3a76c6b Math
Videos
tcodd@relational.com
rboyce@relational.com
username description
tcodd
My cat plays
the pianodog
Now my
tcodd
plays
tags
cats,piano,lol
dogs,piano,lol
CREATE TABLE videos (!
videoid uuid,!
videoname varchar,!
username varchar,!
description varchar, !
tags varchar,!
upload_date timestamp,!
PRIMARY KEY(videoid)!
);
31. Паттерн #2: денормализация
• Лукап видео по username
• Запись в две таблицы сразу
• Без встроенных индексов
CREATE TABLE username_video_index (!
username varchar,!
videoid uuid,!
upload_date timestamp,!
video_name varchar,!
PRIMARY KEY (username, videoid)!
);
SELECT video_name!
FROM username_video_index!
WHERE username = ‘ctodd’!
AND videoid = ‘99051fe9’
32. Паттерн #2: денормализация
• Users 1-M Comments
• Videos 1-M Comments
• Без встроенных индексов
Users
username firstname
lastname email
tcodd
rboyce
Codd
Boyce
videoid
99051fe9
Edgar
Raymond
videoname
My funny cat
b3a76c6b Math
Videos
tcodd@relational.com
rboyce@relational.com
username description
tcodd
My cat plays
the pianodog
Now my
tcodd
plays
tags
cats,piano,lol
dogs,piano,lol
Comments
username
tcodd
videoid
99051fe9
comment
Sweet!
rboyce
b3a76c6b
Boring :(
33. Паттерн #2: денормализация
• Две таблицы для выборки по видео и по пользователям
• Запись в три таблицы сразу (comments, comments_by_video,
comments_by_user)
• Не жалеть запись
CREATE TABLE comments_by_video (!
videoid uuid,!
username varchar,!
comment_ts timestamp,!
comment varchar,!
PRIMARY KEY (videoid,username)!
);
CREATE TABLE comments_by_user (!
username varchar,!
videoid uuid,!
comment_ts timestamp,!
comment varchar,!
PRIMARY KEY (username,videoid)!
);
35. Анти-паттерн #2: очереди и очередеподобные нагрузки
• Cassandra не лучшее решение для очередей
• Можно, но требует гимнастики
• Лучше использовать что-нибудь вроде Kafka/RabbitMQ
• Любой паттерн, где в partition удаляются ячейки и выполняются
slice сканы, включающие удалённые ячейки
• Пример - нарочно упрощён, ради иллюстрации
36. Анти-паттерн #2: очереди и очередеподобные нагрузки
—— naive путь
CREATE TABLE queues (
name text,
enqueued_at timeuuid,
payload blob,
PRIMARY KEY (name, enqueued_at) —— ячейки упорядочены по времени вставки, ASC
) WITH COMPACT STORAGE;
37. Анти-паттерн #2: очереди и очередеподобные нагрузки
—— 1000x добавить в очередь:
INSERT INTO queues (name, enqueued_at, payload) VALUES (‘queue-1’, now(), 0x…);
!
—— 999x убрать из очереди, индивидуально:
DELETE FROM queues WHERE name = ‘queue-1’ AND enqueued_at = ?;
!
—— достать последний элемент из очереди:
SELECT enqueued_at, payload
FROM queues
WHERE name = 'queue-1'
LIMIT 1;
38. Анти-паттерн #2: очереди и очередеподобные нагрузки
queue-1
51c220e0-60…
58940a00-60..
61163b30-60..
995 x …
X
X
X
X
6e1ac030-60… ab36fa10-60..
X
<some blob>
39. Анти-паттерн #2: очереди и очередеподобные нагрузки
activity
| source
| elapsed
-------------------------------------------+-----------+--------
execute_cql3_query | 127.0.0.3 |
0
Message received from /127.0.0.3 | 127.0.0.1 |
42
Sending message to /127.0.0.1 | 127.0.0.3 |
718
Executing single-partition query on queues | 127.0.0.1 |
145
Acquiring sstable references | 127.0.0.1 |
158
Merging memtable contents | 127.0.0.1 |
189
Merging data from memtables and 0 sstables | 127.0.0.1 |
235
Read 1 live and 9999 tombstoned cells | 127.0.0.1 |
251102
Enqueuing response to /127.0.0.3 | 127.0.0.1 |
252976
Sending message to /127.0.0.3 | 127.0.0.1 |
253052
Message received from /127.0.0.1 | 127.0.0.3 |
324314
Processing response from /127.0.0.1 | 127.0.0.3 |
324535
Request complete | 127.0.0.3 |
324812
40. Анти-паттерн #2: очереди и очередеподобные нагрузки
• partition сканируется до тех пор пока LIMIT неудалённых ячеек
не будет собран, или
• пока не закончились ячейки в partition, или
• пока не встретится ячейка вне заданных границ (если указано в
where)
41. Анти-паттерн #2: очереди и очередеподобные нагрузки
—— зная последний удалённый элемент
SELECT enqueued_at, payload
FROM queues
WHERE name = 'queue-1'
AND enqueued_at > 6e1ac030-60b9-11e3-949a-0800200c9a66
LIMIT 1;
42. Анти-паттерн #2: очереди и очередеподобные нагрузки
queue-1
51c220e0-60…
58940a00-60..
61163b30-60..
995 x …
X
X
X
X
6e1ac030-60… ab36fa10-60..
X
<some blob>
43. Анти-паттерн #2: очереди и очередеподобные нагрузки
activity
| source
| elapsed
-------------------------------------------+-----------+--------
execute_cql3_query | 127.0.0.3 |
0
Sending message to /127.0.0.1 | 127.0.0.3 |
965
Message received from /127.0.0.3 | 127.0.0.1 |
34
Executing single-partition query on queues | 127.0.0.1 |
339
Acquiring sstable references | 127.0.0.1 |
355
Merging memtable contents | 127.0.0.1 |
461
Partition index lookup over for sstable 3 | 127.0.0.1 |
1122
Merging data from memtables and 1 sstables | 127.0.0.1 |
2268
Read 1 live and 0 tombstoned cells | 127.0.0.1 |
4404
Message received from /127.0.0.1 | 127.0.0.3 |
6109
Enqueuing response to /127.0.0.3 | 127.0.0.1 |
4492
Sending message to /127.0.0.3 | 127.0.0.1 |
4606
Processing response from /127.0.0.1 | 127.0.0.3 |
6608
Request complete | 127.0.0.3 |
6901
44. Анти-паттерн #2: очереди и очередеподобные нагрузки
• partitioning по дате
• хинты для чтения
• то же относится к TTL
• худший сценарий - death by OOM
• https://issues.apache.org/jira/browse/CASSANDRA-6117 (Avoid
death-by-tombstone by default) 10k warning, 50k error
• http://www.datastax.com/dev/blog/cassandra-anti-patternsqueues-and-queue-like-datasets
45. Что дальше - вебинары по моделированию
• http://youtu.be/px6U2n74q3g - The Data Model is Dead, Long Live
the Data Model
• http://youtu.be/qphhxujn5Es - Become a Super Modeler
• http://youtu.be/T_WRC_GjRd0 - The World's Next Top Data Model
• http://youtu.be/UP74jC1kM3w - Understanding How CQL3 Maps to
Cassandra's Internal Data Structure