Как известно, уже больше года DataStax всячески пропагандирует свой новый бинарный протокол для работы с Cassandra и язык для работы с ним – CQL 3.x. Разработчикам детально разъясняют различия со старым “дедовским” способом работы через Thrift и просят не переживать по поводу обратной совместимости. На самом деле все обстоит несколько не так радужно, потому что направление на CQL 3 частью политики имеет не развивать новые возможности в Thrift. Впрочем, статья не совсем об этом.
Мы рассматриваем переход на CQL 3 для новых сервисов и нас больше всего обеспокоили принципы формирования составного ключа для колонок в динамических таблицах (раньше они назывались column family). Дело в том, что часть составного первичного ключа будет присутствовать во всех колонках для конкретной записи. Мало того, дополнительно будет вставлена пустая колонка-закладка с этой частью первичного ключа. Выглядит это примерно вот так:
Нигде об этом явно не говорится с негативным оттенком, но за подобную структуру придется “заплатить” местом на диске. Например, использование композитного типа для имени колонки дает дополнительных 2 байта на каждую колонку, а повторение части первичного ключа дает дополнительных “размер части ключа” x (“количество колонок в одной записи” + 1) байт. А вот это уже может быть очень существенным для некоторых случаев.
Мы подготовили тестовые данные и решили рассмотреть 5 различных форматов хранения. В качестве доменной модели мы будем использовать “родную” для нас область SEO и ранжирования доменов по различным кейвордам. Каждый домен и кейворд имеют свои идентификатор, отдельно будет поле data, с которым можно будет экспериментировать в вариациях разного размера полезных данных. Остальные поля должны быть достаточно понятными.
Итак, первый вариант выглядит наиболее “натурально” в рамках CQL 3 и это первое что приходит в голову:
CREATE TABLE natural (
domain_id bigint,
keyword_id bigint,
ranking_date int,
rank int,
data blob,
PRIMARY KEY ((domain_id), keyword_id, ranking_date));
Второй вариант призван проверить зависимость размера данных от имен колонок:
CREATE TABLE short_names (
d bigint,
k bigint,
rd int,
r int,
da blob,
PRIMARY KEY ((d), k, rd));
Третий вариант пытается сэкономить на количестве колонок и объединяет несколько полей в одно, а именно rank и data:
CREATE TABLE single_column (
domain_id bigint,
keyword_id bigint,
ranking_date int,
ranking_data blob,
PRIMARY KEY ((domain_id), keyword_id, ranking_date));
Четвертый вариант идет дальше и делает из двух частей первичного ключа одну:
CREATE TABLE everything_is_blob (
domain_id bigint,
ranking_id blob,
ranking_data blob,
PRIMARY KEY ((domain_id), ranking_id));
Пятый и последний вариант пытается воспользоваться “компактным” хранением данных, задуманном для поддержки таблиц, созданных с помощью Thrift:
CREATE TABLE old_storage_emulation (
domain_id bigint,
ranking_id blob,
ranking_data blob,
PRIMARY KEY ((domain_id), ranking_id)) WITH COMPACT STORAGE;
Код для загрузки данных оставим для домашнего задания читателям (осторожнее будьте с использованием класса ByteBuffer в Java, потому что он содержит массу сюрпризов для новичков) и перейдем непосредственно к результатам (перед замерами не забудьте вызвать nodetool flush, а возможно и сделайте полный compaction).
При загрузке 1 млн. записей по 100 на один домен с использованием для поля data битовой маски из одного байта результаты выглядят так:
natural - 73 MB
short_names - 73 MB
single_column - 64 MB
everything_is_blob - 62 MB
old_storage_emulation - 47 MB
Разницы между natural и short_names нет и это сильно радует (было бы совсем печально иначе). Разница между single_column и everything_is_blob незначительная, что говорит о небольших потерях на хранение частей первичного ключа. Разница между худшим и лучшим вариантом составляет 26 MB (около 35% от худшего результата).
А это говорит сразу о многом. Во-первых, если данные достаточно небольшие и их очень много, то новые механизмы хранения могут сильно ударить по объему диска, что незамедлительно ударит по стоимость решения (особенно в облаке), скорости работы с данными (чтение, поиск, сжатие и т.д.). Во-вторых, при росте размера первичного ключа все еще больше усугубится, а это значит, что не стоит использовать большие значения в качестве составляющих первичного ключа. В-третьих, схема подобного дублирования и “закладки” выглядит действительно не так здорово и отличия от старой схемы хранения существенны.
При загрузке 1 млн. записей по 100 на один домен с использованием для поля data реальных существующих URL различных сайтов результаты выглядят так:
natural - 114 MB
short_names - 114 MB
single_column - 102 MB
everything_is_blob - 99 MB
old_storage_emulation - 84 MB
А это значит, что между худшим и лучшим вариантом разница составляет 30 MB (около 25% от худшего результата). И это при использовании достаточно больших значений в поле data, размер которых превышает в несколько раз размер чистого первичного ключа.
Детальнее о данной тебе планирую рассказать на одной из будущих конференций. Поэтому наберитесь терпения либо повторите опыты в домашних условиях. 🙂
Не хочешь пропускать ничего интересного? Подпишись на ленту RSS или следи за нами в Twitter!