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

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

В платформе контейнеризации dBrain.cloud нам пришлось пройти через несколько сценариев миграции Cassandra на разных проектах и в различных средах: от перехода со StatefulSet на оператор до переноса кластеров между дата-центрами. За это время сформировалось несколько практических подходов, каждый из которых работает только в определенных условиях.

Разберем, где достаточно snapshot, когда стоит использовать междатацентровую репликацию, а в каких случаях лучше вообще отказаться от переноса исторических данных.

Как работала Cassandra в dBrain.cloud

Первая версия сервиса БД Cassandra в платформе dBrain.cloud была максимально простой.

Каждый кластер представлял собой обычный StatefulSet в Kubernetes. Мы собирали собственный образ Cassandra, использовали стандартные абстракции k8s, такие как ConfigMap, Secrets, Service, подключали экспортер метрик в виде сайдкара. Никаких операторов тогда не было, только наш специализированный контроллер-менеджер развертывания БД.

Данный контроллер-менеджер (первая версия нашего сервиса управления Cassandra) по jinja2-шаблону генерировал и применял в namespace весь набор объектов кластера, включая, помимо перечисленных выше, еще ServiceAccount и Role/RoleBinding, а параметры cassandra.yaml и jvm.options (heap, concurrent_, memtable_, commitlog) рассчитывал исходя из выданных кластеру CPU, RAM и объема диска.

Операции над нодами — repair, cleanup, compaction, flush, garbage collect, drain, decommission, removenode, очистка snapshot’ов и truncate hints — менеджер выполнял через JMX (по Jolokia) как по запросу, так и по расписанию (через создаваемые k8s CronJob). А масштабирование, изменение ресурсов и heap, ресайз дисков и правку конфигов с rolling-restart — уже через Kubernetes API. Автоматического резервного копирования в самом сервисе при этом не было: снапшоты при необходимости снимались вручную.

Фактически кластером управляли сама Cassandra и базовые механизмы Kubernetes. Узлы находили друг друга по gossip-протоколу, используя headless-сервис, данные хранились на постоянных томах, а все операции по обновлению и сопровождению выполнялись вручную.

Такой подход долгое время нас устраивал. Но по мере роста количества кластеров и их объемов сопровождать инфраструктуру становилось все сложнее.

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

Поэтому следующим этапом развития стал переход на K8ssandra.

Что изменилось после появления оператора

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

Появилась автоматизация обслуживания кластера, резервное копирование через Medusa, плановые repair-процедуры, контроль состояния нод и поддержка обновления версий Cassandra.

Стоит уточнить, что под оператором здесь имеется в виду весь стек K8ssandra, а не один компонент. Верхнеуровневый K8ssandra-operator работает с ресурсом K8ssandraCluster, а нижележащий cass-operator управляет ресурсом CassandraDatacenter: создает и сопровождает сами StatefulSet’ы, отвечает за rolling-restart, масштабирование и замену нод, а также позволяет реализовать мультирековую топологию, о которой мы еще поговорим.

Важное отличие от старой схемы в том, что управление нодами больше не идет через прямой JMX — между оператором и Cassandra теперь стоит cass-management-api (REST), а конфигурацию готовит cass-config-builder. За счет этого эксплуатация стала декларативной: достаточно описать желаемое состояние кластера в одном манифесте, а оператор автоматически приведет инфраструктуру в соответствие с ним.

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

Поверх оператора мы построили вторую версию собственного сервиса управления Cassandra и интегрировали ее в платформу.

С новыми кластерами проблем не возникало. Они сразу создавались через оператор и управлялись единообразно. Сложности появились в другом месте.

К этому моменту в эксплуатации уже находилось большое количество существующих кластеров, развернутых по старой схеме через StatefulSet. Их тоже нужно было перевести на новую архитектуру. Именно здесь выяснилось, что самой сложной частью миграции оказались не новая архитектура кластеров Cassandra и не ограничения Kubernetes, а сами данные.

Почему не все snapshot’ы одинаково полезны

Первое решение, которое обычно приходит в голову при миграции Cassandra, — использовать snapshot с последующим переносом всех нужных SSTable и его восстановлением в новом кластере.

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

Для небольших кластеров, особенно без больших keyspace с избыточной репликацией данных, это действительно вполне рабочий сценарий.

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

Именно с такими системами мы чаще всего сталкиваемся в платформе контейнеризации dBrain.cloud.

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

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

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

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

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

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

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

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

Репликация между дата-центрами

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

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

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

На практике именно этот подход стал для нас основным сценарием миграции со старых StatefulSet-кластеров на операторскую модель. Однако здесь есть важный нюанс.

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

Перед отключением старого дата-центра необходимо убедиться, что все данные корректно распределены по репликам нового кластера. Для этого требуется выполнить repair нового дата-центра относительно старого. Именно на этом этапе обычно возникают основные затраты.

