company_banner

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

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

    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.


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

    Флант
    DevOps-as-a-Service, Kubernetes, обслуживание 24×7

    Комментарии 5

      0
      Спасибо за материал!
      Интересно было бы узнать, как вы готовите Cassandra.
      В частности, такие аспекты:
      • каждая нода-под живет на отдельной воркер-ноде?
      • выделяете ли под commitlog и data files разные физические диски (как это советует документация — cassandra.apache.org/doc/latest/operating/hardware.html)
      • память на нодах куба, где живет cassandra — ECC? (из той же доки)
      • уже что-то можете сказать об операторе CassKop?
      • случалось ли ловить такую проблему: удалили промаркировали надгробиями какие-то данные, надгробия исчезли (но часть реплик этих данных остались живы, т.к. одна из нод была в дауне и не удалила у себя), запустили repair — данные вновь появились (здесь — www.youtube.com/watch?v=SAyClLjN6Sk докладчик рассказывал об этом)
      • можете поделиться какими-то цифрами по корреляции кол-ва записей в секунду/минуту/час/сутки и занимаемым местом на дисках?
      • такая же корреляция, но с CPU/RAM выделенных нодам-подам? (реквесты, лимиты)
        +2
        Добрый день. Вот ответы на ваши вопросы:
        • Да, каждый под выкачен на свою ноду.
        • Разделения по дискам мы не делали, просто используем ssd.
        • К памяти тоже не педъявляли особых требований.
        • К оператору пока нареканий нет, работает отлично.
        • Единственная проблема с которой сталкиывались, когда переполнилось место и удалили keyspace, он не удалился на переполненной ноде и после добавления места и перезапуска пода, сохранился старый IP пира, вот в этой таблице (SELECT peer,rpc_address FROM system.peers;) Пришлось удалить в ручную. Проявилось это тем, что приложение получало не правильный адрес, и пыталось подключиться по несуществующему IP.
        • Используем выделенные ноды 16 CPU и 32Gb RAM. Данных по 2.2-2.3Тб на каждой.
        • Нагрузочное тестирование показало 360rps при размере сообщений от 400Кб до 4500Кб, трафик был в сумме 180Мб/c.
          0
          Благодарю!
        0
        использование одинаковых pod network — не очень хорошая идея

        Я правильно понял, что имелось ввиду то, что в рамках одного проекта в разных кластерах был установлен одинаковый pod-network-cidr / podSubnet?

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

      Самое читаемое