Всем привет! Я Илья Глазунов, системный аналитик в проекте карточного хранилища T-Pay Online — быстрого способа оплаты для наших клиентов. В качестве БД в проекте хранилища мы используем Apache Cassandra. В статье — обзорный материал, который поможет познакомиться с БД.

В культуре Т-Банка важно, чтобы системные аналитики знали особенности интеграционных схем с другими компонентами, умели строить схемы БД так, чтобы минимизировать издержки, связанные с i/o-операциями. А еще — чтобы могли вычислять узкие места в кейсах и влиять на выбор верхнеуровневого компонента в проекте. 

Когда мы выбирали БД для нашего проекта, столкнулись с тем, что существует мало материалов, которые бы сжато передавали главные особенности Apache Cassandra без глобальных неточностей и на русском языке для первичного ознакомления. Поэтому решили помочь тем, кто окажется в таком же положении, что и мы. 

Разберем, для чего нужна Cassandra, рассмотрим основные концепции архитектуры, разложим принципы проектирования БД с ее особенностями модели данных и затронем некоторые принципы расчета конфигурирования БД Cassandra.

Что такое Apache Cassandra

Начнем с одного из главных заблуждений, которое часто мне попадалось на просторах интернета: «Apache Cassandra относится к колоночной базе данных». Это не совсем так, а если быть точным — совсем не так. 

Cassandra — частный случай строкового хранилища, в котором данные хранятся в разреженных многомерных хэш-таблицах. Данные внутри Cassandra не организованы в виде столбцов.

Отчасти путаница связана с понятиями колоночного семейства и колонки. Эти термины описывают контейнеры коллекции строк, которые сгруппированы по ключу, но не являются столбцами в полном понимании этого слова. Например, в ClickHouse или GreenPlum. 

В отличие от реляционных БД, в Cassandra не происходит резервирования дискового пространства под пустые «колонки». Чаще всего в реляционных БД под конкретную колонку или строку резервируется блок на жестком диске, даже если значение является пустым. Cassandra так не поступает. Вместо этого столбцы в блочной структуре хранения просто опускаются, не резервируя под пустые значения адреса на жестком диске, что повышает эффективность запросов и использования памяти.

Важно, что Cassandra — не колоночная БД, потому что существует шаблон мышления того, для чего можно использовать ту или иную БД в зависимости от ее типа. И если мы при проектировании нового продукта будем придерживаться логики, что Cassandra — колоночная, то придем к неверному выводу. Мы будем думать, что она нам подойдет для решения таких типовых задач, как, например, аналитика BigData, интернет вещей, рекомендательные системы и так далее. 

Колоночные БД обладают рядом характеристик, которые подходят для перечисленных систем:

  1. Они предоставляют эффективные способы хранения и обработки данных единой природы для разных сущностей (например, временные ряды, измерения таких физических характеристик, как температура и т. д.).

  2. Оптимизированное хранение изолированных колонок позволяет с большей эффективностью строить запросы, когда требуется чтение лишь избранных колонок, экономя время за счет игнорирования ненужных колонок.

  3. Колоночные БД позволяют более гибко определять схемы данных, меняя содержание витрин без значительных перестроений на уровне твердотельных накопителей.

Apache Cassandra предоставляет иные преимущества (и обладает некоторыми недостатками), которые необходимо учитывать при проектировании систем.

Архитектура Apache Cassandra

Cassandra — распределенная, высокодоступная, децентрализованная база данных с реализованными механиками антиэнтропии и настройкой уровня согласованности между инстансами. 

«Распределенная» означает, что весь дизайн Cassandra предполагает использование решения на нескольких серверах от объединенных в локальный кластер до геораспределенных. Конечно, Cassandra можно использовать технически и в одном экземпляре на одной «машине», но все преимущества БД меркнут при использовании в таком сценарии.

Для описания кластеров в Cassandra используется система терминов, привычная многим по Apache Kafka. Для группировки нод, инстансов или узлов внутри кластера используются два уровня: стойка и центр обработки данных.

