MongoDB — популярная NoSQL/документоориентированная база данных, поєтому ее часто используют в различных продуктах, в production, в том числе. Давайте разберемся, какие варианты для запуска Mongo в K8s существуют и в их особенностях.
Kubernetes позволяет очень удобно масштабировать и администрировать приложения, но также важно уделить внимание должному планированию, иначе неприятностей можно получить больше, чем пользы. То же самое относится и к MongoDB в Kubernetes.
Основные моменты
При размещении Mongo в кластере стоит учитывать:
- Хранилище. Идеальный вариант для гибкой работы в Kubernetes для Mongo — удаленные хранилища, переключаемые между узлами, если нужно будет перемещение Mongo при обновлении узлов кластера или удалении узлов. Но удаленные диски часто доступны с более низким показателем iops (если сравнивать их с локальными). ЕслиУ вас высоконагруженная база и важны хорошие показания по latency, то обратите на это внимание.
- Корректные requests и limits на pod’ах с репликами Mongo (и соседствующих с ними pod’ами на узле). Важна их правильная настройка, иначе можно получить нежелательное поведение — при внезапно возросшей нагрузке на узле Kubernetes начнет убивать pod’ы с репликами Mongo и переносить их на соседние, менее загруженные. А пока pod с Mongo поднимется на другом узле, может пройти значительное время, так что это не очень приятно. А если упавшая реплика была primary, то результат может быть совсем плачевный — это приведет к перевыборам: вся запись встанет, а приложение должно быть к этому готово и/или будет простаивать.
- Если случился пик нагрузки, в Kubernetes есть возможность быстро отмасштабировать узлы и перенести Mongo на узлы с большими ресурсами. Не забывайте про podDisruptionBudget, это не даст удалять или переносить pod’ы все вместе и будет поддерживать нужное количество реплик в рабочем состоянии.
Если со всеми этими моментами справиться, то получите быстро масштабируемую вертикально и горизонтально базу. Она будет находиться в одной среде с приложениями и удобно управляться общими механизмами Kubernetes. А хорошее планирование размещения внутри кластера позволит гарантировать надежность, если учтены основные негативные сценарии использования.
При выборе хранилища у провайдера, отдавайте приоритет сетевым дискам, это позволит динамически расширять кластер MongoDB. Но стоит отметить, что они проигрывают в производительности локальным. Пример из Google Cloud:
Также есть зависимость от дополнительных факторов:
В AWS это выглядит вроде лучше, но до производительности в локальном варианте также далеко:
Хотя зачастую для большинства задач для MongoDB достаточно ресурсов, предоставляемых провайдерами.
Как поднять MongoDB в Kubernetes?
Конечно, можно обойтись кастомным решением, подготовив несколько манифестов со StatefulSet и init-скриптом. Но мы рассмотрим уже существующие подходы проверенные и рабочие.
1. Helm-чарт от Bitnami
Первый вариант — это Helm-чарт от Bitnami. Это довольно популярное решение.
Чарт позволяет запускать MongoDB несколькими способами:
- standalone;
- Replica Set (здесь и далее по умолчанию подразумевается терминология MongoDB; если речь пойдет про ReplicaSet в Kubernetes, на это будет явное указание);
- Replica Set + Arbiter.
Используется свой (т.е. неофициальный) образ.
Из преимуществ — хорошая параметризация и документация. Из минусов — функций довольно много и может понадобиться время, чтоб решить что вам действительно нужно. Использование этого чарта очень похоже на конструктор, где вы можете сами собрать нужную конфигурацию.
Необходимый минимум по конфигурации:
- Указать архитектуру (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 в чарте по умолчанию):
Этот минимум не отвечает главным вызовам, перечисленным в начале статьи, так что включим в нее дополнительно:
- Установить PDB (по умолчанию он выключен). Мы не хотим терять кластер в случае drain’а узлов — можем позволить себе недоступность максимум 1 узла (Values.yaml#L430-L437):
... pdb: create: true maxUnavailable: 1 ...
- Установить requests и limits (Values.yaml#L350-L360):
... resources: limits: memory: 8Gi requests: cpu: 4 memory: 4Gi ...
Также рекомендуем повысить приоритет у pod’ов с базой относительно других pod’ов (Values.yaml#L326).
- По умолчанию чарт создает нежесткое anti-affinity для pod’ов кластера. Scheduler будет стараться назначать pod’ы на разные узла, но если выбора не будет, то начнет размещать туда, где есть место.
При достаточно количестве ресурсов и узлов, стоит не выносить две реплики кластера на один и тот же узел (Values.yaml#L270):
... podAntiAffinityPreset: hard ...
Запуск кластера в чарте происходит так:
- Запускаем StatefulSet с нужным числом реплик и двумя init-контейнерами: volume-permissions и auto-discovery.
- Volume-permissions создает директорию для данных и выставляет права на неё.
- Auto-discovery ждёт, пока появятся все сервисы, и пишет их адреса в shared_file, который является точкой передачи конфигурации между init-контейнером и основным контейнером.
- Запускается основной контейнер с подменой command, определяются переменные для entrypoint’а и run.sh.
- Запускается 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, но менее нагружен (нет такого нагромождения маленьких скриптов с функциями):
- Init-контейнер copyconfig копирует конфиг из configdb-readonly (ConfigMap) и ключ из секрета в директорию для конфигов (emptyDir, который будет смонтирован в основной контейнер).
- Секретный образ unguiculus/mongodb-install копирует исполнительный файл peer-finder в work-dir.
- Init-контейнер bootstrap запускает peer-finder с параметром /init/on-start.sh — этот скрипт занимается поиском поднятых узлов кластера MongoDB и добавлением их в конфигурационный файл Mongo.
- Скрипт /init/on-start.sh отрабатывает в зависимости от конфигурации, передаваемой ему через переменные окружения (аутентификация, добавление дополнительных пользователей, генерация SSL-сертификатов…), плюс может исполнять дополнительные кастомные скрипты, которые нужно запускать перед стартом базы.
- Список пиров получают как:
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
- Выполняется проверка по списку пиров: кто из них — primary, а кто — master.
- Если не primary, то пир добавляется к primary в кластер.
- Если это самый первый пир, он инициализирует себя и объявляется мастером.
- Конфигурируются пользователи с правами администратора.
- Запускается сам процесс MongoDB.
3. Официальный оператор
В 2020 году вышел в свет официальный Kubernetes-оператор community-версии MongoDB. Он дает возможность легко разворачивать, обновлять и масштабировать кластер MongoDB. Также оператор сильно проще чартов в первичной настройке.
Но мы рассматриваем community-версию и она довольно ограничена в возможностях и не подлежит сильной кастомизации по сравнению с чартами выше. Это логично, учитывая, что существует и enterprise-редакция.
Архитектура оператора:
Тут потребуется установить сам оператор и CRD (CustomResourceDefinition), что будет использоваться для создания объектов в Kubernetes.
Установка кластера оператором выглядит следующим образом:
- Оператор создает StatefulSet, содержащий pod’ы с контейнерами MongoDB. Каждый из них — член ReplicaSet’а в Kubernetes.
- Создается и обновляется конфиг для sidecar-контейнера агента, который будет конфигурировать MongoDB в каждом pod’е. Конфиг хранится в Kubernetes-секрете.
- Создается pod с одним init-контейнером и двумя основными.
- Init-контейнер копирует бинарный файл хука, проверяющего версию MongoDB, в общий empty-dir volume (для его передачи в основной контейнер).
- Контейнер для агента MongoDB выполняет управление основным контейнером с базой: конфигурация, остановка, рестарт и внесение изменений в конфигурацию.
- Далее контейнер с агентом на основе конфигурации, указанной в 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-редакции). Также ни в одном из описанных вариантов нет возможности из коробки запускать скрытые реплики.