Базы данных и Kubernetes (обзор и видео доклада)

8 ноября в главном зале конференции HighLoad++ 2018, в рамках секции «DevOps и эксплуатация», прозвучал доклад «Базы данных и Kubernetes». В нём рассказывается о высокой доступности баз данных и подходах к отказоустойчивости до Kubernetes и вместе с ним, а также практических вариантах размещения СУБД в кластерах Kubernetes и существующие для этого решения (включая Stolon для PostgreSQL).

oqmhkpy4pxg-olk9yybf_julwvu.jpeg

По традиции рады представить видео с докладом (около часа, гораздо информативнее статьи) и основную выжимку в текстовом виде. Поехали!

Теория


Этот доклад появился как ответ на один из самых популярных вопросов, что все последние годы нам неустанно задают в разных местах: комментариях на хабре или YouTube, социальных сетях и т.п. Звучит он просто: «Можно ли запустить базу в Kubernetes?», — и если мы обычно отвечали на него «в целом да, но…», то пояснений к этим «в целом» и «но» явно не хватало, а уместить их в короткое сообщение никак не удавалось.

Однако для начала обобщу вопрос от «базы [данных]» до stateful в целом. СУБД — это лишь частный случай stateful-решений, более полный список которых можно представить так:

pxps2_ff80ru5qfth8bduiu_kdw.png

Перед тем, как смотреть конкретные случаи, расскажу о трёх важных особенностях работы/использования Kubernetes.

1. Философия высокой доступности в Kubernetes


Все знают аналогию «питомцы против стада» (pets vs cattle) и понимают, что если Kubernetes — история из мира стада, то классические СУБД — это именно питомцы.

И как выглядела архитектура «питомцев» в «традиционном» варианте? Классический пример инсталляции MySQL — репликация на двух железных серверах с резервированным питанием, диском, сетью… и всем остальным (включая инженера и различные вспомогательные средства), что поможет нам быть уверенными, что процесс MySQL не упадёт, а если случится проблема с любым из критичных для него компонентов, отказоустойчивость будет соблюдена:

0mqgs3e3cco1zukmlah1jg2ubjs.png

Как то же самое будет выглядеть в Kubernetes? Здесь обычно гораздо больше железных серверов, они проще и у них нет резервированного питания и сети (в том смысле, что выпадание одной машины ни на что не влияет) — всё это объединено в кластер. Его отказоустойчивость обеспечивается софтом: если что-то происходит с узлом, Kubernetes это обнаруживает и запускает нужные экземпляры на другом узле.

Какие есть механизмы для высокой доступности в K8s?

n2gwmhiogm3uzv1m5igrzqpyifq.png

  1. Контроллеры. Их много, но основных два: Deployment (для stateless-приложений) и StatefulSet (для stateful-приложений). В них хранится вся логика действий, предпринимаемых в случае падения узла (недоступности pod’а).
  2. PodAntiAffinity — возможность указывать определённым pod’ам, чтобы они не находились на одном и том же узле.
  3. PodDisruptionBudgets — лимит на количество экземпляров pod’ов, которые можно одновременно выключить в случае плановых работ.


2. Гарантии согласованности в Kubernetes


Как работает привычная схема отказоустойчивости с одним мастером? Два сервера (master и standby), к одному из которых постоянно обращается приложение, которым в свою очередь пользуются через балансировщик нагрузки. Что происходит в случае сетевой проблемы?

6p1kvelnrzyrphtu6sb_agftgaa.gif

Классический split-brain: приложение начинает обращаться к обоим экземплярам СУБД, каждый из которых считает себя главным. Чтобы избежать этого, keepalived заменялся на corosync уже с тремя его экземплярами для достижения кворума при голосовании за мастера. Однако даже в таком случае есть проблемы: если отвалившийся экземпляр СУБД всячески пытается «самоубиться» (убрать IP-адрес, перевести БД в read-only…), то другая часть кластера не знает, что произошло с мастером — ведь может случиться и так, что тот узел в действительности всё ещё работает и к нему попадают запросы, а значит, что переключать мастера мы ещё не можем.

Чтобы решить такую ситуацию, существует механизм изоляции узла с целью защиты всего кластера от некорректной работы, — этот процесс называют fencing. Практическая суть сводится к тому, что мы пытаемся какими-либо внешними способами «убить» отвалившуюся машину. Подходы могут быть разные: от выключения машины по IPMI и блокирования порта на свитче до обращения к API облачного провайдера и т.п. И только после этой операции можно переключать мастера. Так достигается гарантия at-most-once, которая обеспечивает нам согласованность (consistency).

slxb9rwf-lk8mcdaeqiuankz3z8.png

