Привет, меня зовут Максим Качинский, я ведущий DevOps‑инженер в компании RocketData. Более 6 лет мы развиваем одноимённую платформу для управления репутацией — за это время она выросла с монолитного MVP на единственном сервере до 15+ микросервисов с пятью разными видами баз данных. И всё это в гибридной инфраструктуре.

Чем больше рос проект, тем сложнее нам было его поддерживать. Чтобы облегчить жизнь DevOps‑инженерам, мы начали постепенно переходить на managed‑решения. Но этот путь не был идеально прямым, и нам пришлось пересмотреть многие привычные рабочие инструменты.

В этом посте поделюсь нашим опытом: покажу, как мы используем Terraform, и расскажу, что стоит учесть при масштабировании гибридной инфраструктуры и переезде в облако.

Техническая изнанка платформы к моменту переезда

Платформа RocketData помогает компаниям управлять информацией в 80+ онлайн‑источниках: каталогах, справочниках, картах, ресурсах с отзывами и т. д. Например, клиент нашей платформы может размечать на картах точки присутствия компании, централизованно редактировать описание для посетителей, отслеживать отзывы в геосервисах и реагировать на них.

За последние пару лет количество продуктов для разных пользовательских сценариев перевалило за 20, практически каждый из них представлял собой отдельный микросервис. Машин, выделенных под разные задачи и типы данных, становилось больше, и поддерживать их было всё труднее.

Из чего конкретно состояла инфраструктура на старте истории переезда:

  • Self‑hosted кластер PostgreSQL, где хранились основные данные, от пользователей до отзывов.
    Размер кластера:

    • 96 vCPU, 384 GB RAM, 960 Gb SATA NVMe, 10 GB/s MC‑LAG lan master

    • 96 vCPU, 384 GB RAM, 960 Gb SATA NVMe, 1 GB/s lan slave.

  • Инстанс ClickHouse в качестве хранилища аналитических данных: статистики по отзывам, скорости ответов и т. д.
    Размер: 12 vCPU, 48 GB RAM, 500 Gb SSD.

  • Инстанс MongoDB для редко используемых больших данных.
    Размер: 4 vCPU, 16 GB RAM, 100 Gb SSD.

  • Шесть разных инстансов Redis для короткоживущих ключей: токенов авторизаций, метрик, результатов Celery‑задач.

  • 10 серверов размером 400 vCPU 1500 Gb RAM 15Tb SSD RAID1.

При этом часть «железа» мы арендовали у сервис-провайдера. 

Тогда схема архитектуры выглядела так
Тогда схема архитектуры выглядела так

Все сервисы запускали через Docker Compose, собирали и разворачивали с помощью GitLab CI/CD. Для упрощения деплоя очевидным решением было перейти на Managed Kubernetes в облаке, таким образом отдать на аутсорсинг часть инфраструктурных задач и облегчить поддержание инфраструктуры. Перенести в облако сразу всё с нашими объёмами выходило очень дорого, ибо воркеры потребляли очень много ресурсов. Так что нам пришлось изобретать варианты гибридной инфраструктуры: API и микросервисы пусть работают в облаке, а тяжеловесные базы и воркеры остаются на bare metal.

Мы не преследовали идею сразу и полностью распилить наш монолит — решили действовать постепенно. В наследство от первых монолитных версий платформы у нас оставался основной репозиторий, в котором в том числе лежали API для кабинета и некоторые внешние API‑функции: воркеры для фоновых задач, синхронизация данных. Но если хотя бы микросервисы переедут в облако, возможно, станет полегче? Оказалось, что всё было не так просто.

Первое облако и первые грабли

Первым поставщиком Managed Kubernetes стал провайдер, который предоставлял нам в аренду серверы. Это казалось отличным решением, так как ресурсы располагались бы в одном дата‑центре (да, на тот момент мы не разносили данные по разным ДЦ), и потери в сети были бы минимальными.

Для запуска сервисов в выбранном облаке мы создали 2 кластера Kubernetes: production и staging. При этом «на земле» у нас оставались 5 серверов (2 с БД, остальные под воркеры), где никакого Kubernetes, разумеется, не было.

Всем деплоем управлял пайплайн, который принимал на вход тег, подготавливал образы и обновлял окружение. Как вкратце выглядел процесс деплоя в гибридной конфигурации:

  1. Бастион, с которого происходит деплой, обращается к bare‑metal‑серверам через Docker Remote и останавливает воркеры.

  2. Мы выкатываем наш Helm Chart в облако, там запускаются миграции в виде pre‑upgrade job, мы приводим базы данных к необходимому состоянию и обновляем деплойменты для нашего API.

  3. После того как у нас выкатывается API, мы через Docker Remote опять выкатываем обновлённые воркеры.

На этом пути мы встретили сразу несколько трудностей.

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

  • 7–10 минут уходило на то, чтобы полностью остановить задачи.

  • Дальше применялась миграция, и код выкатывался в Kubernetes, на это требовалось 5–7 минут.

  • Потом на каждый сервер нужно было зайти через Docker Remote, спулить образы и запустить необходимые контейнеры. Это тоже занимало 7–10 минут, так как каждый сервер обновлялся по очереди.

Во‑вторых, из‑за невозможности использовать Kubernetes «на земле» мы не могли применить все возможности autodiscovery & failover. Контейнеры, которые деплоились с помощью Docker, были жёстко привязаны к серверу и сетевому порту. Если бы везде был Kubernetes или хотя бы Docker Swarm, можно было бы обращаться не к какому‑то серверу напрямую, а в настроенный endpoint c возможностями autodiscovery, что значительно облегчило бы задачи по балансировке нагрузки.

Но самой большой проблемой гибридного деплоя было даже не это. Поскольку мы обновляли воркеры через бастион‑хост с подключением Docker Remote, самую большую боль вызывало то, что Docker Remote достаточно часто попросту терял соединение с другой машиной. Когда контейнеры шли обновляться, соединение по какой‑то причине рвалось посреди процесса и деплой падал. Выяснить причину сразу не удалось ни самостоятельно, ни с подключением техподдержки (уже позже мы выяснили, что в этих падениях всему виной был баг Docker Compose, но открытый нами тикет до сих пор не решён).

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

  1. Он плохо поддерживал работу с Terraform для описания Infrastructure as a Сode. Если с описанием Kubernetes‑кластера много вопросов не возникало, то описать сетевую связность L3 между облаком и bare‑metal не предоставлялось возможным. Для решения этой задачи половину инфраструктуры (L3 связанность, сервисные аккаунты) приходилось настраивать через личный кабинет, половину через OpenStack CLI (роуты между облаком и железом).

  1. Не хватало возможностей RBAC. Разработчикам платформы периодически тоже нужен был доступ в облако: посмотреть, куда заехали ноды, как много у нас места и т. д. У нас не было возможности точечно выдавать личные аккаунты и ограничивать в них права, а у провайдера это можно было сделать только вручную в личном кабинете. Из‑за нехватки ресурсов команды DevOps и невозможности сконфигурировать ролевой доступ для всей группы разработки приходилось либо периодически выдавать root‑права, либо не пускать разработчика вообще. В итоге проблему решили с помощью самописного модуля Terraform и личных токенов, но осадочек остался.

  2. Совершенно неожиданно мы столкнулись с нехваткой ресурсов в облаке. Когда на старте мы выбирали зону доступности у провайдера, то нашли регион, который будет максимально близко расположен к bare‑metal‑серверам с нашими базами данных. В один прекрасный момент мы попытались докупить ноды в выбранной зоне для кластера Kubernetes и выяснили, что место в этом регионе банально закончилось. Оставалось 2 варианта решения, все с печальными последствиями:

    • переехать в другую зону доступности и смириться, что пинг до базы данных повышается;

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

    Ни тот, ни другой вариант нас не устраивал, и мы задумались о смене провайдера.

  3. Появились новые вводные, а именно 152-ФЗ + GDPR. Нам надо было запросить у провайдера подтверждение соответствия нормам обработки персональных данных. После общения со специалистами провайдера нам предложили переехать в аттестованный сегмент ЦОД, но при этом лишиться Managed Kubernetes и заплатить более высокую цену за серверы. Наверное, это и стало последней каплей.

