Так ли хорош формат хранения данных CQL 3 в Cassandra?

Как известно, уже больше года DataStax всячески пропагандирует свой новый бинарный протокол для работы с Cassandra и язык для работы с ним – CQL 3.x. Разработчикам детально разъясняют различия со старым “дедовским” способом работы через Thrift и просят не переживать по поводу обратной совместимости. На самом деле все обстоит несколько не так радужно, потому что направление на CQL 3 частью политики имеет не развивать новые возможности в Thrift. Впрочем, статья не совсем об этом.

Мы рассматриваем переход на CQL 3 для новых сервисов и нас больше всего обеспокоили принципы формирования составного ключа для колонок в динамических таблицах (раньше они назывались column family). Дело в том, что часть составного первичного ключа будет присутствовать во всех колонках для конкретной записи. Мало того, дополнительно будет вставлена пустая колонка-закладка с этой частью первичного ключа. Выглядит это примерно вот так:

row view in Cassandra

Нигде об этом явно не говорится с негативным оттенком, но за подобную структуру придется “заплатить” местом на диске. Например, использование композитного типа для имени колонки дает дополнительных 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!

Обсуждение (2)

First of all, compression is also enabled for compact storage. 🙂

When you are working in cloud environment with Cassandra cluster of many TB then 10-20% reduce in costs is significant…

Nice experiment )

From the mentioned DataStax blog link we have explicit explanation of the storage vs. flexibility trade off of CQL3 imlp:
“”Please do note that while non-compact table are less “compact” internally, we highly suggest using them for new developments. The extra possibility of being able to evolve your table with the use of collections later (even if you don’t think you will need it at first) is worth the slight storage overhead — overhead that is further diminished by sstable compression, which is enabled by default since Cassandra 1.1.0. “”

Compact storage stores an entire row in a single column on disk instead of storing each non-primary key column in a column that corresponds to one column on disk. Using compact storage prevents you from adding new columns that are not part of the PRIMARY KEY.

Additionall to this, writes on compressed tables can show up to a 10 percent read performance improvement.

And don’t forget: the driving force of CQL3 movement is not technical: the relative low level Thrift can lead to more expensive development costs comparing to well known SQL-like CQL API (I assume Thrift will go CORBA way), potentially attracting entry level programmers to the technology. And storage costs play in most BigData Hadoop Ecosystem solutions (where Cassandra logically belongs to) not the crucial role unless you blame replication factor existence as the violation DRY 😉

Leave a Reply

Your email address will not be published. Required fields are marked *