Стойка — некоторый набор серверов, расположенных физически вместе, обычно на одной серверной стойке. Так сетевые издержки между узлами одной стойки можно считать отсутствующими или минимальными. ЦОД — некоторое объединение стоек, находящихся в одном здании либо соединенных в одну локальную сеть.

Кольцевая схема кластера Apache Cassandra на www.luketillman.com
Кольцевая схема кластера Apache Cassandra на www.luketillman.com

Кластер Cassandra — набор узлов, выстроенных в кольцо. Каждому узлу в таком кольце назначается маркер (token), который представляет собой 64-разрядное число и принимает значение от −263 до 263 −1. Наиболее широкими кольцами по количеству узлов за историю использования Apache Cassandra можно считать примеры, которые говорят о легкости горизонтального масштабирования и при этом о сохранении целостности кластера:

  1. В Apple — там дата-центр на основе Cassandra насчитывает порядка 75 000 нод. 

  2. Google Clouds, который разворачивал для своих VM Cassandra в 330 нод.

  3. Netflix, который демонстрировал бенчмарки Cassandra на 288 нод.

Маркер — инструмент очерчивания зоны ответственности того или иного узла в рамках заданных данных (таблицы): узел отвечает за диапазон значений такой, что m − 1 < i ≤ m, где m — значение маркера текущего, m − 1 — значение маркера предыдущего по кольцу, i — текущее значение. 

При этом один из узлов будет владеть диапазонами, где значение меньше или равно собственному маркеру и где значение больше наибольшего маркера, так кольцо «замыкается». Такое кольцо нужно, чтобы определить, к какому узлу отнести данные по результатам вычисления хэш-функции — разделителя (partitioner). На самом деле кольцевая схема чуть сложнее, но мы не будем слишком углубляться.

В Cassandra реализована функциональность репликации, это дополнительный фактор обеспечения высокодоступного характера нашей БД. Каждый узел из кольца не только отвечает за выделенный ему диапазон значений, но и является репликой данных для иных диапазонов. Распределение реплицированных данных по узлам происходит по аналогии с распределением нереплицированных данных, то есть по кольцу, рассмотренному ранее. 

Отмечу, что если выбрана соответствующая стратегия репликации, то Cassandra способна учитывать и расположение узлов по стойкам относительно друг друга при переносе копий данных. Если один из узлов «прилег», то запросы не останутся без ответа, поможет координатор.

Координатор — узел, выступающий маршрутизатором запроса от клиента, определяется клиентом путем обращения к конкретной ноде. Координатором может выступать любой узел, так как в Apache Cassandra допустимо обращение клиентом к любой из нод. Такая нода направит запрос к одной из реплик, которая владеет данными по диапазону и располагается наиболее оптимально относительно координатора по топологии кластера. 

Работа координатора при запросах, схема выложена на blog.stackademic.com
Работа координатора при запросах, схема выложена на blog.stackademic.com

Где будет располагаться реплика, определяется стратегией репликации. Такие стратегии задаются независимо для каждого пространства ключей. Для версии 5.0.4 существует две стратегии репликации: SimpleStrategy и NetworkTopologyStrategy. 

SimpleStrategy реплицирует данные в рамках одного ЦОДа без учета стоек — иными словами, все кластеры имеют одноуровневую архитектуру. 

NetworkTopologyStrategy копирует данные с учетом как разных ЦОДов, так и стоек на случай выхода из строя по разным причинам: техническим, техногенным и так далее. 

Так как Apache Cassandra позиционируется как распределенная и высокодоступная, разработчики рекомендуют использовать стратегию NetworkTopologyStrategy. Но при этом SimpleStrategy менее ресурсоемкая, что может оказаться весомым фактором при учете нефункциональных требований, выставленных проекту. В рамках стратегии есть возможность выставить фактор репликации (множитель количества копий одной и той же информации, распределенной на другие узлы). По умолчанию выставляется равным 3.

Для обеспечения схемы с более эффективным чтением (или записью) необходимо знать топологию. За предоставление данных о топологии во время запроса отвечает осведомитель. Причем, если реализован не самый простой осведомитель вида SimpleSnitch, то динамическая его реализация мониторит изменения состояния всех инстансов, постоянно актуализируя так оптимальные маршруты запросов.

