company_banner

Миграция Cassandra в Kubernetes: особенности и решения



    С базой данных Apache Cassandra и необходимостью её эксплуатации в рамках инфраструктуры на базе Kubernetes мы сталкиваемся регулярно. В этом материале поделимся своим видением необходимых шагов, критериев и существующих решений (включая обзор операторов) для миграции Cassandra в K8s.

    «Кто может управлять женщиной, справится и с государством»


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

    • Cassandra написана на Java.
    • Топология Cassandra включает несколько уровней:
      • Node — один развернутый экземпляр Cassandra;
      • Rack — группа экземпляров Cassandra, объединенных по какому-либо признаку, находящаяся в одном дата-центре;
      • Datacenter — совокупность всех групп экземпляров Cassandra, находящихся в одном дата-центре;
      • Cluster — совокупность всех дата-центров.
    • Для идентификации узла Cassandra использует IP-адрес.
    • Для быстроты операций записи и чтения часть данных Cassandra хранит в оперативной памяти.

    Теперь — к собственно потенциальному переезду в Kubernetes.

    Check-list для переноса


    Говоря о миграции Cassandra в Kubernetes, мы надеемся, что с переездом управлять ей станет удобнее. Что для этого потребуется, что в этом поможет?

    1. Хранилище для данных


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


    Схема транзакций записи в Cassandra

    В Kubernetes мы можем использовать для хранения данных PersistentVolume. Благодаря отработанным механизмам, работать с данными в Kubernetes с каждым годом становится всё проще.


    Каждому pod’у с Cassandra мы выделим свой PersistentVolume

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

    Другой вопрос, если вы хотите создавать для каждой feature-ветки отдельное окружение для разработчиков. В этом случае правильным подходом будет поднимать один узел Cassandra, а данные хранить в распределенном хранилище, т.е. упомянутые Ceph и GlusterFS станут вашей опцией. Тогда разработчик будет уверен, что не потеряет тестовые данные даже при потере одного из узлов Kuberntes-кластера.

    2. Мониторинг


    Практически безальтернативным выбором для реализации мониторинга в Kubernetes является Prometheus (подробно мы об этом рассказывали в соответствующем докладе). Как дела у Cassandra с экспортерами метрик для Prometheus? И, что в чем-то даже главнее, с подходящими к ним dashboard’ами для Grafana?


    Пример внешнего вида графиков в Grafana для Cassandra

    Экспортеров всего два: jmx_exporter и cassandra_exporter.

    Мы выбрали для себя первый, потому что:

    1. JMX Exporter растет и развивается, в то время как Cassandra Exporter не смог получить должной поддержки сообщества. Cassandra Exporter до сих пор не поддерживает большинство версий Cassandra.
    2. Можно запустить его как javaagent при помощи добавления флага -javaagent:<plugin-dir-name>/cassandra-exporter.jar=--listen=:9180.
    3. Для него есть адекватный dashboad, который несовместим с Cassandra Exporter.

    3. Выбор примитивов Kubernetes


    Согласно вышеизложенной структуре кластера Cassandra, попробуем перевести всё, что там описано, в терминологию Kubernetes:

    • Cassandra Node → Pod
    • Cassandra Rack → StatefulSet
    • Cassandra Datacenter → пул из StatefulSets
    • Cassandra Cluster → ???

    Получается, что не хватает какой-то дополнительной сущности, чтобы управлять всем кластером Cassandra сразу. Но если чего-то нет, мы можем это создать! В Kubernetes для этого предназначен механизм определения собственных ресурсов — Custom Resource Definitions.


    Объявление дополнительных ресурсов для логов и оповещений

    Но сам по себе Custom Resource ничего не значит: ведь для него нужен контроллер. Возможно, придется прибегнуть к помощи Kubernetes-оператора

    4. Идентификациия pod’ов


    Пунктом выше мы согласились, что один узел Cassandra будет равняться одному pod’у в Kubernetes. Но IP-адреса у pod’ов каждый раз будут разными. А идентификация узла в Cassandra происходит именно на основе IP-адреса… Получается, что после каждого удаления pod’а кластер Cassandra будет добавлять в себя новый узел.

    Выход есть, и даже не один:

    1. Мы можем вести учет по идентификаторам хостов (UUID’ам, однозначно идентифицирующим экземпляры Cassandra) или по IP-адресам и сохранять это всё в каких-то структурах/таблицах. У метода два основных недостатка:

      • Риск возникновения условия гонки при падении сразу двух узлов. После поднятия узлы Cassandra одновременно пойдут запрашивать для себя IP-адрес из таблицы и конкурировать за один и тот же ресурс.
      • Если узел Cassandra потерял свои данные, мы больше не сможем его идентифицировать.
    2. Второе решение кажется небольшим хаком, но тем не менее: мы можем создавать Service с ClusterIP для каждого узла Cassandra. Проблемы этой реализации:

      • Если в кластере Cassandra очень много узлов, нам придется создать очень много Service’ов.
      • Возможность ClusterIP реализована через iptables. Это может стать проблемой, если в кластере Cassandra много (1000… или даже 100?) узлов. Хотя балансировка на базе IPVS способна решить эту проблему.
    3. Третье решение — использовать для узлов Cassandra сеть узлов вместо выделенной сети pod’ов при помощи включения настройки hostNetwork: true. Данный метод накладывает определённые ограничения:

      • На замену узлов. Нужно, чтобы новый узел обязательно имел тот же IP-адрес, что и предыдущий (в облаках вроде AWS, GCP это сделать практически невозможно);
      • Используя сеть узлов кластера, мы начинаем конкурировать за сетевые ресурсы. Следовательно, выложить на один узел кластера более одного pod’а с Cassandra будет проблематично.

    5. Бэкапы


    Мы хотим сохранять полную версию данных одного узла Cassandra по расписанию. Kubernetes предоставляет удобную возможность с использованием CronJob, но тут палки в колеса нам вставляет сама Cassandra.

    Напомню, что часть данных Cassandra хранит в памяти. Чтобы сделать полный бэкап, нужно данные из памяти (Memtables) перенести на диск (SSTables). В этот момент узел Cassandra перестает принимать соединения, полностью выключаясь из работы кластера.

    После этого снимается бэкап (snapshot) и сохраняется схема (keyspace). И тут выясняется, что просто бэкап нам ничего не дает: нужно сохранить идентификаторы данных, за которые отвечал узел Cassandra, — это специальные токены.


    Распределение токенов для идентификации, за какие данные отвечают узлы Cassandra

    Пример скрипта для снятия бэкапа Cassandra от Google в Kubernetes можно найти по этой ссылке. Единственный момент, который скрипт не учитывает, — это сброс данных на узел перед снятием snapshot’а. То есть бэкап выполняется не для текущего состояния, а состояния чуть ранее. Но это помогает не выводить узел из работы, что видится очень логичным.

    set -eu
    
    if [[ -z "$1" ]]; then
      info "Please provide a keyspace"
      exit 1
    fi
    
    KEYSPACE="$1"
    
    result=$(nodetool snapshot "${KEYSPACE}")
    
    if [[ $? -ne 0 ]]; then
      echo "Error while making snapshot"
      exit 1
    fi
    
    timestamp=$(echo "$result" | awk '/Snapshot directory: / { print $3 }')
    
    mkdir -p /tmp/backup
    
    for path in $(find "/var/lib/cassandra/data/${KEYSPACE}" -name $timestamp); do
      table=$(echo "${path}" | awk -F "[/-]" '{print $7}')
      mkdir /tmp/backup/$table
      mv $path /tmp/backup/$table
    done
    
    
    tar -zcf /tmp/backup.tar.gz -C /tmp/backup .
    
    nodetool clearsnapshot "${KEYSPACE}"

    Пример bash-скрипта для снятия бэкапа с одного узла Cassandra

    Готовые решения для Cassandra в Kubernetes


    Что вообще сейчас используют для разворачивания Cassandra в Kubernetes и что из этого больше всего подходит под заданные требования?

    1. Решения на базе StatefulSet или Helm-чартов


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

    Обычно это работает нормально… пока не случится что-то неожиданное — например, выход узла из строя. Стандартные средства Kubernetes просто не могут учесть все вышеописанные особенности. Кроме того, данный подход очень ограничен в том, насколько он может быть расширен для более сложного использования: замены узлов, резервного копирования, восстановления, мониторинга и т.д.

    Представители:


    Оба чарта одинаково хороши, но при этом подвержены описанным выше проблемам.

    2. Решения на базе Kubernetes Operator


    Такие опции более интересны, потому что предоставляют широкие возможности по управлению кластером. Для проектирования оператора Cassandra, как и любой другой базы данных, хороший паттерн выглядит как Sidecar <-> Controller <-> CRD:


    Схема управления узлами в правильно спроектированном операторе Cassandra

    Рассмотрим существующие операторы.

    1. Cassandra-operator от instaclustr


    • GitHub
    • Готовность: Alpha
    • Лицензия: Apache 2.0
    • Реализован на: Java

    Это действительно очень многообещающий и активно развивающийся проект от компании, которая предлагает управляемые развертывания Cassandra. Он, как и описано выше, использует sidecar-контейнер, который принимает команды через HTTP. Написан на Java, поэтому иногда ему не хватает более продвинутой функциональности библиотеки client-go. Также оператор не поддерживает разные Racks для одного Datacenter.

    Зато у оператора есть такие плюсы, как поддержка мониторинга, высокоуровневого управления кластером при помощи CRD и даже документация по снятию бэкапов.

    2. Navigator от Jetstack


    • GitHub
    • Готовность: Alpha
    • Лицензия: Apache 2.0
    • Реализован на: Golang

    Оператор, предназначенный для развертывания DB-as-a-Service. На данный момент поддерживает две базы данных: Elasticsearch и Cassandra. Имеет в себе такие интересные решения, как контроль доступа к базе данных через RBAC (для этого поднимается свой отдельный navigator-apiserver). Интересный проект, к которому стоило бы присмотреться, однако последний коммит был сделан полтора года назад, что явно снижает его потенциал.

    3. Cassandra-operator от vgkowski


    • GitHub
    • Готовность: Alpha
    • Лицензия: Apache 2.0
    • Реализован на: Golang

    Рассматривать его «всерьёз» не стали, так как последний коммит в репозиторий был больше года назад. Разработка оператора заброшена: последняя версия Kubernetes, заявленная как поддерживаемая, — это 1.9.

    4. Cassandra-operator от Rook


    • GitHub
    • Готовность: Alpha
    • Лицензия: Apache 2.0
    • Реализован на: Golang

    Оператор, развитие которого идет не так быстро, как хотелось бы. Имеет продуманную структуру CRD для управления кластером, решает проблему с идентификацией узлов при помощи Service с ClusterIP (тот самый «хак»)… но пока что это всё. Мониторинга и бэкапов из коробки сейчас нет (кстати, за мониторинг мы взялись сами). Интересный момент, что при помощи этого оператора можно также развернуть ScyllaDB.

    NB: Данный оператор с небольшими доработками мы использовали в одном из наших проектов. Проблем в работе оператора за все время эксплуатации (~4 месяца работы) замечено не было.

    5. CassKop от Orange


    • GitHub
    • Готовность: Alpha
    • Лицензия: Apache 2.0
    • Реализован на: Golang

    Самый молодой оператор в списке: первый коммит был сделан 23 мая 2019 года. Уже сейчас он имеет в своем арсенале большое количество фич из нашего списка, подробнее с которыми можно ознакомиться в репозитории проекта. Оператор построен на базе популярного operator-sdk. Поддерживает мониторинг «из коробки». Главным отличием от других операторов является использование плагина CassKop, реализованного на Python и используемого для коммуникации между узлами Cassandra.

    Выводы


    Количество подходов и возможных вариантов переноса Cassandra в Kubernetes говорит само за себя: тема востребована.

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

    Думаю, в будущем эта женщина на корабле придется к месту!

    P.S.


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

    Флант
    674,02
    Специалисты по DevOps и Kubernetes
    Поделиться публикацией

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

      +1
      Говоря о миграции Cassandra в Kubernetes, мы надеемся, что с переездом управлять ей станет удобнее.


      Можно поподробнее, в чем улучшилось удобство управления после миграции?
        +2
        Конкретно в нашем случае — удобнее готовить новый узел. Инфраструктура в наших kubernetes кластерах выстроена так, что при добавлении ноды мы автоматически получаем всё необходимое: настройку системных параметров, мониторинг и т. д.
        Чтобы на этом (уже готовом для работы) узле оказалась Cassandra, зачастую нужно сделать только две вещи:
        1. Определиться, где будут лежать данные.
        2. Увеличить количество нод в statefulset’е (или другой абстракции оператора) на 1.
          +1
          Чтобы «однообразно» ставить Кассандру «с нуля» на сервер нужен маленький скрипт. А если запускать ее в обычном docker (или podman) — этот скрипт будет еще меньше. При этом не нужно искать решения для проблем типа таких:

          Пунктом выше мы согласились, что один узел Cassandra будет равняться одному pod’у в Kubernetes. Но IP-адреса у pod’ов каждый раз будут разными. А идентификация узла в Cassandra происходит именно на основе IP-адреса… Получается, что после каждого удаления pod’а кластер Cassandra будет добавлять в себя новый узел.

          Выход есть, и даже не один


          На данном этапе пробовать что-то из вышеописанного можно на свой страх и риск: ни один из разработчиков не гарантирует 100%-ую работу своего решения в production-среде.


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

          Но для production, получается, сыровата технология?
            0
            Не достаточно отточена для production, я бы сказал.
        0
        В статье подразумевается, что это все будет в одном экземпляре кубера? Как насчет multiDC и K8S?
          0
          multiDC можно достичь заведением нод kubernetes кластера в нескольких DC. Повесить на ноды специфичные для DC label’ы/taint’ы. Указать для cassandra toleration и affinity. Для этого, как указано в статье, нам придется использовать более чем один statefulset. Практически все операторы это вопрос решают.

          Возникает вопрос по поводу того, как гарантировать доступность master нод, но это уже другая история :)
            0

            Доступность master нод? До сих пор был уверен, что у Кассандры все ноды равноправные.

              0
              Имелась ввиду доступность master нод kubernetes кластера. Если master нода только в одном dc, могут возникнуть проблемы.
          +3
          Больше похоже про сову на глобус. Какая цель всего этого? Такого удобства управления явно не достаточно для перехода с 100+ железных нод.
            0
            Чтобы осуществлять переезд с одной инфраструктуры на другую, нужно четко понимать зачем.
            Если есть понимание, что переезд будет натягиванием совы на глобус — лучше оставить птичку в покое.
            Суть статьи — помочь с пониманием нюансов, если кто-то захочет перевезти свою Cassandra в Kubernetes.

            P.s.: В ходе написания статьи ни одна сова не пострадала.
              +1
              «Чтобы осуществлять переезд с одной инфраструктуры на другую, нужно четко понимать зачем.» — вот это и хочется понять, зачем?
              Или Вы это делаете, без цели, просто для общего развития…
                +4
                Если вопрос конкретно про нас, то мы обслуживаем большое число Kubernetes-кластеров, где Cassandra, например, используется для инсталляций Jaeger, поэтому удобно (эффективно для управления), чтобы всё было сделано унифицировано. Другой кейс — уже упомянут выше, когда регулярно (и автоматически) нужно выкатывать однотипные окружения (для целей разработки), имеющие в своём составе Cassandra. Какие потребности у других — может быть, они расскажут об этом сами. Статья написана не для ответа на вопрос, «зачем», а о том, «как» это можно делать на сегодняшний день (и для тех, кому это нужно уже сейчас или в скором времени).

                Призывов проводить миграцию (тем более — как самоцель) здесь нет.
            +3
            Напомню, что часть данных Cassandra хранит в памяти. Чтобы сделать полный бэкап, нужно данные из памяти (Memtables) перенести на диск (SSTables). В этот момент узел Cassandra перестает принимать соединения, полностью выключаясь из работы кластера.

            это не так, возможно вы делаете что то не то. ни nodetool flush ( сброс memtables на диск ) ни nodetool snapshot (что есть сброс на диск + создание хардлинков на текущее состоянии данных на диске ) не вызывают остановку ноды кассандры.
            возможно, вы пытаетесь сделать nodetool drain, что есть flush + остановка сетевых сервисов ноды кассандры. но для целей бакапа это бессмысленно.
            обычно nodetool snapshot и перемещение получившихся хардлинков из поддиректорий snapshots должно быть достаточно для бакапа и последующего восстановления ( если нода в которую идет восстановление имеет тот же ip ).
              0
              Да, всё верно, тут хорошее замечание. В статье есть скрипт с примером бекапов. Он делает как раз то, что вы описали. Если не делать nodetool drain, все должно пройти гладко.
              +4
              Также, всевозможные варианты бакапов и восстановлений хорошо описаны тут: thelastpickle.com/blog/2019/11/05/cassandra-medusa-backup-tool-is-open-source.html
                0
                Спасибо! Упустил из виду. Вышла уже после того, как я закончил основную часть статьи. Ознакомлюсь.

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

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