image

Собираетесь ли вы развертывать базу данных в кластере Kubernetes? Если так – то это отличный выбор. Kubernetes – это инструмент оркестрации контейнеров, который при помощи множества инструментов координирует эксплуатацию приложений в контейнерах (подах). Один из этих контроллеров называется StatefulSet и используется для эксплуатации приложений, сохраняющих состояние.

Развертывание приложений с сохранением состояния в кластере Kubernetes порой бывает утомительной задачей. Дело в том, что приложение, сохраняющее состояние ожидает, что придется работать с архитектурой «ведущий-ведомый» (primary-replica), а имя пода будет фиксированным. Контроллер StatefulSets решает эту проблему, развертывая в кластере Kubernetes приложение с сохранением состояния.

Из этой статьи вы подробнее узнаете, что представляют собой StatefulSet в кластере Kubernetes, и в каких ситуациях из использовать, а также как развернуть приложение с сохранением состояния при помощи контроллера StatefulSets. Все это будет рассмотрено в пошаговом примере, в который мы включим все файлы манифеста (YAML). Более того, концепция StatefulSets будет продемонстрирована при помощи базы данных MySQL, и мы заодно рассмотрим, как создать фиксированное имя пода для каждой репликации MySQL, а также как обращаться к реплицированным подам через объект Services.

Что такое приложения с сохранением состояния?


Это приложения, которые сохраняют данные и помогают их отслеживать. Все базы данных, в частности, MySQL, Oracle и PostgreSQL – это примеры приложений, сохраняющих состояние. С другой стороны, в приложениях без сохранения состояния данные долго не держатся. Примеры приложений без сохранения состояния — Node.js и Nginx. Если состояние в приложении не сохраняется, то на каждый запрос приложение будет получать новые данные и обрабатывать их.

В современных веб-приложениях такие приложения без сохранения состояния соединяются с приложениями, сохраняющими состояние, чтобы обслужить пользовательский запрос. Приложение Node.js не сохраняет состояние, оно получает новые данные при каждом запросе, поступающем от пользователя. Далее это приложение соединяется для обработки данных с другим, сохраняющим состояние, например, с базой данных MySQL. База данных MySQL сохраняет данные и продолжает их обновлять, исходя из пользовательского запроса.

image

Далее подробнее поговорим о структурах StatefulSet в кластере Kubernetes — что они собой представляют, как ими пользоваться, и каковы наилучшие практики обращения с ними.

О структурах StatefulSet


StatefulSet – это контроллер Kubernetes, применяемый для эксплуатации сохраняющих состояние приложений в виде контейнеров (подов) в кластере Kubernetes. StatefulSet присваивают каждому поду идентификатор-липучку (sticky identity) – порядковый номер начиная с нуля — а не случайные ID каждой реплике пода. Новый под создается клонированием данных уже существовавшего пода. Если ранее существовавший под находился в ожидающем состоянии, то новый под создан не будет. Удаление подов происходит в обратном порядке, а не в случайном. Например, если у вас было четыре реплики, и в результате масштабирования их количество было сокращено до трех, то под номер 3 будет удален.

На приведенной ниже схеме показано, как поды нумеруются, начиная с нуля, и как в StatefulSet к поду прикрепляется том долговременного хранения данных.

image

Когда использовать StatefulSets


Есть несколько причин, по которым может быть целесообразно использовать StatefulSets. Рассмотрим два примера:

  1. Допустим, вы развернули базу данных MySQL в кластере Kubernetes и масштабировали ее до трех реплик, а клиентское приложение пытается получить доступ к кластеру MySQL, чтобы считывать и записывать данные. Запрос на считывание будет переадресовываться на три пода. Однако запрос на запись будет переадресовываться только на первый (ведущий) под, а записанные сюда данные будут синхронизироваться с другими подами. Это достижимо при помощи StatefulSets.
  2. Если удалить StatefulSet или отмасштабировать вниз, то не будут удалены тома, связанные с приложением, сохраняющим состояние. Так вашим данным обеспечивается безопасность. Если удалить или перезапустить под с MySQL, то вы сможете обращаться к данным из все того же тома, что и раньше.

Объект Deployment и StatefulSets


Также можно создавать поды (контейнеры), используя в кластере Kubernetes объект Deployment. Так вы сможете с легкостью реплицировать поды и прикреплять к подам тома, служащие в качестве хранилищ данных. То же самое можно проделать и при помощи StatefulSets. В чем же тогда преимущество работы со StatefulSets?

Дело в том, что подам, создаваемым при помощи объекта Deployment, присваиваются случайные ID. Например, вы создаете под с именем “my-app”, после чего расширяете его до трех реплик. Имена этих подов создаются примерно так:

my-app-123ab
my-app-098bd
my-app-890yt


Вслед за именем “my-app” добавляются случайные ID. Если под перезапускается, либо выполняется масштабирование вниз, то, опять же, объект Kubernetes Deployment присвоит иные случайные ID каждому поду. После перезапуска имена всех подов примут вид:

my-app-jk879
my-app-kl097
my-app-76hf7


Все эти поды ассоциированы с одним сервисом, выполняющим балансировку нагрузки. Поэтому в приложении, не сохраняющем состояния, изменения в именах подов легко идентифицируются, а сервисный объект легко обрабатывает случайные ID подов и распределяет нагрузку. Такой тип развертывания очень хорош с приложениями, не сохраняющими состояние.

image

Но вот приложения, сохраняющие состояние, таким образом развертывать невозможно. Приложению, сохраняющему состояние, требуется липкий идентификатор для каждого пода, поскольку поды-реплики не идентичны.

Рассмотрим развернутый инстанс базы данных MySQL. Предположим, вы создаете поды для базы данных MySQL при помощи объекта Kubernetes Deployment и масштабируете поды. Если вы записываете данные в один под MySQL, то ни в коем случае не реплицируйте те же данные на другом поде MySQL, если под перезапускается. Это первая проблема с объектом Kubernetes Deployment применительно к приложениям, сохраняющим состояние.

image

Приложениям, сохраняющим состояние, всегда нужен липкий идентификатор. Притом, что объект Kubernetes Deployment предлагает случайные ID для каждого пода, контроллер Kubernetes StatefulSets предлагает для каждого пода порядковый номер начиная с нуля, вида: mysql-0, mysql-1, mysql-2 и т. д.

image

Для приложений, сохраняющих состояние и работающих с контроллером StatefulSet, можно задать первый под в качестве ведущего, а остальные поды сделать репликами ведущего. Первый под будет обрабатывать поступающие от пользователя как на чтение, так и на запись, а прочие поды будут всегда синхронизироваться с первым для репликации данных. Если под погибает, то создается одноименный ему новый под.

На следующей схеме показана архитектура с ведущим инстансом MySQL и репликами, где предусмотрен том для долговременного хранения данных и система репликации данных.

image

Теперь добавим сюда еще один под. Четвертый под будет создан лишь в случае, если третий под активен и работает, и на четвертый под будут склонированы данные с существовавшего ранее пода.

image

Резюмируя, обозначим, что StatefulSets обеспечивают следующие преимущества по сравнению с объектами Deployment:

  1. Порядковые номера для каждого из подов.
  2. Первый под может выступать в качестве ведущего, благодаря чему хорошо подходит для подготовки конфигурации с реплицируемой базой данных – такая конфигурация позволяет обрабатывать как чтение, так и запись.
  3. Другие поды действуют в качестве реплик.
  4. Новые поды будут создаваться лишь в случае, если более ранний под сейчас действует, причем, данные более раннего пода клонируются.
  5. Поды удаляются в порядке, обратном тому, в котором создавались.

Как создать StatefulSet в Kubernetes


В этом разделе будет рассказано, как создать под для базы данных MySQL, воспользовавшись для этого контроллером StatefulSets.

Создаем секрет


Сперва нужно создать для приложения MySQL секрет, в котором будет храниться конфиденциальная информация, в частности, имена пользователей и пароли. Здесь я создаю простой секрет. Но при работе в продакшене рекомендуется использовать хранилище Vault от HashiCorp. Секрет для MySQL создается при помощи следующего кода:

apiVersion: v1
kind: Secret
metadata:
  name: mysql-password
type: opaque
stringData:
  MYSQL_ROOT_PASSWORD: password


Сохраните код в файле, который будет называться mysql-secret.yaml, и выполните код в вашем кластере Kubernetes при помощи следующего кода:

kubectl apply -f mysql-secret.yaml


Получите список секретов:

kubectl get secrets


Создаем приложение StatefulSet для MySQL


Прежде, чем создать приложение StatefulSet, проверьте ваши тома – для этого просто выведите список томов долговременного хранения:

kubectl get pv
NAME                   CAPACITY   ACCESS MODES   RECLAIM     STATUS
pvc-e0567   		10Gi       	RWO            	Retain           Bound


Далее выведите список заявок на тома долговременного хранения:

kubectl get pvc

NAME                      	      STATUS   VOLUME                 	    CAPACITY   ACCESS 
mysql-store-mysql-set-0   Bound    	pvc-e0567d43ffc6405b   10Gi       	RWO


Наконец, получим список классов хранилища:

kubectl get storageclass

NAME                    			PROVISIONER               	RECLAIMPOLICY  
linode-block-storage    		linodebs.csi.linode.com   	Delete         
linode-block-storage-retain (default)   linodebs.csi.linode.com  	Retain         


Затем при помощи следующего кода создадим для MySQL приложение StatefulSet в кластере Kubernetes:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql-set
spec:
  selector:
    matchLabels:
      app: mysql
  serviceName: "mysql"
  replicas: 3
  template:
    metadata:
      labels:
        app: mysql
    spec:
      terminationGracePeriodSeconds: 10
      containers:
      - name: mysql
        image: mysql:5.7
        ports:
        - containerPort: 3306
        volumeMounts:
        - name: mysql-store
          mountPath: /var/lib/mysql
        env:
          - name: MYSQL_ROOT_PASSWORD
            valueFrom:
              secretKeyRef:
                name: mysql-password
                key: MYSQL_ROOT_PASSWORD
  volumeClaimTemplates:
  - metadata:
      name: mysql-store
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: "linode-block-storage-retain"
      resources:
        requests:
          storage: 5Gi


Здесь нужно отметить несколько вещей:

  1. kind – это StatefulSet. kind приказывает Kubernetes создать приложение MySQL с возможностью сохранения состояния
  2. Пароль берется из объекта Secret при помощи secretKeyRef.
  3. Блоковое хранилище Linode использовалось в volumeClaimTemplates. Если здесь не упомянуть имени никакого класса хранилища, то для вашего кластера будет выбрано то хранилище, что задано по умолчанию.
  4. Количество реплик здесь равно 3 (указывается в параметре replica), поэтому будет создано три пода с именами mysql-set-0, mysql-set-1 и mysql-set-2.

Далее сохраняем код в файле под именем mysql.yaml и выполняем его при помощи следующей команды:

kubectl apply -f mysql.yaml
Теперь, когда поды MySQL созданы, получаем список подов:
kubectl get pods

NAME          READY   STATUS      RESTARTS   AGE
mysql-set-0   1/1         Running        0                 142s
mysql-set-1   1/1         Running        0                 132s
mysql-set-2   1/1         Running        0                 120s


Создаем сервис для приложения со StatefulSet


Теперь создаем сервис для пода MySQL. С приложением, сохраняющим состояние, нельзя использовать балансировщик нагрузки. Вместо этого создадим для приложения MySQL «безголовый» сервис, воспользовавшись следующим кодом:

apiVersion: v1
kind: Service
metadata:
  name: mysql
  labels:
    app: mysql
spec:
  ports:
  - port: 3306
  clusterIP: None
  selector:
    app: mysql


Сохраняем этот код в файле под именем mysql-service.yaml и выполняем при помощи следующей команды:

kubectl apply -f mysql-service.yaml


Получаем список работающих сервисов:

kubectl get svc


Создаем клиент для MySQL


Если вы хотите обращаться к MySQL, то вам понадобится клиентский инструмент для работы с MySQL. Разверните клиент MySQL, воспользовавшись следующим кодом манифеста:

apiVersion: v1
kind: Pod
metadata:
  name: mysql-client
spec:
  containers:
  - name: mysql-container
    image: alpine
    command: ['sh','-c', "sleep 1800m"]
    imagePullPolicy: IfNotPresent


Сохраните этот код в файле под именем mysql-client.yaml и выполните при помощи следующей команды:

kubectl apply -f mysql-client.yaml


Затем введите в клиент MySQL следующий код:

kubectl exec --stdin --tty mysql-client -- sh


Наконец, установите клиентский инструмент для работы с MySQL:

apk add mysql-client


Обратитесь к приложению MySQL через клиент MySQL


Далее будем обращаться к приложению MySQL через клиент MySQL и создавать базы данных в подах.

Если вы еще не зашли в под с клиентом MySQL, введите следующий код:
kubectl exec -it mysql-client /bin/sh

Чтобы обратиться к MySQL, можно воспользоваться все той же стандартной командой MySQL, чтобы подключиться к серверу MySQL:

mysql -u root -p -h host-server-name


Для доступа вам понадобится имя сервера MySQL. Синтаксис сервера MySQL в кластере Kubernetes приведен ниже:

stateful_name-ordinal_number.mysql.default.svc.cluster.local

#Example
mysql-set-0.mysql.default.svc.cluster.local


Подключитесь к ведущему поду MySQL при помощи следующей команды. Когда система запросит пароль, введите тот, который вы подобрали в предыдущем разделе о создании секрета.

mysql -u root -p -h mysql-set-0.mysql.default.svc.cluster.local


Далее создайте базу данных в ведущем поде с MySQL, после этого выйдите:

create database erp;
exit;


Теперь подключайтесь к другим подам и создайте базу данных так, как показано выше:

mysql -u root -p -h mysql-set-1.mysql.default.svc.cluster.local


mysql -u root -p -h mysql-set-2.mysql.default.svc.cluster.local


Не забывайте, что, конечно, Kubernetes помогает настроить приложение с сохранением состояния, но клонирование и синхронизацию данных вы должны настроить самостоятельно. StatefulSets сами этого сделать не смогут.

Наилучшие практики


Если вы планируете развертывать приложения, работающие с сохранением состояния, например, Oracle, MySQL, Elasticsearch и MongoDB, то StatefulSets – отличный вариант для вас.
Создавая приложения с сохранением состояния, учитывайте следующие факторы:

  1. Создайте для баз данных отдельное пространство имен.
  2. Все компоненты, необходимые для приложений с сохранением состояния, например, ConfigMaps, секреты и сервисы, размещайте в конкретном пространстве имен.
  3. Ваши собственные скрипты размещайте в ConfigMaps.
  4. При создании объектов «сервис» пользуйтесь безголовым сервисом, а не сервисом для балансировки нагрузки.
  5. Секреты храните в Vault или HashiCorp.
  6. Данные храните только в томе для долговременного хранения. В таком случае ваши данные не будут удалены, если под погибнет или аварийно завершит работу.


Объекты Deployment – это самые ходовые контроллеры, используемые для создания подов в Kubernetes. Эти поды легко масштабировать, для этого достаточно указать в файле манифеста количество реплик. Например, допустим, вы планируете развернуть приложение Node.js, после чего собираетесь распространить его на пять реплик. В таком случае вам хорошо подойдет объект Deployment.

На следующей схеме показано, как Deployment и StatefulSets присваивают имена подам.

image

StatefulSets создают пронумерованные по порядку конечные точки сервисов для каждого пода, созданного путем репликации. На следующей схеме показано, как создаются конечные точки сохраняющих состояние подов (пронумерованные по порядку), и как они обмениваются информацией друг с другом.

image

Заключение


В этой статье было рассказано о двух главных контроллерах, используемых в Kubernetes для создания подов: Deployment и StatefulSet. Объект Deployment очень хорош для работы с приложениями, не сохраняющими состояние, а StatefulSets – с сохраняющими. Если вы планируете развертывать приложения, сохраняющие состояние, например, MySQL и Oracle, следует воспользоваться контроллером StatefulSets, а не объектом Deployment.

Контроллер StatefulSets предоставляет возможность пронумеровать все поды по порядку, начиная с нуля. Поэтому при работе с приложениями, сохраняющими состояние, легко обустроить архитектуру, в которой один под является ведущим, а остальные – его репликами. Если один из подов погибнет, то заново создается одноименный ему новый под. Эта возможность очень полезна и не нарушает цепочку кластеров с приложениями, сохраняющими состояние. Если же вы масштабируетесь вниз, то избыточные поды удаляются в обратном порядке.

Пользуйтесь контроллером StatefulSets в кластере Kubernetes, если нужно развертывать приложения, сохраняющие состояние, например, Oracle, MySQL, Elasticsearch и MongoDB. Притом, что клонирование и синхронизацию данных все равно потребуется обеспечивать вручную, StatefulSets в значительной степени упрощают развертывание приложений, сохраняющих состояние.

Комментарии (18)


  1. DikSoft
    05.12.2022 19:09
    +2

    Был бы признателен, если кто-то смог бы аргументированно рассказать про преимущества подхода, когда СУБД размещается в контейнерах.
    Кроме банальных: "ну, круто же, сейчас всё в контейнерах".


    1. saboteur_kiev
      05.12.2022 20:09
      +1

      да нет никаких аргументов.

      Просто если у тебя уже есть кластер типа кубера или опенщифта, то вместо клепания виртуалок, можно уже и субд в контейнер запустить, чтобы все было в одном месте.

      Но если ты в облаке, то проще заказать готовую базу.
      А если у тебя не слишком много всего в кубере/опенщифте, и нет требования стандартизировать все, можно делать так, как тебе удобно.


    1. JuriM
      05.12.2022 21:23
      +1

      Если используется k8s и gitops, то быстрее и удобнее создавать кластера через k8s оператора+helm/kustomize, чем переключаться на ансибл, терраформ


      1. DikSoft
        05.12.2022 21:29

        Ок, а во что в результате обходится это вот "удобнее" если база более-менее серьёзная?


        1. JuriM
          05.12.2022 21:46

          Не совсем понял вопрос. Если отказоустойчивость и бэкапы, то это обеспечивает оператор. Если размер базы и сохранность файлов, то это обеспечивает сторадж контроллер и файловая система


        1. sgjurano
          05.12.2022 23:09

          Требуется разбираться в другом стеке технологий, k8s operator + контейнеры против условных lxc + puppet.

          Это никогда не было бесплатно, но на больших масштабах (от сотен баз) экономия уже заметна.


        1. berendiaev
          06.12.2022 02:18
          +3

          Оператор придётся выбирать, тренировать бэкапирование и восстановление, писать доки по типовым операциям типа масштабирования во все стороны.

          Это, кстати, может потребовать разбирательств в сорцах оператора. Кроме того, часто даже широко используемые операторы содержат серьёзные баги.

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

          SaaS сервисы не требуют такого вдумчивого подхода - как правило, облачный провайдер уже написал всю документацию, а саппорт поможет с проблемами. В случае с проблемами операторов вам никто в срочном порядке помогать не станет.


          1. vitaly_il1
            06.12.2022 07:35

            +1 - У меня очень похожие мысли:
            - всегда рекомендую DBaaS
            - поднять надежный сервер БД непросто - нужно знать БД, репликацию, performance tuning, backups, HA и т.п. А если мы это делаем под K8S - то дополнительно нужно еще разбираться с K8S и оператором.


            1. gto
              06.12.2022 23:51
              +1

              Как насчёт бюджета? DBaaS всё-таки ощютимее дороже.


              1. vitaly_il1
                07.12.2022 07:36
                +1

                Как насчет надежности, производительности, scalability, возможности восстановления? За это мы и платим больше.
                Я меня большой опыт с MySQL и я хорошо знаю, что поддерживать production-ready кластер очень непросто. А в DBaaS мы получаем это, плюс каждая кухарка может восстановить базу включая  Point-in-Time Recovery. А админ может сделать scale up \ scale down одной кнопкой и это будет работать.


                1. gto
                  07.12.2022 11:44

                  Ни в коем разе не сомневаюсь в Вашей компетентности. Но всё-таки надёжность, масштабируемость и возможность востановления это всё очень субъективно. Про производительность даже не говорю. Любое приложение на железке будет производительней (может так же, но точно не хуже) чем в облаке. А уменьшение сложности управления может сыграть злую шутку, когда кухарка решит, что можно начать оптимизировать базу просто выкручивая параметры.


                  1. vitaly_il1
                    07.12.2022 12:34

                    Я имел в виду что поддерживать базу и так непросто, а K8S добавляет свои заморочки.
                    Я не сомневаюсь, что инсталлировать базу с помощью "helm install" в K8S может любой студент курса DevOps. Но чтобы быть уверенным, что есть возможность восстановления, и чтобы знать что делать когда "все упало" нужен очень серьезный уровень и в DBA и в K8S. То есть дорогой человек или группа. Наверно это окупается когда есть десятки-сотни production баз.
                    Для небольших фирм я всегда советую оутсорсить все это платя за DBaaS.


    1. gto
      06.12.2022 23:46

      Преимущества те же, что и размещение любого приложения в контейнере. Изолированность среды. Данные-то всё равно в контейнер пихать не стоит. А так бегает себе движок в контейнере со своими либами, надо обновить - старый новым образом заменил и готово (конечно + миграция и т.п. ). Опять же дебажить удобно. Образ на десктоп поднял и уверен, что все либы те же. Если вопрос в прозводительности, то контейнеры её точно не прибавят.


  1. sloniki
    05.12.2022 23:42

    1. Секреты храните в Vault или HashiCorp.

    Имелось ввиду наверное HashiCorp Vault.

    И да, как уже верно заметили, если есть stateful аппликуха, то нужно сразу искать уже готовый оператор. В большинстве случаев он уже есть. Это cloud-native подход, когда есть управление полным циклом.


  1. vanyas
    06.12.2022 14:08
    +1

    пользуйтесь безголовым сервисом

    это прям прекрасно


  1. gto
    06.12.2022 23:48

    Второй абзац только я не понял?


  1. indestructable
    07.12.2022 17:00

    mysql -u root -p -h mysql-set-0.mysql.default.svc.cluster.local - вот эта строчка подключается напрямую к контейнеру? Или сервис создает "подсервисы" для каждого пода стейтфул сета?


  1. booger_man
    08.12.2022 11:03

    Почему в статье не описаны стандартные сценарии отказоустойчивости?
    Что будет с приложением и кластером бд если упадет mysql-set-0 ?
    Что будет при проблемах с сетью по типу splitbrain (мастер недоступен ля приложения)?
    Понятно что между Deployment и StatefulSet есть разница но отказоустойчивость они обеспечивают примерно одинаковую.