В Cassandra реализована многокомпонентная структура хранения данных, призванная реализовать быстрый доступ к данным как при чтении, так и при записи. 

Компоненты хранения данных в узле Cassandra
Компоненты хранения данных в узле Cassandra

Каждая запись заносится в журнал фиксации. Журнал — инструмент восстановления базы на случай сбоев, который обеспечивает долговечность хранения данных. Любая операция записи не считается завершенной, если она не размещена в журнале фиксации. Инструмент отдаленно напоминает журнал предзаписи (WAL) почти любой реляционной БД.

После успешной записи информации в журнале данные попадают в таблицу памяти — memtable. В ней данные, относящиеся к одной таблице, хранятся вместе до тех пор, пока не образуют критическую массу, оставаясь доступными к чтению из оперативной памяти, что ускоряет выдачу запросов. 

Вместе с попаданием в memtable данные помещаются в структуру, именуемую LSM-деревом, которая призвана предоставлять функциональность индексированного поиска, оптимизированного под алгоритмы. Эти алгоритмы предполагают частую вставку — баланс i/o-операций смещен в сторону i. По достижении порога таблица перебрасывается в файл SSTable вместе с LSM-деревом на диске, а в памяти операционной системы создается таблица под эту сущность.

Запись данных в Apache Cassandra на уровне компонентов, схема с www.oreilly.com
Запись данных в Apache Cassandra на уровне компонентов, схема с www.oreilly.com

Важно, что процесс сброса данных в файл SSTable не блокирующий: система позволяет существовать в памяти нескольким таблицам, относящимся к одной таблице, а их сброс выстраивается в очередь. Но ждать такой очереди приходится не очень долго, так как процесс сброса не ресурсоемкий.

В журнале фиксации есть флаг сброса данных на диск. Когда процесс завершился, флаг опускается — это сигнал к очистке журнала фиксации. Данные уже помещены в долгосрочную память и не нужно хранить информацию. По умолчанию асинхронная очистка журнала производится раз в 10 минут, асинхронная очистка memtable — раз в 10 секунд. Также Apache Cassandra позволяет настроить синхронный способ очистки и кэша, и журнала фиксации. Важно, что синхронное очищение в моменте может съедать оперативную память сервера, что негативно повлияет на производительность как конкретной ноды, так и кластера в целом. 

Кстати, построение базы данных на основе SSTable механики как нижнего уровня хранения информации подтверждает тот факт, что Cassandra не является колоночной БД.

Для большей эффективности работы с объемом памяти на носителе реализована механика сжатия файлов SSTable. Если описывать механику вкратце, то это слияние двух или более файлов со строковыми таблицами в один с помощью алгоритма, напоминающего сортировки слияния (mergesort). 

В итоге у нас будет один файл таблицы, содержащий уникальные ключи, отсортированные в порядке ключа. При этом необъединенные файлы отсортированы в порядке записи: запись идет всегда в конец файла, чтобы облегчить процесс. 

Я не буду детально описывать работу SSTable, LSM-деревьев и индексов на их основе. Просто держим в голове факт, что обе структуры оптимизированы по своей природе в сторону записи в балансе операций записи и чтения, что обуславливает характер использования Apache Cassandra. 

Решение от Apache Cassandra поддерживает несколько уровней кэша. Помимо уже упомянутой таблицы в памяти, реализованы кэш ключей, кэш строк и фильтры Блума. Кэш ключей хранит соотношение ключей разделов на элементы индекса строк. Кэш строк содержит строки целиком, но выборка строк ограничена списком наиболее адресуемых строк. 

Фильтры Блума — алгоритм на основе хэширования отдельных данных и представлениях всех данных в виде дайджест-строки варьирующейся длины. Алгоритм нацелен на повышение эффективности операций чтения. Фильтры — инструмент приблизительной оценки наличия искомых данных в долгосрочной памяти: если он дает положительный ответ, значит, искомые данные возможно есть в узле, если ответ отрицательный — искомых данных точно нет. 

