MongoDB — популярная NoSQL/документоориентированная база данных, поєтому ее часто используют в различных продуктах, в production, в том числе. Давайте разберемся, какие варианты для запуска Mongo в K8s существуют и в  их особенностях.

Kubernetes позволяет очень удобно масштабировать и администрировать приложения, но также важно уделить внимание должному планированию, иначе неприятностей можно получить больше, чем пользы. То же самое относится и к MongoDB в Kubernetes.

Основные моменты

При размещении Mongo в кластере стоит учитывать:

  1. Хранилище. Идеальный вариант для гибкой работы в Kubernetes для Mongo — удаленные хранилища, переключаемые между узлами, если нужно будет перемещение Mongo при обновлении узлов кластера или удалении узлов. Но удаленные диски часто доступны с более низким показателем iops (если сравнивать их с локальными). ЕслиУ вас высоконагруженная база и важны хорошие показания по latency, то обратите на это внимание.
  2. Корректные requests и limits на pod’ах с репликами Mongo (и соседствующих с ними pod’ами на узле). Важна их правильная настройка, иначе можно получить нежелательное поведение — при внезапно возросшей нагрузке на узле Kubernetes начнет убивать pod’ы с репликами Mongo и переносить их на соседние, менее загруженные. А пока pod с Mongo поднимется на другом узле, может пройти значительное время, так что это не очень приятно. А если упавшая реплика была primary, то результат может быть совсем плачевный — это приведет к перевыборам: вся запись встанет, а приложение должно быть к этому готово и/или будет простаивать.
  3. Если случился пик нагрузки, в Kubernetes есть возможность быстро отмасштабировать узлы и перенести Mongo на узлы с большими ресурсами. Не забывайте про podDisruptionBudget, это не даст удалять или переносить pod’ы все вместе и будет поддерживать нужное количество реплик в рабочем состоянии.

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

При выборе хранилища у провайдера, отдавайте приоритет сетевым дискам, это позволит динамически расширять кластер MongoDB. Но стоит отметить, что они проигрывают в производительности локальным. Пример из Google Cloud:

 

Также есть зависимость от дополнительных факторов:

В AWS это выглядит вроде лучше, но до производительности в локальном варианте также далеко:

Хотя зачастую для большинства задач для MongoDB достаточно ресурсов, предоставляемых провайдерами.

Как поднять MongoDB в Kubernetes?

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

1. Helm-чарт от Bitnami

Первый вариант — это Helm-чарт от Bitnami. Это довольно популярное решение.

Чарт позволяет запускать MongoDB несколькими способами:

  1. standalone;
  2. Replica Set (здесь и далее по умолчанию подразумевается терминология MongoDB; если речь пойдет про ReplicaSet в Kubernetes, на это будет явное указание);
  3. Replica Set + Arbiter.

Используется свой (т.е. неофициальный) образ.

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