Переезд в другое облако с новыми вводными

Формулируем требования. Первый опыт переезда в облако помог нам лучше понять наш запрос к сервис‑провайдеру. Во‑первых, мы больше не хотели собирать грабли гибридного деплоя, и решили постепенно переносить в облако больше элементов платформы, в том числе базы данных. А значит, нужно было облако, где поддерживаются все необходимые БД как сервис. Во‑вторых, мы стали внимательнее относиться к уровню сервиса провайдера: техподдержке, документации, нюансам SLA.

Раз мы решили перенести в облако не только Kubernetes c микросервисами, но и базы, то возник вопрос хранения персональных данных — как мы помним, в БД на bare metal у нас хранилось много пользовательской информации.

В итоге новая облачная платформа должна была:

  • отвечать требованиям GDPR и 152-ФЗ — в том числе мы обращали внимание на аттестацию облака;

  • предлагать несколько зон доступности с прозрачными условиями оказания услуг;

  • иметь инструменты для управления инфраструктурой при помощи Terraform;

  • иметь возможность ролевого управления доступом;

  • предоставлять в качестве сервиса PostgreSQL, ClickHouse, Redis, MongoDB, Kafka;

  • не сильно превышать по цене стоимость гибридной инфраструктуры.

Мы проанализировали информацию о разных провайдерах и склонялись в пользу Yandex Cloud. Чтобы предусмотреть возможные риски, составили такой план переезда:

  1. Развернуть в новом облаке препродакшен‑кластер, чтобы протестировать в нём работу баз и микросервисов

  2. После тестов выяснить все узкие места, возможно доработать наши сервисы.

  3. Если всё пройдёт успешно, организовать переезд продакшен‑кластера.

Процесс миграции со всеми тестами мы должны были уложить в месяц.

Описываем облако кодом. Подготавливать облако мы сразу начали с помощью Terraform. Тут здорово помогла подробная документация как самого облака, так и провайдера Terraform, а также видеоуроки на YouTube. В Yandex Cloud предусмотрена блокировка HashiCorp‑сайтов и предоставляется своё зеркало, которому можно доверять, что тоже было совсем не лишним.

Допиливаем разработку под облако. Первый пилотный этап сразу помог прочувствовать особенности работы БД в облаке, с которыми мы раньше не сталкивались на практике:

  1. База данных как сервис по сути не совсем принадлежит нам. Как только появляется разделение ответственности с провайдером, сразу возникают ограничения, чтобы клиент облака случайно не навредил своей же инфраструктуре. Соответственно, некоторые привычные способы работы становятся недоступны: нельзя вручную поправить схему, установить свои расширения, получить полный доступ к настройке бэкапов, скачать дампы и т. д..

  2. Производительность тех же самых ресурсов будет меньше, чем было на bare metal. Точно такие же диски, как были у нас, демонстрировали задержки, которых мы не ожидали, так как привыкли работать на своём сервере с NVME‑дисками, которые втыкаются напрямую в сервер.

Так что пришлось учитывать неизбежные сетевые задержки, оптимизировать конфигурацию железа и кое‑что рефакторить для наилучшего использования ресурсов.

Также нам нужно было учесть более строгие требования к безопасности. Когда машины жили во внутренней сети без возможности доступа извне, не нужно было думать об отдельной защите инстансов Redis, ClickHouse и т. д. Теперь же перед переездом нам потребовалось разобраться с авторизацией и сертификатами.

Докинуть сертификат было несложно, а вот от привычки использовать ресурсы на полную катушку пришлось избавляться. В процессе переезда обнаружили, что БД скидывает очень много данных на диск, и за сутки мы переписывали базу 12(!) раз полностью. Оказалось, что мы не совсем правильно работаем с реляционной базой данных и постоянно обновляем одно поле, что приводит к перезаписи всей строки.

Ещё одной проблемой стала более низкая частота процессоров, чем мы привыкли: чтобы её решить, пришлось ещё больше распараллелить задачи и использовать больше воркеров. Но если раньше воркеры у нас были включены всегда (и на железе нам это было не дорого), то такое же решение в облаке — непозволительное расточительство, которое обходится в копеечку. Поэтому мы внедрили для воркеров автоскейлер Keda и теперь запускаем поды только тогда, когда у нас действительно есть задачи.