Фильтры Блума могут давать ложноположительные результаты, но их процент довольно низок. Точность можно повысить, выделив больше памяти под сам фильтр. При этом фильтры никогда не дадут ложноотрицательных результатов.

Процессы чтения и записи

Так как Cassandra представляет собой распределенную базу данных, логично, что в ней реализовано несколько политик согласованности записи и чтения. 

Уровни согласованности при записи:

Уровни согласованности

Значение

ANY

Любой узел (не менее одного), являющийся репликой данных, должен подтвердить факт записи. Напоминание также является фактом записи

ONE/TWO/THREE

Любые три узла, которые подтвердили факт записи и являются репликой данных

LOCAL_ONE

Любой узел (не менее одного), являющийся репликой данных, должен подтвердить факт записи. При этом этот узел должен располагаться в ЦОДе, к которому идет обращение

QUORUM

Большинство узлов подтвердили факт записи и являются репликой данных

LOCAL_QUORUM

Большинство узлов подтвердили факт записи и являются репликой данных. Узлы должны располагаться в ЦОДе, к которому идет обращение

EACH_QUORUM

Большинство узлов каждого ЦОДа, которые являются репликами данных, подтвердили факт записи

ALL

Все узлы подтвердили запись и являются репликами данных

Отдельно расскажу про уровень ANY. Этот уровень гарантии — единственный, который не обеспечивает долговечного хранения 100%. Значит в случае, когда не работает узел, на который должны записаться данные, сервер сделает лишь пометку — напоминание. При этом сохранения в журнал фиксации не будет.

Напоминание — это сообщение «У меня, узла-координатора А, есть информация для узла Б. Я сохраню информацию и буду мониторить доступность узла. Как только он снова окажется доступным, я с помощью протокола сплетен донесу эту информацию до него». 

Вероятность сценария, при котором потеря данных реализовалась бы как фактор риска, крайне мала. Узел-реплика должен пролежать достаточно долго, чтобы закончился срок хранения напоминания, или помимо узла-реплики должен упасть и узел-координатор. Тем не менее такой сценарий стоит учитывать при проектировании базы данных.

Последовательность шагов для записи данных:

1. Появляется запись в конце журнала фиксации.

2. Данные добавляются в таблицу памяти.

3. Если в данных содержатся строки, которые хранятся в кэше, то строка объявляется недействительной в кэше.

4. Если таблица в памяти или журнал фиксации достигла порога объема данных, то она сбрасывается в файл SSTable на диске.

5. Узел-координатор при наличии незавершенных операций записи в другие узлы-реплики создает напоминания.

В реляционных БД для упорядочивания операций часто используются транзакции с разным уровнем изолированности. В случае распределенных систем для линеаризуемой согласованности используются облегченные транзакции LWT — lightweight transactions на основе алгоритма получения консенсуса Paxos. При линеаризуемой согласованности параллельные операции выглядят для пользователя так, будто выполняются последовательно (или результат их совокупности аналогичен). 

LWT чем-то напоминает двухфазный коммит (2PC). Если кратко, то в случае рассматриваемой нами БД процесс выглядит так:

LWT sequence diagramm
LWT (Paxos-based) sequence diagramm

LWT имеет некоторую схожесть с уровнем изоляции serializable из реляционных баз данных, который является самым строгим и ресурсоемким. LWT хоть и облегченный аналог, но тоже довольно прожорливая операция. Это необходимо учитывать при проектировании БД.

Уровни согласованности при чтении

Уровни согласованности

Значение

ONE/TWO/THREE

Немедленное возвращение данных клиенту, которые предоставил первый узел. При этом создается экземпляр работы, который в фоновом режиме сравнивает данные реплик. Если данные в какой-то из реплик устарели, то происходит исправление на этапе чтения для синхронизации с самым последним значением — это один из сценариев антиэнтропийного исправления данных

LOCAL_ONE

Любой узел (не менее одного), являющийся репликой данных, должен предоставить данные. При этом этот узел должен располагаться в ЦОДе, к которому идет обращение

QUORUM