Необходимый минимум по конфигурации:

  1. Указать архитектуру (Values.yaml#L58-L60). По умолчанию это standalone, но нас интересует replicaset:
...
architecture: replicaset
...

        2. Указать тип и размер хранилища (Values.yaml#L442-L463):

...
persistence:
  enabled: true
  storageClass: "gp2" # у нас это general purpose 2 из AWS
  accessModes:
    - ReadWriteOnce
  size: 120Gi
...

Далее через helm install мы получаем готовый кластер MongoDB с инструкцией, как к нему подключиться из Kubernetes:

NAME: mongobitnami
LAST DEPLOYED: Fri Feb 26 09:00:04 2021
NAMESPACE: mongodb
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
** Please be patient while the chart is being deployed **

MongoDB(R) can be accessed on the following DNS name(s) and ports from within your cluster:

    mongobitnami-mongodb-0.mongobitnami-mongodb-headless.mongodb.svc.cluster.local:27017
    mongobitnami-mongodb-1.mongobitnami-mongodb-headless.mongodb.svc.cluster.local:27017
    mongobitnami-mongodb-2.mongobitnami-mongodb-headless.mongodb.svc.cluster.local:27017

To get the root password run:

    export MONGODB_ROOT_PASSWORD=$(kubectl get secret --namespace mongodb mongobitnami-mongodb -o jsonpath="{.data.mongodb-root-password}" | base64 --decode)

To connect to your database, create a MongoDB(R) client container:

    kubectl run --namespace mongodb mongobitnami-mongodb-client --rm --tty -i --restart='Never' --env="MONGODB_ROOT_PASSWORD=$MONGODB_ROOT_PASSWORD" --image docker.io/bitnami/mongodb:4.4.4-debian-10-r0 --command -- bash

Then, run the following command:
    mongo admin --host "mongobitnami-mongodb-0.mongobitnami-mongodb-headless.mongodb.svc.cluster.local:27017,mongobitnami-mongodb-1.mongobitnami-mongodb-headless.mongodb.svc.cluster.local:27017,mongobitnami-mongodb-2.mongobitnami-mongodb-headless.mongodb.svc.cluster.local:27017" --authenticationDatabase admin -u root -p $MONGODB_ROOT_PASSWORD

В пространстве имен увидим готовый кластер с арбитром (он enabled в чарте по умолчанию):

Этот минимум не отвечает главным вызовам, перечисленным в начале статьи, так что включим в нее дополнительно:

  1. Установить PDB (по умолчанию он выключен). Мы не хотим терять кластер в случае drain’а узлов — можем позволить себе недоступность максимум 1 узла (Values.yaml#L430-L437):
    ...
    pdb:
      create: true
      maxUnavailable: 1
    ...
    

     

  2. Установить requests и limits (Values.yaml#L350-L360):
    ...
    resources:
      limits:
        memory: 8Gi
      requests: 
        cpu: 4
        memory: 4Gi
    ...
    

    Также рекомендуем повысить приоритет у pod’ов с базой относительно других pod’ов (Values.yaml#L326).

  3.  По умолчанию чарт создает нежесткое anti-affinity для pod’ов кластера. Scheduler будет стараться назначать pod’ы на разные узла, но если выбора не будет, то начнет размещать туда, где есть место.

    При достаточно количестве ресурсов и узлов, стоит не выносить две реплики кластера на один и тот же узел (Values.yaml#L270):

    ...
    podAntiAffinityPreset: hard
    ...
    

    Запуск кластера в чарте происходит так:

  1. Запускаем StatefulSet с нужным числом реплик и двумя init-контейнерами: volume-permissions и auto-discovery.
  2. Volume-permissions создает директорию для данных и выставляет права на неё.
  3. Auto-discovery ждёт, пока появятся все сервисы, и пишет их адреса в shared_file, который является точкой передачи конфигурации между init-контейнером и основным контейнером.
  4. Запускается основной контейнер с подменой command, определяются переменные для entrypoint’а и run.sh.
  5. Запускается entrypoint.sh, который вызывает каскад из вложенных друг в друга Bash-скриптов с вызовом описанных в них функций.

В конечном итоге инициализируется MongoDB через такую функцию:

mongodb_initialize() {
       local persisted=false

       info "Initializing MongoDB..."

       rm -f "$MONGODB_PID_FILE"
       mongodb_copy_mounted_config
       mongodb_set_net_conf
       mongodb_set_log_conf
       mongodb_set_storage_conf

       if is_dir_empty "$MONGODB_DATA_DIR/db"; then
               info "Deploying MongoDB from scratch..."
               ensure_dir_exists "$MONGODB_DATA_DIR/db"
               am_i_root && chown -R "$MONGODB_DAEMON_USER" "$MONGODB_DATA_DIR/db"

               mongodb_start_bg
               mongodb_create_users
               if [[ -n "$MONGODB_REPLICA_SET_MODE" ]]; then
               if [[ -n "$MONGODB_REPLICA_SET_KEY" ]]; then
                       mongodb_create_keyfile "$MONGODB_REPLICA_SET_KEY"
                       mongodb_set_keyfile_conf
               fi
               mongodb_set_replicasetmode_conf
               mongodb_set_listen_all_conf
               mongodb_configure_replica_set
               fi
               mongodb_stop
       else
               persisted=true
               mongodb_set_auth_conf
               info "Deploying MongoDB with persisted data..."
               if [[ -n "$MONGODB_REPLICA_SET_MODE" ]]; then
               if [[ -n "$MONGODB_REPLICA_SET_KEY" ]]; then
                       mongodb_create_keyfile "$MONGODB_REPLICA_SET_KEY"
                       mongodb_set_keyfile_conf
               fi
               if [[ "$MONGODB_REPLICA_SET_MODE" = "dynamic" ]]; then
                       mongodb_ensure_dynamic_mode_consistency
               fi
               mongodb_set_replicasetmode_conf
               fi
       fi
       mongodb_set_auth_conf
       }

2. «Старый» чарт

Также доступен и старый чарт в главном репозитории Helm. Сейчас он уже deprecated, но при этом поддерживается и используется некоторыми организациями.

Он не умеет запускать Replica Set + Arbiter и использует маленький сторонний образ в init-контейнерах, но в остальном достаточно прост и отлично выполняет задачу деплоя небольшого кластера. 

Минимальная конфигурация сильно схожа с предыдущим чартом, стоит только отметить, что affinity нужно задавать вручную (Values.yaml#L108):

affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchLabels:
               app: mongodb-replicaset

Алгоритм его работы схож с чартом от Bitnami, но менее нагружен (нет такого нагромождения маленьких скриптов с функциями):

  1. Init-контейнер copyconfig копирует конфиг из configdb-readonly (ConfigMap) и ключ из секрета в директорию для конфигов (emptyDir, который будет смонтирован в основной контейнер).
  2. Секретный образ unguiculus/mongodb-install копирует исполнительный файл peer-finder в work-dir.
  3. Init-контейнер bootstrap запускает peer-finder с параметром /init/on-start.sh — этот скрипт занимается поиском поднятых узлов кластера MongoDB и добавлением их в конфигурационный файл Mongo.
  4. Скрипт /init/on-start.sh отрабатывает в зависимости от конфигурации, передаваемой ему через переменные окружения (аутентификация, добавление дополнительных пользователей, генерация SSL-сертификатов…), плюс может исполнять дополнительные кастомные скрипты, которые нужно запускать перед стартом базы.
  5. Список пиров получают как:
     args:
                - -on-start=/init/on-start.sh
                - "-service=mongodb"
    log "Reading standard input..."
    while read -ra line; do
        if [[ "${line}" == *"${my_hostname}"* ]]; then
            service_name="$line"
        fi
        peers=("${peers[@]}" "$line")
    done
    

     

  6. Выполняется проверка по списку пиров: кто из них — primary, а кто — master.
    • Если не primary, то пир добавляется к primary в кластер.
    • Если это самый первый пир, он инициализирует себя и объявляется мастером.
  7. Конфигурируются пользователи с правами администратора.
  8. Запускается сам процесс MongoDB.

3. Официальный оператор

В 2020 году вышел в свет официальный Kubernetes-оператор community-версии MongoDB. Он дает возможность легко разворачивать, обновлять и масштабировать кластер MongoDB. Также оператор сильно проще чартов в первичной настройке.

Но мы рассматриваем community-версию и она довольно ограничена в возможностях и не подлежит сильной кастомизации по сравнению с чартами выше. Это логично, учитывая, что существует и enterprise-редакция.

Архитектура оператора:

Тут потребуется установить сам оператор и CRD (CustomResourceDefinition), что будет использоваться для создания объектов в Kubernetes.

Установка кластера оператором выглядит следующим образом:

  1. Оператор создает StatefulSet, содержащий pod’ы с контейнерами MongoDB. Каждый из них — член ReplicaSet’а в Kubernetes.
  2. Создается и обновляется конфиг для sidecar-контейнера агента, который будет конфигурировать MongoDB в каждом pod’е. Конфиг хранится в Kubernetes-секрете. 
  3. Создается pod с одним init-контейнером и двумя основными.
    1. Init-контейнер копирует бинарный файл хука, проверяющего версию MongoDB, в общий empty-dir volume (для его передачи в основной контейнер).
    2. Контейнер для агента MongoDB выполняет управление основным контейнером с базой: конфигурация, остановка, рестарт и внесение изменений в конфигурацию.
  4. Далее контейнер с агентом на основе конфигурации, указанной в Custom Resource для кластера, генерирует конфиг для самой MongoDB.

Вся установка кластера укладывается в:

---
apiVersion: mongodb.com/v1
kind: MongoDBCommunity
metadata:
  name: example-mongodb
spec:
  members: 3
  type: ReplicaSet
  version: "4.2.6"
  security:
    authentication:
      modes: ["SCRAM"]
  users:
    - name: my-user
      db: admin
      passwordSecretRef: # ссылка на секрет ниже для генерации пароля юзера
        name: my-user-password
      roles:
        - name: clusterAdmin
          db: admin
        - name: userAdminAnyDatabase
          db: admin
      scramCredentialsSecretName: my-scram

# учетная запись пользователя генерируется из этого секрета
# после того, как она будет создана, секрет больше не потребуется
---
apiVersion: v1
kind: Secret
metadata:
  name: my-user-password
type: Opaque
stringData:
  password: 58LObjiMpxcjP1sMDW

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

При этом у него нет встроенной возможности отдачи метрик в Prometheus, как у предыдущих вариантов, а вариант запуска только один — Replica Set (нельзя создать арбитра). И его не получится сильно кастомизировать, т.к. практически все параметры регулируются через кастомную сущность для поднятия кластера, а сама она ограничена.

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

Enterprise-версия оператора предоставляет большие возможности — установку не только Replica Set’ов, но и shared-кластеров с настройками шардирования, конфигурации для доступа извне кластера (с указанием имен, по которым он будет доступен извне), дополнительные способы аутентификации т.д. И, конечно же, документация к нему описана гораздо лучше.

Саммари

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

У разных вариантов запуска MongoDB есть разные плюсы. Чарты легко модифицировать под ваши нужды, но вы столкнетесь с проблемами при обновлении MongoDB или при добавлении узлов, т.к. всё равно потребуются ручные операции с кластером. Способ с оператором в этом смысле лучше, но ограничен по другим параметрам (по крайней мере, в своей community-редакции). Также ни в одном из описанных вариантов нет возможности из коробки запускать скрытые реплики.