Наш опыт миграции Cassandra между Kubernetes-кластерами без потери данных

в 9:05, , рубрики: cassandra, devops, kubernetes, kubernetes operator, Администрирование баз данных, Блог компании Флант, системное администрирование

Наш опыт миграции Cassandra между Kubernetes-кластерами без потери данных - 1

Последние ~полгода для работы с 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… — невозможна. Для решения этой задачи мы создадим временный кластер, используя его как своеобразный буфер для осуществления миграции.

Общая последовательность действий сводится к следующим шагам:

  1. Поднимаем Cassandra-new новым оператором в новом кластере.
  2. Масштабируем в 0 кластер Cassandra-new.
  3. Новые диски, созданные PVC, переключаем в старый кластер.
  4. Поднимаем Cassandra-temporary при помощи оператора в параллель с Cassandra-current так, чтобы диски были использованы от Cassandra-new.
  5. Масштабируем оператор Cassandra-temporary в 0 (иначе он восстановит исходное состояние кластера) и правим конфигурацию Cassandra-temporary так, чтобы Cassandra-temporary объединилась с Cassandra-current. Таким образом мы должны получить одну Cassandra и два дата-центра (подробнее про эту и другие сущности в Cassandra можно прочитать в нашей предыдущей статье).
  6. Переносим данные между дата-центрами Cassandra-temporary и Cassandra-current.
  7. Масштабируем кластеры 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).

Примечания:

  1. Когда запускаем Cassandra-temporary, надо указать одинаковое (с Cassandra-current) имя кластера. Это можно сделать через переменную CASSANDRA_CLUSTER_NAME.
  2. Чтобы 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 этапов:

  1. Масштабируем Cassandra-temporary и Cassandra-current (не забываем, что здесь у нас всё ещё работает оператор!) в 0.
  2. Переключаем диски (это сводится к настройке PV для Cassandra-new).
  3. Запускаем Cassandra-new, отслеживая, что подключаются нужные диски.
  4. Делаем ALTER всех таблиц, чтобы удалить старый кластер:
    ALTER KEYSPACE example WITH replication = {'class': 'NetworkTopologyStrategy', 'x2': 2};
  5. Удаляем все узлы старого кластера. Для этого достаточно выполнить такую команду в одном из его 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.

Читайте также в нашем блоге:

Автор: Timoshenko Eduard

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js