Как добиться такого же в Kubernetes? Для этого есть уже упомянутые контроллеры, поведение которых в случае недоступности узла различается:

  1. Deployment: «Мне сказали, что должно быть 3 pod’а, а теперь их всего 2 — я создам новый»;
  2. StatefulSet: «Pod’а не стало? Подожду: либо этот узел вернётся, либо нам скажут его убить», — т.е. контейнеры сами (без действий оператора) не пересоздаются. Именно так достигается та же самая гарантия at-most-once.


Однако и здесь, в последнем случае, требуется fencing: нужен механизм, который подтвердит, что этого узла точно больше нет. Сделать его автоматическим, во-первых, очень сложно (требуется множество реализаций), а во-вторых, что ещё хуже, он обычно убивает узлы медленно (обращение к IPMI может занять секунды или десятки секунд, а то и минуты). Мало кого устроит ожидание в минуту для переключения базы на нового мастера. Но есть другой подход, не требующий механизма fencing’а…

Начну его описание вне Kubernetes. В нём используется особый балансировщик нагрузки (load balancer), через который бэкенды обращаются к СУБД. Его специфика заключается в том, что он обладает свойством согласованности, т.е. защитой от сетевых сбоев и split-brain, поскольку позволяет убрать все подключения к текущему мастеру, дождаться синхронизации (реплики) на другом узле и переключиться на него. Я не нашёл устоявшегося термина для этого подхода и назвал его Consistent Switchover.

ctoqlkp3efyctjlvsyiheesk2s4.gif

Главный вопрос с ним заключается в том, как сделать его универсальным, обеспечив поддержку и облачных провайдеров, и частных инсталляций. Для этого к приложениям добавляются прокси-серверы. Каждый из них будет принимать запросы от своего приложения (и пересылать их в СУБД), а из всех вместе будет собран кворум. Как только происходит отказ какой-то части кластера, те прокси, что потеряли кворум, сразу убирают свои подключения к СУБД.

njy0-tahnwp8uxeyevxot_tlntq.png

3. Хранение данных и Kubernetes


Основной механизм — это сетевой диск Network Block Device (он же SAN) в разных реализациях для нужных вариантов облака или bare metal. Однако поместить нагруженную базу данных (например, MySQL, от которой требуется 50 тысяч IOPS) в облако (AWS EBS) из-за имеющейся задержки (latency) не получится.

qqwpr7fae-nbek0y2o6ex9lbzsa.png

В Kubernetes для таких случаев есть возможность подключения локального жёсткого диска — Local Storage. Если происходит сбой (диск перестаёт быть доступным в pod’е), то мы вынуждены чинить эту машину — по аналогии с классической схемой в случае отказа одного надёжного сервера.

Оба варианта (Network Block Device и Local Storage) относятся к категории ReadWriteOnce: хранилище невозможно примонтировать в два места (pod’а) — для такого масштабирования потребуется создать новый диск и подключить его к новому pod’у (для этого есть встроенный механизм K8s), а затем наполнить нужными данными (делается уже нашими силами).

Если нам нужен режим ReadWriteMany, то доступны реализации Network File System (или NAS): для публичного облака это AzureFile и AWSElasticFileSystem, а для своих инсталляций — CephFS и Glusterfs для любителей распределённых систем, а также NFS.

eqy6gpf2duz9ljtzc342bj9upg4.png

Практика


1. Standalone


Этот вариант про случай, когда ничто не мешает запустить СУБД в режиме отдельного сервера с локальным хранилищем. Тут не идёт речь о высокой доступности… хотя и она может быть в некоторой мере (т.е. достаточно для данного применения) реализована на уровне железа. Есть множество случаев для такого применения. В первую очередь это всевозможные staging- и dev-окружения, но не только: сюда же попадают и вторичные сервисы, отключение которых на 15 минут не является критичным. В Kubernetes это реализуется StatefulSet'ом с одним pod’ом:

hlxkhaodepe50imvuobtoilrz6o.png

В целом это жизнеспособный вариант, у которого, с моей точки зрения, нет минусов по сравнению с установкой СУБД на отдельной виртуалке.

2. Реплицированная пара с ручным переключением


Снова используется StatefulSet, но общая схема выглядит уже следующим образом:

vq_itonyvigrh0uezqek_lwtlt4.png

Если произошёл сбой у одного из узлов (mysql-a-0), чуда не происходит, но у нас есть реплика (mysql-b-0), на которую мы можем переключить трафик. При этом — ещё до переключения трафика — важно не забыть не только убрать запросы к СУБД от сервиса mysql, но и зайти на СУБД вручную и убедиться, что все соединения завершены (убить их), а также зайти на второй узел с СУБД и перенастроить реплику в обратную сторону.

Если вы сейчас используете классический вариант с двумя серверами (master + standby) без автоматического переключения (failover), то это решение — эквивалент в Kubernetes. Подходит для MySQL, PostgreSQL, Redis и других продуктов.

3. Масштабирование нагрузки на чтение


По сути этот случай не является stateful, потому речь идёт только про чтение. Здесь основной сервер СУБД находится вне рассматриваемой схемы, а в рамках Kubernetes создаётся «ферма из slave-серверов», доступных только на чтение. Общий механизм — использование init-контейнеров для наполнения данными СУБД на каждом новом pod’е этой фермы (с помощью горячего дампа или обычного с дополнительными действиями и т.п. — зависит от используемой СУБД). Чтобы быть уверенным, что каждый экземпляр не отстаёт слишком сильно от мастера, можно использовать liveness-пробы.

nzpdukumat3zbax7vnkcs5jtsvs.png

4. Умный клиент


Если сделать StatefulSet из трёх memcached, в Kubernetes доступен специальный сервис, который будет не балансировать запросы, а создаст каждому pod’у по своему домену. С ними сможет работать клиент в том случае, если он сам умеет шардинг и репликацию.

За примером далеко ходить не надо: так «из коробки» работает хранение сессий в PHP. На каждый запрос сессии делаются запросы одновременно на все серверы, после чего из них отбирается самый актуальный ответ (аналогично и на запись).

t8iz261adru0mbdw7cd2o5i3y8o.png

5. Cloud Native-решения


Есть множество решений, изначально ориентированных на выход из строя узлов, т.е. они сами умеют делать отказоустойчивость (failover) и восстановление узлов (recovery), предоставляют гарантии согласованности (consistency). Это не полный их список, а лишь часть популярных примеров:

9uahqzayfbdsokgyod153jwfjfs.png

Все они попросту ставятся в StatefulSet, после чего узлы находят друг друга и образуют кластер. Сами же продукты отличаются тем, как в них реализованы три вещи:

  1. Как узлы узнают друг о друге? Для этого есть такие способы, как Kubernetes API, DNS-записи, статическая конфигурация, специализированные узлы (seed), сторонний service discovery…
  2. Как подключается клиент? Через балансировщик нагрузки, распределяющий по хостам, или же клиенту нужно знать обо всех хостах, а он сам решит, как действовать дальше.
  3. Как делается горизонтальное масштабирование? Никак, полноценно или сложно/с ограничениями.


Вне зависимости от выбранных путей решения этих вопросов, все такие продукты хорошо работают с Kubernetes, потому что изначально были созданы как «стадо» (cattle).

6. Stolon PostgreSQL


Stolon фактически позволяет превратить СУБД PostgreSQL, созданную как pet, в cattle. За счёт чего это достигается?

g_z4pcehfw6p985duphcgrbukzo.png

  • Во-первых, нужен service discovery, в роли которого может быть etcd (доступны и другие варианты) — кластер из них помещается в StatefulSet.
  • Другая часть инфраструктуры — StatefulSet с экземплярами PostgreSQL. Кроме собственно СУБД рядом с каждой инсталляцией также помещается компонент под названием keeper, который осуществляет настройку СУБД.
  • Другой компонент — sentinel — разворачивается как Deployment и следит за конфигурацией кластера. Именно он решает, кто будет master и standby, записывает эту информацию в etcd. А keeper читает данные из etcd и выполняет соответствующие актуальному статусу действия с экземпляром PostgreSQL.
  • Ещё один компонент, разворачиваемый в Deployment и стоящий перед экземплярами PostgreSQL, — proxy — является реализацией уже упомянутого паттерна Consistent Switchover. Эти компоненты подключаются к etcd, а если эта связь теряется, то proxy сразу убивает исходящие соединения, потому что с этого момента он не знает роль своего сервера (сейчас это master или standby?).
  • Наконец, перед экземплярами proxy стоит обычный LoadBalancer из Kubernetes.


Выводы


Так можно ли базу в Kubernetes? Да, конечно, можно, в некоторых случаях… И если это целесообразно, то делается вот так (см. схему работы Stolon)…

Все знают, что технологии развиваются волнами. Изначально любое новое устройство бывает очень сложным в использовании, но со временем всё меняется: технология становится доступной. К чему идём? Да, оно останется вот таким внутри, но как оно будет работать, мы уже не будем знать. В Kubernetes активно развиваются операторы. Пока их не так много и они не так хороши, но есть движение в этом направлении.

Видео и слайды


Видео с выступления (около часа):

Презентация доклада:

P.S. Мы также нашли в сети очень (!) краткую текстовую выжимку с этого доклада — спасибо за неё Николаю Волынкину.

P.P. S.


Другие доклады в нашем блоге:
Вероятно, вас также заинтересуют следующие публикации:

© Habrahabr.ru