---
{{- range $key, $value := .Values.deployments }}
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: {{ .name }}
  namespace: {{ $.Release.Namespace }}
  annotations:
    autoscaling.keda.sh/paused-replicas: "0"
spec:
  minReplicaCount: {{ .minReplicas }}
  maxReplicaCount: {{ .maxReplicas }}
  scaleTargetRef:
    kind: Deployment
    name: {{ $.Release.Name }}-{{ .name }}
  advanced:
  {{- toYaml $.Values.hpaConfig | nindent 4 }}
  {{- if .cooldownPeriod }}
  cooldownPeriod: {{ .cooldownPeriod }}
  {{- end }}
  triggers:
  {{- range $value.queues }}
  - type: rabbitmq
    metadata:
      protocol: http
      queueName: {{ .name }}
      mode: QueueLength
      value: "{{ .length }}"
    authenticationRef:
      name: keda-trigger-auth-rabbitmq-conncd
  {{- end }}
{{- end }}

Также для фоновых задач SLA у нас ниже, чем для того же самого API, поэтому мы можем ещё удешевить инфраструктуру путём предпочтения прерываемых нод для асинхронных воркеров.

  spec:
    affinity:
      nodeAffinity:
        requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                - key: yandex.cloud/preemptible
                  operator: In
                  values:
                  - "true"

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

Подобрать оптимальную конфигурацию также помог более широкий взгляд на возможности облака: часть задач мы вынести из managed‑сервисов в другие хранилища. Например, для медиафайлов по соотношению цена — качество больше подошло объектное хранилище в связке с CDN: туда переехали размещённые в сервисах фотографии от клиентов платформы, а также виджеты, которые размещаются на клиентских сайтах.

Осваиваем больше возможностей Terraform. Самое главное — в облаке поддерживались нужные Тerraform‑модули, что позволило нам наконец избавиться от многих рутинных операций. Мы начали с готового набора для Terraform и освоили модули yandex‑cloud/yandex, cloudflare/cloudflare, kubernetes, helm. Но этого нам показалось мало, и мы написали свои. Вот какие задачи решаются уже сейчас:

  • Описание кодом статичных приложений: NGINX Ingress, RabbitMQ, Cert Manager, Vault, Prometheus Operator и т. д.

  • Создание новой базы данных для нового микросервиса с генерацией пароля по нашим требованиям.

  • Создание поддоменов.

  • Создание неймспейса в K8s, токенов Vault, GitLab Container Registry и TLS‑секретов в нём.

  • Создание токенов и сервисных аккаунтов, а также самих бакетов S3.

Письмо себе в прошлое: какие уроки мы извлекли из переездов

Второй переезд снял ограничения, которые не давали нам масштабироваться, и позволил платформе вырасти в 2 раза. Также удалось улучшить показатели CI/CD: скорость доставки обновления для клиентов выросла с 15 до 5 минут.

Говорить об экономическом эффекте пока рано, но, по крайней мере, сейчас мы детально видим все расходы и через некоторое время сможем всё подсчитать. Как оказалось, когда большая часть инфраструктуры была на нашей стороне, многие издержки были невидимыми: например, сложно было оценить ad‑hoc затраты команды DevOps на решение незапланированных проблем с гибридным деплоем.

Какие выводы мы сделали из всей этой истории:

  1. Не экономить на спичках. Временные решения, которые призваны решить проблемы малой кровью, могут привести к неожиданным издержкам. Из‑за ограниченных ресурсов мы растягивали распил монолита во времени, но возникший потолок в масштабировании вынудил нас искать ресурсы для быстрого решения проблемы.

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

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

  4. Ценить управление доступом на основе ролей — чем больше объёмы, тем очевиднее необходимость создавать сервисные аккаунты и выдавать гранулярные права доступа разным группам.

Отдельный респект хочу выразить команде саппорта Yandex Cloud, которая поддерживала нас на всех этапах переезда и продолжает отвечать на наши вопросы во время эксплуатации облака.

Буду рад ответить на вопросы, связанные с Terraform и не только.

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