Запрос отправляется всем узлам. После получения ответа от большинства ответ предоставляется клиенту, вместе с тем запускается процесс исправления на этапе чтения. Формул кворума с округлением в большую сторону: количество реплик/2+1

LOCAL_QUORUM

Запрос отправляется всем узлам. После получения ответа от большинства ответ предоставляется клиенту, вместе с тем запускается процесс исправления на этапе чтения. Но кворум должен быть достигнут в рамках локального ЦОДа

EACH_QUORUM

Запрос отправляется всем узлам. После получения ответа от большинства ответ предоставляется клиенту, вместе с тем запускается процесс исправления на этапе чтения. Но кворум должен быть достигнут в рамках каждого из ЦОДов, в которых расположены реплики

ALL

Запрос отправляется всем узлам. После получения ответа от всех реплик ответ предоставляется клиенту. В фоновом режиме запускается процесс исправления на этапе чтения

Рассмотрим путь чтения по компонентам:

1. Строка ищется в кэше, в котором хранятся строки, к которым чаще всего обращаются клиенты.

2. Если в кэше строк ничего не найдено, поиск идет по кэшу ключей.

3. Если в обоих кэшах по запросу пусто, идет чтение в таблицах в памяти.

4. Если на всех уровнях кэша данные отсутствуют, то идет чтение из долгосрочного источника хранения — SSTable. Если информация найдена в файле строковой таблицы — данные копируются в кэш строк (здесь же отрабатывает и фильтр Блума).

Проектирование БД

Apache Cassandra — распределенное строковое хранилище, формирующее разряженные хэш-таблицы. Несколько выводов:

1. Мы будем иметь дело с кластерами в качестве слоя формирования схемы данных. 

2. Большое значение будет иметь ключ строки. В случае Cassandra он многокомпонентный.

3. Структура разряженной хэш-таблицы приводит к тому, что стандартные механизмы построения схемы данных возможно использовать частично при проработке схемы данных в Cassandra, но крайне не рекомендуется, потому что является антипаттерном. Стандартные схемы построения данных будут неэффективны, так как разные классы хранилищ по-разному обходятся с памятью, выделяемой для данных.

Схема данных в Cassandra на https://dev.to Столбец — пара имя-значение. Строка — контейнер столбцов, на который можно сослаться по первичному ключу. Таблица — контейнер строк. Пространство ключей — контейнер таблиц. Кластер — контейнер пространства ключей, расположенных в одном или нескольких узлах.
Схема данных в Cassandra на https://dev.to Столбец — пара имя-значение. Строка — контейнер столбцов, на который можно сослаться по первичному ключу. Таблица — контейнер строк. Пространство ключей — контейнер таблиц. Кластер — контейнер пространства ключей, расположенных в одном или нескольких узлах.

Все слои данных вложены один в другой подобно матрешке. Где-то может быть связь «один к одному», но чаще наблюдается соотношение «один-ко-многим», где единицей выступает внешний контейнер. 

Пространство ключей — самый внешний контейнер для хранимых данных, а значит и крупный. Во многом он похож на понятие базы данных в реляционных БД: он является контейнером для таблиц и обладает рядом атрибутов, которые предопределяют его поведение в целом. 

Таблица — набор строк, упорядоченных по ключам и часто широких. Их еще называют разделами. Каждая строка содержит свой набор столбцов. Чтобы обратиться к конкретной строке, нужно найти адрес одной части ключа (composite/compound key), которая называется ключом раздела (partition key). Ключ раздела обязателен и определяет абсолютный адрес раздела и второй части, которая называется кластерным ключом или столбцом (clustering key/column). Кластерный ключ отвечает за сортировку строк внутри широкой строки и является необязательной частью. 

Порядок столбцов в кластерном ключе невероятно важен, так как при построении запроса в таблицу с фильтрацией по кластерному ключу нельзя откинуть промежуточную часть ключа. Например, нельзя установить фильтр по первому и третьему столбцу кластерного ключа, опуская по второму столбцу (но можно по первому и второму, не фильтруя по третьему). 