Если речь идет о кластере из 3–9 нод и нескольких сотнях гигабайт данных на ноду, операция проходит относительно спокойно. Но по мере роста масштаба стоимость такого подхода резко увеличивается.

На кластерах размером более 30 нод с replication factor = 3 и большими SSTable repair может занимать очень продолжительное время. Помимо длительности самой операции появляется и дополнительная нагрузка на инфраструктуру.

Во время repair возрастает задержка обработки запросов, увеличивается нагрузка на дисковую подсистему и сеть. Дополнительно Cassandra начинает активно выполнять compaction SSTable-файлов, что приводит к временному росту потребления дискового пространства и увеличению нагрузки по IOPS.

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

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

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

Именно поэтому обновление Cassandra после миграции мы всегда предварительно проверяем на тестовых и препродакшен-окружениях.

Когда лучший способ миграции — не переносить данные

После snapshot и репликации между дата-центрами остается еще один класс кластеров, для которых оба предыдущих подхода оказываются неэффективными, иногда даже принципиально невозможными без дополнительных существенных затрат.

Речь идет о действительно крупных инсталляциях. В нашей практике это кластеры на 30–50 и более нод с терабайтами данных на каждом сервере и большими SSTable. На таких объемах начинают работать уже не только ограничения самой Cassandra, но и ограничения инфраструктуры.

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

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

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

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

Исторические данные продолжают храниться в старом кластере и постепенно удаляются естественным образом по мере истечения TTL. Новый кластер параллельно наполняется только актуальными данными.

В нашем случае такой подход особенно хорошо работает при использовании стратегии TimeWindowCompactionStrategy (TWCS). При хранении временных рядов данные группируются в SSTable по временным окнам, а после истечения TTL Cassandra может удалять нужные SSTable целиком, не выполняя тяжелые операции по очистке отдельных записей внутри разрозненных SSTable’ов.

За счет этого старый кластер постепенно разгружается практически естественным образом, без существенной дополнительной нагрузки на систему хранения и RAM/CPU, без лишних compaction.

Именно эта особенность позволила нам эффективно использовать подобный сценарий даже на очень крупных инсталляциях. Сегодня отдельные ноды Cassandra в нашей инфраструктуре хранят до 12 ТБ данных, а самый крупный кластер Cassandra в платформе контейнеризации dBrain занимает около 0,5 ПБ данных.

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

Такой подход позволяет избежать практически всех тяжелых операций, характерных для классической миграции Cassandra, а главное избежать downtime, практически не увеличивая объемы хранимых в моменте данных. Не требуется репликация между дата-центрами. Не требуется выполнять масштабный repair всего объема данных. Не возникает дополнительной нагрузки на сеть и систему хранения. Не нужно временно резервировать объем дискового пространства под вторую копию базы данных.

Фактически миграция происходит за счет естественного жизненного цикла самих данных.

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

Тем не менее для самых крупных кластеров этот подход оказался наиболее предсказуемым и наименее затратным с точки зрения инфраструктуры.

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

Когда нужно переехать, а не мигрировать

Есть еще один сценарий, который стоит рассматривать отдельно. Речь уже не о переходе на оператор, обновлении версии Cassandra или изменении архитектуры кластера. Иногда задача выглядит гораздо проще: нужно перенести существующий кластер Cassandra из одного кластера Kubernetes в другой или переехать из одного дата-центра в другой. За последние годы в платформе контейнеризации dBrain.cloud у нас было немало подобных кейсов.

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

В таких условиях привычные механизмы миграции данных далеко не всегда оказываются лучшим выбором. На практике наиболее эффективным подходом для подобных задач стала обычная синхронизация физических данных Cassandra через rsync.

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

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

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

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

По сути, мы не мигрируем данные логически, а переносим физическое состояние кластера практически без изменений.

Выглядит это примерно так: rsync -e “ssh -T -i /path/to/sshkey -c aes128-gcm@openssh.com -o Compression=no -o StrictHostKeyChecking=no -x” --progress --stats -s -avh --partial --delete --no-compress /mnt/cassandra-0/data/ root@destserver:/newcasspvmount-0/data/

Запускать можно параллельно на нескольких пар соответствующих инстансов (старый-новый соответственно), лучше в screen или tmux, предварительно настроив межсетевое взаимодействие и смонтировав директории, естественно начальная синхронизация данных, о которой мы говорили, осуществляется без флага –delete.

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

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

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

Если это условие выполняется, то перенос физических данных через rsync зачастую оказывается значительно быстрее и проще любых логических миграций.

Выводы

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

Для небольших систем достаточно snapshot. Для средних хорошо работает репликация между дата-центрами. Для самых крупных кластеров зачастую приходится пересматривать сам подход к миграции и искать способы вообще отказаться от копирования существующих данных.

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

Будет интересно сравнить этот опыт с практикой других команд. Если вам приходилось мигрировать крупные кластеры Cassandra или вы сталкивались с похожими ограничениями по данным, инфраструктуре или времени простоя, поделитесь своим опытом в комментариях.

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

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