Міграція Cassandra в Kubernetes: особливості та рішення
З базою даних Apache Cassandra та необхідністю її експлуатації в рамках інфраструктури на базі Kubernetes ми зустрічаємося регулярно. У цьому матеріалі поділимося своїм баченням необхідних кроків, критеріїв та існуючих рішень (включаючи огляд операторів) для міграції Cassandra до K8s.
«Хто може керувати жінкою, впорається і з державою»
Хто ж така Cassandra? Це розподілена система зберігання, призначена для управління великими обсягами даних, що забезпечує високу доступність без жодної точки відмови. Проект навряд чи потребує довгої вистави, тому наведу лише основні особливості Cassandra, що будуть актуальні в розрізі конкретної статті:
Кассандра написана на Java.
Топологія Cassandra включає декілька рівнів:
Node – один розгорнутий екземпляр Cassandra;
Rack - група екземплярів Cassandra, об'єднаних за якоюсь ознакою, що знаходиться в одному дата-центрі;
Datacenter - сукупність всіх груп екземплярів Cassandra, що знаходяться в одному дата-центрі;
Cluster – сукупність усіх дата-центрів.
Для ідентифікації вузла Cassandra використовує IP-адресу.
Для швидкості операцій запису та читання частина даних Cassandra зберігає в оперативній пам'яті.
Тепер — до потенційного переїзду в Kubernetes.
Check-list для перенесення
Говорячи про міграцію Cassandra до Kubernetes, ми сподіваємося, що з переїздом керувати їй стане зручніше. Що для цього буде потрібно, що в цьому допоможе?
1. Сховище для даних
Як уже уточнювалося, частина даних Cassanda зберігає в оперативній пам'яті Memtable. Але є й інша частина даних, що зберігається на диск, - у вигляді SSTable. До цих даних додається сутність Журнал фіксації — записи про всі транзакції, які також зберігаються на диск.
Схема транзакцій запису до 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 не зміг отримати належної підтримки спільноти. Cassandra Exporter досі не підтримує більшість версій Cassandra.
Можна запустити його як javaagent за допомогою додавання прапора -javaagent:<plugin-dir-name>/cassandra-exporter.jar=--listen=:9180.
Згідно з вищевикладеною структурою кластера Cassandra, спробуємо перевести все, що там описано, у термінологію Kubernetes:
Cassandra Node → Pod
Касандра Рак → 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 додаватиме новий вузол.
Вихід є, і навіть не один:
Ми можемо вести облік за ідентифікаторами хостів (UUID'ам, які однозначно ідентифікують екземпляри Cassandra) або за IP-адресами і зберігати це все в якихось структурах/таблицях. У методу два основних недоліки:
Ризик виникнення умови гонки під час падіння відразу двох вузлів. Після підняття вузли Cassandra одночасно підуть вимагати для себе IP-адресу з таблиці і конкурувати за той самий ресурс.
Якщо вузол Cassandra втратив дані, ми більше не зможемо його ідентифікувати.
Друге рішення здається невеликим хаком, проте: ми можемо створювати Service з ClusterIP для кожного вузла Cassandra. Проблеми цієї реалізації:
Якщо в кластері Cassandra дуже багато вузлів, нам доведеться створити дуже багато Service'ів.
Можливість ClusterIP реалізована через iptables. Це може стати проблемою, якщо в кластері Cassandra багато (1000… чи навіть 100?) вузлів. Хоча балансування на базі IPVS здатна вирішити цю проблему.
Третє рішення - використовувати для вузлів Cassandra мережу вузлів замість виділеної мережі pod'ів за допомогою увімкнення налаштування hostNetwork: true. Цей метод накладає певні обмеження:
На заміну вузлів. Потрібно, щоб новий вузол обов'язково мав ту саму IP-адресу, що й попередня (у хмарах на кшталт AWS, GCP це зробити практично неможливо);
Використовуючи мережу вузлів кластера, ми починаємо конкурувати за мережеві ресурси. Отже, викласти на один вузол кластера більше одного pod'а з Cassandra буде проблематично.
5. Бекапи
Ми хочемо зберігати повну версію даних одного вузла Cassandra за розкладом. Kubernetes надає зручну можливість з використанням CronJobАле тут палиці в колеса нам вставляє сама Cassandra.
Нагадаю, що частина даних Cassandra зберігає у пам'яті. Щоб зробити повний бекап, потрібні дані з пам'яті (Memtables) перенести на диск (SSTables). У цей момент вузол Cassandra перестає приймати з'єднання, повністю виключаючись із роботи кластера.
Після цього знімається бекап (знімок) та зберігається схема (простір клавіш). І тут з'ясовується, що просто бекап нам нічого не дає: потрібно зберегти ідентифікатори даних, за які відповідав вузол 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
Це дійсно дуже перспективний проект, що активно розвивається від компанії, яка пропонує керовані розгортання Cassandra. Він, як і описано вище, використовує sidecar-контейнер, який приймає команди через HTTP. Написаний на Java, тому іноді йому не вистачає більш функціональності бібліотеки client-go. Також оператор не підтримує різні Racks для одного Datacenter.
Зате оператор має такі плюси, як підтримка моніторингу, високорівневого управління кластером за допомогою CRD і навіть документація зі зняття бекапів.
Оператор призначений для розгортання DB-as-a-Service. На даний момент підтримує дві бази даних: Elasticsearch і Cassandra. Має такі цікаві рішення, як контроль доступу до бази даних через RBAC (для цього піднімається свій окремий navigator-apiserver). Цікавий проект, до якого варто було б придивитися, проте останній коміт було зроблено півтора роки тому, що явно знижує його потенціал.
Розглядати його «всерйоз» не стали, оскільки останній коміт у репозиторій був понад рік тому. Розробка оператора занедбана: остання версія Kubernetes, заявлена як підтримувана, це 1.9.
Оператор, розвиток якого йде не так швидко, як хотілося б. Має продуману структуру CRD для управління кластером, вирішує проблему з ідентифікацією вузлів за допомогою Service з ClusterIP (той самий «хак»)… але поки що це все. Моніторингу та бекапів із коробки зараз немає (до речі, за моніторинг ми взялися самі). Цікавий момент, що за допомогою цього оператора можна розгорнути ScyllaDB.
NB: Цей оператор з невеликими доопрацюваннями ми використовували в одному з наших проектів. Проблем у роботі оператора за весь час експлуатації (~4 місяці роботи) помічено не було.
Наймолодший оператор у списку: перший коміт було зроблено 23 травня 2019 року. Вже зараз він має у своєму арсеналі велику кількість фіч із нашого списку, докладніше з якими можна ознайомитись у репозиторії проекту. Оператор збудований на базі популярного operator-SDK. Підтримує моніторинг "з коробки". Головною відмінністю від інших операторів є використання плагіна CassKop, реалізований на Python і використовується для комунікації між вузлами Cassandra.
Висновки
Кількість підходів та можливих варіантів перенесення Cassandra у Kubernetes говорить сама за себе: тема затребувана.
На даному етапі пробувати щось із вищеописаного можна на свій страх і ризик: жоден з розробників не гарантує 100% роботу свого рішення в production-середовищі. Але вже зараз багато продуктів виглядають багатообіцяюче, щоб спробувати використовувати їх у стендах для розробки.
Думаю, у майбутньому ця жінка на кораблі прийдеться до місця!