Столбцы — самая базовая единица в модели данных. Более-менее полный аналог столбцов в реляционной базе данных, за исключением особого вида столбца под названием статичный (static column). Такой столбец не является частью ключа, но при этом его значение одинаково для всех строк раздела и не хранится в каждой строке, не дублируя информацию для них, что экономит дисковое пространство.

На фоне перечисленных особенностей Apache Cassandra тот факт, что проектирование схемы данных в данной БД обладает рядом особенностей, не является чем-то из ряда вон выходящим. 

Что нужно учитывать при «набрасывании» схемы БД:

  1. Раз мы имеем дело с нереляционной БД, то логично, что операции соединений на уровне кода базы данных не поддерживается. Мы можем попробовать соединить на уровне сервиса или вспомнить про понятие витрины в реляционной БД и делать таблицу под конкретный запрос.

  2. Все таблицы строятся от противного — от запроса. Потенциально у нас может быть куча дублирующейся информации между таблицами. Бывают кейсы, когда таблицы «зеркалят» друг друга. Например, в одном из наших сервисов есть опция выдать все карточки, которые связаны с каким-либо номером телефона или почтовым ящиком, и есть инвертированная опция — выдать все номера телефонов или почтовые ящики, которые так или иначе связаны с определенной картой. 

    В первом случае таблица, в которой ключом будет выступать ID карты, а номер телефона и почтовый ящик будут обычными вторичными колонками. Во втором случае ключом будет номер телефона и почтовый ящик, а ID карты — вторичной колонкой.

    В остальном эти две таблицы будут обладать идентичным набором вторичных колонок: статус, дата изменения, дата создания и так далее. Как следствие, мы должны понимать, что такое понятие, как нормализация, нам совсем не товарищ при проектировании базы данных в этом решении.

  3. Начиная с версии 3.0 Apache Cassandra поддерживает материализованные представления, которые работают аналогично представлениям в классической SQL-парадигме. В теории они могут сократить количество необходимых таблиц, но не во всех кейсах. Из-за необходимости постоянно актуализировать материализованные представления из-за обновления базовых таблиц страдает производительность записи (и чем больше материализованных представлений реализовано, тем больше производительность страдает). Если в представлении требуется другая структура ключей, отличная от структуры ключей базовой таблицы, то реализовать такое представление будет невозможно и мы вернемся к варианту построения полноценной таблицы.

  4. Еще одним аспектом проектирования БД в Apache Cassandra является оптимизация хранения на этапе проектирования. Здесь она имеет еще большее значение, чем в реляционной таблице, потому как у разработчика куда меньше инструментов оптимизации работы с данными на уровне запросов. 

    В Apache Cassandra существуют вторичные индексы, но они скорее зло, так как сильно бьют по производительности записи. Поскольку каждая таблица хранится в отдельном файле на жестком диске, важно, чтобы взаимосвязанные столбцы хранились в одной таблице, иначе мы получим большое количество операций ввода-вывода с диска, которые очень часто становятся узким горлышком в современных проектах. 

    С другой стороны, нужно соблюдать баланс, потому что слишком большие таблицы начнут влиять как на уровень кэша, так как и на уровень SSTable. По возможности стоит использовать статичные колонки, которые дают экономию по объему хранимых данных.

  5. Сортировка проводится на этапе построения таблицы. В Apache Cassandra поддерживается конструкция ORDER BY, но только в рамках колонок, составляющих кластерный ключ. Кластерные ключи отвечают за сортировку данных в рамках таблицы и в рамках широкой строки. Сортировка по вторичным колонкам невозможна.

За основными типами данных и «базой» по языку CQL, используемым в Cassandra вместо выведенного из оборота протокола Thrift, знание которых наверняка пригодится при проектировании БД, рекомендую обратиться к официальной документации данного решения от Apache. Отмечу, что в Cassandra куда чаще сталкиваешься с UDT, так что стоит обратить пристальное внимание на соответствующий раздел.

Максимальный размер партиции или раздела в Cassandra составляет около двух млрд ячеек. Чтобы прикинуть размер раздела в ячейках, можем воспользоваться формулой:

Nv = Nr(Nc-Npk-Ns)+Ns
Nv = Nr(Nc-Npk-Ns)+Ns

где Nv — число ячеек (значений) в разделе, Nr — число строк, Nc — число столбцов, Npk —число столбцов первичного ключа и Ns — число статических столбцов.

Например, у нас в одном из сервисов есть таблица, «обменивающая» PAN карты на наш внутренний ID. Помимо колонок PAN (в зашифрованном виде) и ID в таблице присутствуют и другие, которые «обогащают» данные. Всего колонок в таблице 7, из которых 0 статических и одна принадлежит к первичному ключу, являясь ключом раздела. Количество карт у нас достигает 100 млн (в качестве примера вполне допустимо округление). Таким образом, в нашем случае при расчете количества значений получится такой результат:

Nv = 100 000 000 × (9 − 1− 0) + 0 = 800 000 000.

Чтобы перейти на менее абстрактный уровень, можем воспользоваться формулой оценки занимаемого места на диске одной таблицей:

St = ∑isizeOf(Cki) + ∑jsizeOf(Csi) + Nr × ∑k(sizeOf(Crk) + ∑lsizeOf(Ccl)) + 8 × Nv
St = ∑isizeOf(Cki) + ∑jsizeOf(Csi) + Nr × ∑k(sizeOf(Crk) + ∑lsizeOf(Ccl)) + 8 × Nv

где Ck — столбцы первичного ключа, Cs — статические столбцы, Cr — обычные столбцы, Cc — кластерные столбцы, Nv — число ячеек (значений) в разделе, Nr — число строк и sizeOf() — внутренняя функция Cassandra, возвращающая размер типа данных переданного столбца в байтах.

Рассчитаем объем занимаемой таблицы, для которой рассчитывали ее объем в ячейках. У нас в таблице имеется:

  • Три поля с типом text, одно из которых представляет собой результат хэш-функции hmac-sha256. Это же поле является единственной частью первичного ключа и весит 32 байта. Еще одно поле нормированной длины в 4 символа UTF-8 имеет размер 4 байта — мы не ожидаем там символов с весом больше 1 байта. Третье поле ненормированной длины — для удобства возьмем среднюю длину в 25 символов в кодировке UTF-8, что будет означать дополнительные 25 байт.

  • Одно поле с типом uuid. Вес у такого типа данных фиксированный и составляет 16 байт.

  • Два поля с типом timestamp. Apache Cassandra кодирует этот тип данных как 64-битный integer, что выливается в 8 байт.

  • Три поля представляет собой UDT — пользовательский тип данных. Они реализованы не в виде blob. Одно поле содержит несколько атрибутов, общий средний размер которых составляет 23 байта. Второе поле содержит два атрибута bigint, что в сумме дает 16 байт. Третье поле имеет сложную вложенную структуру объектов с хэшированием, средний размер одного объекта — 536 байт.

Первый член уравнения — суммарный размер столбцов, формирующих ключ раздела; второй член — сумма размера статических столбцов; третий член — размер ячеек в разделе; четвертый член — количество временных меток, которые в Cassandra хранит для каждой ячейки.

У нас получается расчет:

St = 32 + 0 + 100 000 000 × ((4+25+16+8+8+23+16+536) + 0) + 8 × 800 000 000 = 32 + 63 600 000 000 + 6 400 000 000 = 70 000 000 032 байт = 66 757,2 мегабайт = 65,19 гигабайт.

Последняя формула позволяет нам перейти от оценки размера отдельно взятой таблицы к общему занимаемому ею пространства (с учетом реплик):

Tt = St × RFk × CSFv
Tt = St × RFk × CSFv

где St — размер таблицы, RFk — коэффициент репликации таблицы, CSFv — коэффициент уплотнения таблицы в зависимости от выбранной стратегии (равняется либо 2, либо 1,25).

В нашем случае у таблицы стоит реплика-фактор 3 и выбрана стратегия LeveledCompactionsStrategy, которая является стратегией для более интенсивного чтения и дает коэффициент 1,25 (стратегия по умолчанию дает коэффициент 2).

