Наш опыт миграции Cassandra между Kubernetes-кластерами без потери данных
Последние ~полгода для работы с Cassandra в Kubernetes мы использовали Rook operator. Однако, когда нам потребовалось выполнить весьма тривиальную, казалось бы, операцию: поменять параметры в конфиге Cassandra, — обнаружилось, что оператор не обеспечивает достаточной гибкости. Чтобы внести изменения, требовалось склонировать репозиторий, внести изменения в исходники и пересобрать оператор (конфиг встроен в сам оператор, поэтому ещё пригодится знание Go). Всё это занимает много времени.
Обзор существующих операторов мы уже делали, и на сей раз остановились на CassKop от Orange, который поддерживает нужные возможности, а в частности — кастомные конфиги и мониторинг из коробки.
Задача
В реальной истории, о которой далее и пойдет речь, смену оператора было решено совместить с назревшей потребностью перенести в новый кластер всю инфраструктуру клиента. После миграции основных рабочих нагрузок из важных приложений осталась только Cassandra, потеря данных для которой была, конечно, недопустима.
Требования к её миграции:
- Максимальный простой — 2–3 минуты, чтобы фактически осуществить этот перенос одновременно с перекатом самого приложения в новый кластер;
- Перенести все данные без потерь и головной боли (т.е. без каких-либо дополнительных манипуляций).
Как осуществить такую операцию? По аналогии с RabbitMQ и MongoDB, мы решили запустить новую инсталляцию Cassandra в новом кластере Kubernetes, после чего объединить две Cassandra в разных кластерах и перенести данные, закончив весь процесс простым отключением исходной инсталляции.
Однако всё осложнилось тем, что сети внутри Kubernetes пересекаются, поэтому настроить связь оказалось не так просто. Требовалось прописывать маршруты для каждого pod«а на каждом узле, что весьма трудоёмко и вообще не надёжно. Дело в том, что связь по IP pod«ов работает только с мастеров, а Cassandra запущена на выделенных узлах. Таким образом, надо сначала настроить маршрут до мастера и уже на мастере — до другого кластера. В дополнение к этому, перезапуск pod«а влечёт за собой смену IP, а это ещё одна проблема… Почему? Об этом читайте далее в статье.
В последующей практической части статьи будут использоваться три обозначения для кластеров Cassandra:
- Cassandra-new — новая инсталляция, которую мы запустим в новом кластере Kubernetes;
- Cassandra-current — старая инсталляция, с которой в настоящий момент времени работают приложения;
- Cassandra-temporary — временная инсталляция, которую запустим рядом с Cassandra-current и задействуем только для самого процесса миграции.
Как же быть?
Поскольку Cassandra-current использует localstorage, простая миграция её данных в новый кластер — так могло бы быть, например, в случае дисков vSphere… — невозможна. Для решения этой задачи мы создадим временный кластер, используя его как своеобразный буфер для осуществления миграции.
Общая последовательность действий сводится к следующим шагам:
- Поднимаем Cassandra-new новым оператором в новом кластере.
- Масштабируем в 0 кластер Cassandra-new.
- Новые диски, созданные PVC, переключаем в старый кластер.
- Поднимаем Cassandra-temporary при помощи оператора в параллель с Cassandra-current так, чтобы диски были использованы от Cassandra-new.
- Масштабируем оператор Cassandra-temporary в 0 (иначе он восстановит исходное состояние кластера) и правим конфигурацию Cassandra-temporary так, чтобы Cassandra-temporary объединилась с Cassandra-current. Таким образом мы должны получить одну Cassandra и два дата-центра (подробнее про эту и другие сущности в Cassandra можно прочитать в нашей предыдущей статье).
- Переносим данные между дата-центрами Cassandra-temporary и Cassandra-current.
- Масштабируем кластеры Cassandra-current и Cassandra-temporary в 0 и запускаем Cassandra-new в новом кластере, не забыв перекинуть диски. Параллельно перекатываем приложения в новый кластер.
В результате таких манипуляций простой будет минимален.
В деталях
С первыми 3 шагами проблем не должно возникнуть — всё делается просто и быстро.
На данном этапе кластер Cassandra-current будет выглядеть примерно так:
Datacenter: x1
==============
Status=Up/Down
|/ State=Normal/Leaving/Joining/Moving
-- Address Load Tokens Owns Host ID Rack
UN 10.244.6.5 790.7 GiB 256 ? 13cd0c7a-4f91-40d0-ac0e-e7c4a9ad584c rack1
UN 10.244.7.5 770.9 GiB 256 ? 8527813a-e8df-4260-b89d-ceb317ef56ef rack1
UN 10.244.5.5 825.07 GiB 256 ? 400172bf-6f7c-4709-81c6-980cb7c6db5c rack1
Чтобы проверить, что всё работает, как ожидалось, создаём keyspace в Cassandra-current. Это делается ещё до запуска Cassandra-temporary:
create keyspace example with replication ={'class' : 'NetworkTopologyStrategy', 'x1':2};
Далее создадим таблицу и наполним её данными:
use example;
CREATE TABLE example(id int PRIMARY KEY, name text, phone varint);
INSERT INTO example(id, name, phone) VALUES(1,'Masha', 983123123);
INSERT INTO example(id, name, phone) VALUES(2,'Sergey', 912121231);
INSERT INTO example(id, name, phone) VALUES(3,'Andrey', 914151617);
Запустим Cassandra-temporary, помня, что до этого в новом кластере мы уже запускали Cassandra-new (шаг №1) и сейчас она у нас выключена (шаг №2).
Примечания:
- Когда запускаем Cassandra-temporary, надо указать одинаковое (с Cassandra-current) имя кластера. Это можно сделать через переменную
CASSANDRA_CLUSTER_NAME
. - Чтобы Cassandra-temporary могла увидеть текущий кластер, надо задать сиды. Это делается через переменную
CASSANDRA_SEEDS
или через конфиг.
Внимание! Перед началом перемещения данных необходимо убедиться, что типы согласованности для чтения и записи заданы как LOCAL_ONE
или LOCAL_QUORUM
.
После того, как запустится Cassandra-temporary, кластер должен выглядеть так (обратите внимание на появление второго дата-центра с 3 узлами):
Datacenter: x1
==============
Status=Up/Down
|/ State=Normal/Leaving/Joining/Moving
-- Address Load Tokens Owns Host ID Rack
UN 10.244.6.5 790.7 GiB 256 ? 13cd0c7a-4f91-40d0-ac0e-e7c4a9ad584c rack1
UN 10.244.7.5 770.9 GiB 256 ? 8527813a-e8df-4260-b89d-ceb317ef56ef rack1
UN 10.244.5.5 825.07 GiB 256 ? 400172bf-6f7c-4709-81c6-980cb7c6db5c rack1
Datacenter: x2
===============
Status=Up/Down
|/ State=Normal/Leaving/Joining/Moving
-- Address Load Tokens Owns (effective) Host ID Rack
UN 10.244.16.96 267.07 KiB 256 64.4% 3619841e-64a0-417d-a497-541ec602a996 rack1
UN 10.244.18.67 248.29 KiB 256 65.8% 07a2f571-400c-4728-b6f7-c95c26fe5b11 rack1
UN 10.244.16.95 265.85 KiB 256 69.8% 2f4738a2-68d6-4f9e-bf8f-2e1cfc07f791 rack1
Теперь можно осуществлять перенос. Для этого сначала перенесём тестовый keyspace — убедимся, что всё хорошо:
ALTER KEYSPACE example WITH replication = {'class': 'NetworkTopologyStrategy', x1: 2, x2: 2};
После этого в каждом pod«е Cassandra-temporary выполним команду:
nodetool rebuild -ks example x1
Зайдем в любой pod Cassandra-temporary и проверим, что данные перенесены. Так же можно добавить ещё 1 запись в Cassandra-current, чтобы проверить, что новые данные начали реплицироваться:
SELECT * FROM example;
id | name | phone
----+--------+-----------
1 | Masha | 983123123
2 | Sergey | 912121231
3 | Andrey | 914151617
(3 rows)
После этого можно делать ALTER
всех keyspaces в Cassandra-current и выполнить nodetool rebuild
.
Нехватка места и памяти
На данном этапе полезно вспомнить: когда выполняется rebuild, создаются временные файлы, по размеру эквивалентные размеру keyspace! Мы столкнулись с проблемой, что самый большой keyspace занимал 350 Гб, а свободного места на диске было меньше.
Расширить диск не представлялось возможным, поскольку используется localstorage. На помощь пришла следующая команда (выполнена в каждом pod«е Cassandra-current):
nodetool clearsnapshot
Так место освободилось: в нашем случае было получено 500 Гб свободного диска вместо доступных ранее 200 Гб.
Однако несмотря на то, что места хватало, операция rebuild постоянно вызывала перезапуск pod«ов Cassandra-temporary с ошибкой:
failed; error='Cannot allocate memory' (errno=12)
Её мы решили созданием DaemonSet, который раскатывается только на узлы с Cassandra-temporary и выполняет:
sysctl -w vm.max_map_count=262144
Наконец-то все данные были перенесены!
Переключение кластера
Оставалось только переключить Cassandra, что производилось в 5 этапов:
- Масштабируем Cassandra-temporary и Cassandra-current (не забываем, что здесь у нас всё ещё работает оператор!) в 0.
- Переключаем диски (это сводится к настройке PV для Cassandra-new).
- Запускаем Cassandra-new, отслеживая, что подключаются нужные диски.
- Делаем
ALTER
всех таблиц, чтобы удалить старый кластер:ALTER KEYSPACE example WITH replication = {'class': 'NetworkTopologyStrategy', 'x2': 2};
- Удаляем все узлы старого кластера. Для этого достаточно выполнить такую команду в одном из его pod«ов:
nodetool removenode 3619841e-64a0-417d-a497-541ec602a996
Суммарный простой Cassandra составил около 3 минуты — это время остановки и запуска контейнеров, так как диски были подготовлены заранее.
Финальный штрих с Prometheus
Однако на этом всё не закончилось. С Cassandra-new идёт встроенный экспортер (см. документацию нового оператора) — мы, естественно, им воспользовались. Примерно через 1 час после запуска стали приходить алерты о недоступности Prometheus. Проверив нагрузку, мы увидели, что выросло потребление памяти на узлах с Prometheus.
Дальнейшее изучение вопроса показало, что выросло число собираемых метрик в 2,5 раза (!). Всему виной была Cassandra, с которой собиралось чуть более 500 тысяч метрик.
Мы провели ревизию метрик и отключили те, что не посчитали нужными, — через ConfigMap (в нём, к слову, и настраивается экспортер). Итог — 120 тысяч метрик и значительно сниженная нагрузка на Prometheus (при том, что важные метрики остались).
Заключение
Так нам удалось перенести Cassandra в другой кластер, практически не затронув функционирование production-инсталляции Cassandra и не помешав работе клиентских приложений. Попутно мы пришли к выводу, что использование одинаковых pod network — не очень хорошая идея (теперь более внимательно подходим к первоначальному планированию установки кластера).
Напоследок: почему мы не воспользовались инструментом nodetool snapshot
, упомянутым в прошлой статье? Дело в том, что эта команда создаёт снимок keyspace«а в том состоянии, в котором он был до запуска команды. Кроме того:
- требуется гораздо больше времени, чтобы сделать снимок и перенести его;
- всё, что пишется в это время в Cassandra, будет утеряно;
- простой в нашем случае составил бы около часа — вместо 3 минут, которые получилось удачно совместить с деплоем приложения в новый кластер.
P.S.
Читайте также в нашем блоге: