Автор статьи: Рустем Галиев

IBM Senior DevOps Engineer & Integration Architect

Привет Хабр!

В Kubernetes StatefulSet — это реплицированные группы Pod’ов, аналогичные ReplicaSet’ам.

Каждая реплика получает постоянное имя хоста с уникальным индексом (например, database-0, database-1 и т. д.).

Каждая реплика создается в порядке от самого низкого до самого высокого индекса и создание блокируется до тех пор, пока под с предыдущим индексом не станет работоспособным и доступным. Это относится и к масштабированию.

При удалении StatefulSet каждый из управляемых подов реплики также удаляется в порядке убывания. Это также относится к уменьшению количества реплик.

Оказывается, этот простой набор требований значительно упрощает развертывание приложений для хранения данных в Kubernetes. Например, сочетание стабильных имен хостов (например, database-0) и ограничений порядка означает, что все реплики, кроме первой, могут надежно ссылаться на database-0 для целей обнаружения и установления кворума репликации.

Сегодня мы развернем реплицированный кластер MongoDB с StatefulSet.

Для начала создадим реплицированный набор из трех модулей MongoDB, используя объект StatefulSet. Основной контейнер приложения использует образ контейнера mongo:3.4.24 и запускает процесс mongod. Когда вы запускаете mongod, запускается процесс MongoDB и запускается он в фоновом режиме. У процесса есть несколько параметров по умолчанию, например, сохранение данных в /data/db и запуск через порт 27017.

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mongo
spec:
  serviceName: "mongo"
  replicas: 3
  selector:
    matchLabels:
      app: mongo
  template:
    metadata:
      labels:
        app: mongo
    spec:
      containers:
      - name: mongodb
        image: mongo:3.4.24
        command:
        - mongod
        - --replSet
        - rs0
        ports:
        - containerPort: 27017
          name: peer


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

Инициализируем кластер MongoDB с помощью init Container

Чтобы автоматизировать развертывание нашего кластера MongoDB на основе StatefulSet, мы собираемся добавить дополнительный контейнер в поды для выполнения инициализации.

Чтобы настроить этот модуль без создания нового образа Docker, мы собираемся использовать ConfigMap для добавления скрипта в существующий образ MongoDB.

Мы собираемся запустить этот скрипт, используя контейнер инициализации. Контейнеры инициализации (или init Container) — это специализированные контейнеры, которые запускаются один раз при запуске пода. Они обычно используются в таких случаях, когда есть небольшой объем работы по настройке, которую полезно выполнить до запуска основного приложения. В определении пода есть отдельный список initContainers, в котором можно определить контейнеры инициализации.

Последним этапом производства нашего кластера MongoDB является добавление проверок жизнеспособности в наши контейнеры, обслуживающие Mongo:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mongo
spec:
  serviceName: "mongo"
  replicas: 3
  selector:
    matchLabels:
      app: mongo
  template:
    metadata:
      labels:
        app: mongo
    spec:
      containers:
      - name: mongodb
        image: mongo:3.4.24
        command:
        - mongod
        - --replSet
        - rs0
        ports:
        - containerPort: 27017
          name: web
        livenessProbe:
          exec:
            command:
            - /usr/bin/mongo
            - --eval
            - db.serverStatus()
          initialDelaySeconds: 10
          timeoutSeconds: 10
      # This container initializes the mongodb server, then sleeps.
      - name: init-mongo
        image: mongo:3.4.24
        command:
        - bash
        - /config/init.sh
        volumeMounts:
        - name: config
          mountPath: /config
      volumes:
      - name: config
        configMap:
          name: "mongo-init"


Замените содержимое в файле mongo.yaml. На следующем шаге мы создадим ConfigMap с именем mongo-init.

Создание карты конфигурации

Обратите внимание, что Pod монтирует ConfigMap Volume (том) с именем mongo-init. Этот ConfigMap содержит скрипт, который выполняет нашу инициализацию. Во-первых, скрипт определяет, работает ли он на mongo-0. Если он находится на mongo-0, он создает ReplicaSet с помощью той же команды, которую мы ранее запускали императивно. Если он находится в другой реплике Mongo,ждет, пока ReplicaSet exists (как бы правильно сказать, не появится), а затем регистрируется как мембер этого ReplicaSet.

Следующий манифест YAML содержит полный объект ConfigMap:

apiVersion: v1
kind: ConfigMap
metadata:
  name: mongo-init
data:
  init.sh: |
    #!/bin/bash

    # Need to wait for the readiness health check to pass so that the
    # mongo names resolve. This is kind of wonky.
    until ping -c 1 ${HOSTNAME}.mongo; do
      echo "waiting for DNS (${HOSTNAME}.mongo)..."
      sleep 2
    done

    until /usr/bin/mongo --eval 'printjson(db.serverStatus())'; do
      echo "connecting to local mongo..."
      sleep 2
    done
    echo "connected to local."

    HOST=mongo-0.mongo:27017

    until /usr/bin/mongo --host=${HOST} --eval 'printjson(db.serverStatus())'; do
      echo "connecting to remote mongo..."
      sleep 2
    done
    echo "connected to remote."

    if [[ "${HOSTNAME}" != 'mongo-0' ]]; then
      until /usr/bin/mongo --host=${HOST} --eval="printjson(rs.status())" \
            | grep -v "no replset config has been received"; do
        echo "waiting for replication set initialization"
        sleep 2
      done
      echo "adding self to mongo-0"
      /usr/bin/mongo --host=${HOST} \
         --eval="printjson(rs.add('${HOSTNAME}.mongo'))"
    fi

    if [[ "${HOSTNAME}" == 'mongo-0' ]]; then
      echo "initializing replica set"
      /usr/bin/mongo --eval="printjson(rs.initiate(\
          {'_id': 'rs0', 'members': [{'_id': 0, \
           'host': 'mongo-0.mongo:27017'}]}))"
    fi
    echo "initialized"


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

Сначала создайте объект ConfigMap:

kubectl apply -f mongo-configmap.yaml

Затем мы создадим объект StatefulSet, который теперь сможет ссылаться на ConfigMap по имени.

kubectl apply -f mongo.yaml

После создания различия между ReplicaSet и StatefulSet становятся очевидными. Учитывая, что для создания подов StatefulSet потребуется некоторое время, вам придется повторно запускать одну и ту же команду несколько раз, пока не будет достигнуто количество трех реплик:

kubectl get pods

Между этим и тем, что вы увидите с ReplicaSet, есть два важных отличия. Во-первых, каждый реплицированный Pod имеет числовой индекс (0, 1,…) вместо случайного суффикса, который добавляется контроллером ReplicaSet. Во-вторых, поды медленно создаются по порядку, а не все сразу, как это было бы с ReplicaSet.

Предоставление MongoDB как сервиса

После создания StatefulSet нам также необходимо создать «безголовую» службу для управления записями DNS для StatefulSet. В Kubernetes сервис называется «безголовым», если у него нет виртуального IP-адреса кластера. Поскольку со StatefulSets каждый Pod имеет уникальный идентификатор, на самом деле не имеет смысла иметь IP-адрес для балансировки нагрузки для реплицируемого сервиса. Вы можете создать безголовый сервис, используя clusterIP: None в спецификации сервиса:

apiVersion: v1
kind: Service
metadata:
  name: mongo
spec:
  ports:
  - port: 27017
    name: peer
  clusterIP: None
  selector:
    app: mongo


После создания этой службы обычно заполняются четыре записи DNS. Как обычно, создается mongo.default.svc.cluster.local, но, в отличие от стандартной службы, поиск DNS по этому имени хоста предоставляет все адреса в StatefulSet. Кроме того, создаются записи для mongo-0.mongo.default.svc.cluster​.local, а также для mongo-1.mongo и mongo-2.mongo. Каждый из них разрешается в определенный IP-адрес индекса реплики в StatefulSet. Таким образом, с помощью StatefulSets вы получаете четко определенные постоянные имена для каждой реплики в наборе. Это часто бывает очень полезно при настройке решения для хранения данных с репликацией.

Создайте службу с помощью следующей команды:

kubectl apply -f mongo-service.yaml

После того, как мы объединили StatefulSets, pvc и проверку живучести, у нас есть защищенная, масштабируемая облачная установка MongoDB, работающая в Kubernetes. Хотя в этой теме речь шла о MongoDB, шаги по созданию StatefulSet для управления другими решениями для хранения очень похожи и можно следовать аналогичным шаблонам.

В завершение хочу пригласить вас на бесплатный урок, где мои коллеги из OTUS расскажут как устроен мониторинг кластера, его компоненты и приложения в кластере. Вы изучите различные подходы мониторинга, подходы к мониторингу как приложения так и компонентов кластера, основные метрики Kubernetes. Также узнаете про кластеризацию/федерацию Prometheus, дополнительные хранилища метрик для prometheus (victoria metrics; thanos, cortex).

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


  1. Beastlex
    05.04.2023 06:50

    Спасибо большое. В окончательном листинге StatefulSet вы все-таки используете initContainers или инициализацию выполняете в обычном контейнере?


  1. kWatt
    05.04.2023 06:50

    Простите за мой нубский вопрос, а разве не надо создавать PV и PVC и только потом его подключать к деплойменту?