Соответственно получаем следующий совокупный размер для нашей таблицы (в ГБ):

Tt = 65,19 × 3 × 1,25 = 244,46 ГБ.

Фактический размер несколько отличается — 277,5 ГБ. Важно, что инструмент оценки выше — приблизительный и может давать результат с погрешностями.

Вместо выводов

Apache Cassandra обладает рядом преимуществ и недостатков, которые необходимо учитывать при ее использовании. Она поможет, если:

  • Предполагается сильное превалирование операций записи над операциями чтения.

  • Нет необходимости в сложных и многочисленных аналитических отчетах. Возможно — как хранилище сырых данных с последующей миграцией в более подходящую OLAP-среду для аналитики.

  • Предполагается потоковая обработка данных с последующим «прихраниванием» в долгосрочное хранилище в качестве бэкапа.

  • Есть уверенность в крупных масштабах системы или потребности в резком и гибком горизонтальном масштабировании.

  • Есть понимание, что необходимо будет делать геораспределенную систему.

  • Не столь важна связанность между сущностями.

Почему мы выбрали Apache Cassandra для своего проекта:

  • У нас на продукте есть крен в сторону асинхронной записи, более-менее равномерно распределенный в течение дня. При этом цикл чтений имеет волнообразную структуру, так как в основном чтение необходимо при построении отчетностей клиентами сервиса или записи в DWH (суточные срезы). Тут также помогает связка с valkey (наследник redis, после того как последний перешел на лицензию AWS).

  • Так как наша система имеет довольно критичный уровень для бизнеса, мы сразу понимали, что нам необходимо решение, которое будет легко горизонтально масштабироваться. Cassandra позволяет это делать динамически.

  • В нашем кейсе нет сложных OLAP-запросов.

  • У нас нет или почти нет запросов по диапазону значений: чаще всего батчи мы внутри бьем на единичные запросы (но не во всех кейсах). Особенно плохо работу с батчами Aapche Cassandra переносит при операциях записи на мультипартицированных запросах. Это связано с тем, что координатор становится узким горлышком из-за своей синхронной природы общения с нодами. Батчи на запись стоит использовать только в случаях необходимости поддержания атомарности по группе данных. 

  • Объем данных не колоссальный, что позволяет быть уверенным, что механики репликации не будут сильно нагружать инфраструктуру, при этом позволит быть высокодоступным сервисом.

  • Структура хранения данных в нашей модели (у нас кошелек карточек по сути своей представляет собой граф) в cassandra ложится лучше, чем на модель SQL.

Некоторые результаты НТ на тестовом контуре. Мы тестировали, насколько наш сервис может держать нагрузки на сохранение карточек и на их выдачу при запросе по нашему внутреннему ID или по PAN. Это критичная функциональность системы, так как важно вовремя отдавать аналитический токен потребителям системы. 

Нагрузочное тестирование показало, что отчасти из-за легкой горизонтальной масштабируемости БД система способна выдерживать предъявляемые к ней нефункциональные требования с запасом (при таргетных значениях в 500+ млн карт, что соответствует почти всему рынку карточных продуктов РФ). 

Сохранение новой карты:

По вертикальной оси — количество RPS (Request per second), по горизонтальной оси — временная шкала
По вертикальной оси — количество RPS (Request per second), по горизонтальной оси — временная шкала

Получение карты по cardId:

По вертикальной оси — количество RPS (Request per second), по горизонтальной оси — временная шкала
По вертикальной оси — количество RPS (Request per second), по горизонтальной оси — временная шкала

Конфигурация: 4 CPU 15 GB RAM 9 nodes RF=3

В следующей статье расскажу, как теоретические основы мы применяли на практике, где-то обходя корнер-кейсы, где-то ставя во главу угла неожиданные сильные стороны Cassandra, которые очень хорошо укладывались в концепцию нашего проекта и позволили адаптироваться к выдвигаемым бизнес-подразделениями требованиям. Оказалось, Cassandra сносно справляется с графами.

И на прощание немного полезных ссылок по теме:

Комментарии (0)