Как стать автором
Обновить
568.4
Конференции Олега Бунина (Онтико)
Профессиональные конференции для IT-разработчиков

Теперь готовлю только так: перенос Drupal 8 в Kubernetes

Время на прочтение13 мин
Количество просмотров2K

Привет, Хабр! Я — Алексей Демьянов, тимлид DevOps-команды во «Фланте». В этой статье расскажу, как мы перевели пачку высоконагруженных новостных сайтов клиента на базе Drupal 8 в Kubernetes в облаке. А именно о том, какие шаги предпринять, чтобы успешно мигрировать монолит в Kubernetes, увеличить перформанс, экономить на инфраструктуре благодаря HPA и решить проблему с DDoS-атаками.

Зачем тащить монолит в Kubernetes

Часто к нам обращаются клиенты, у которых где-то в пыльном чулане в коробке стоит монолит. В этот раз так и произошло. Пришли ребята с пачкой новостных сайтов на Drupal. Я сразу предложил переезд в Kubernetes, но услышал привычное: «Зачем нам это? Мы и так справляемся».

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

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

Следующий логичный вопрос: можно ли запустить конкретный монолит, в нашем случае на Drupal, в Kubernetes? Можно, но… Если просто взять монолит и перенести его в Kubernetes без учёта нюансов, можно оказаться в неприятной ситуации и потратить кучу денег на ресурсы в облаке. Это если бы к мужчине пришла его жена и сказала что-то вроде: «Ты потратил все деньги на EC2, теперь мы бездомные».

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

Так, основные сложности, с которыми можно столкнуться в этой задаче, — это расходы на ресурсы, проблемы с производительностью Drupal и вопросы размещения компонентов. Конечно, архитектура была простой, пока всё работало на одной железке.

Архитектура типичного монолита

Давайте вспомним, что же такое этот Drupal. Drupal — монолит на PHP. Свой контент и настройки он хранит в MySQL, на файловой системе валяются папки с модулями, какая-то статика и так далее. Раздаётся это всё nginx или Apache.

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

Когда репутационные издержки от простаивающего сайта становятся высокими, рядом часто ставят резервный сервер, на который в случае чего можно «быстро» переключиться.

Соответственно, возникает ситуация: когда нет трафика, две железки просто стоят и греют воздух, а клиент за это платит.

Какие проблемы мы получаем в такой конфигурации:

  • неконсистентная инфраструктура — на разных серверах могут быть разные версии операционных систем, софта и так далее;

  • отсутствие Health Checks — нет проверки работоспособности приложения;

  • трудности с обновлением;

  • отсутствие CI/CD;

  • масштабирование только вертикальное;

  • отсутствие изоляции сервисов друг от друга;

  • отказоустойчивость условная (требует ручного вмешательства);

  • отсутствие автоматизации инфраструктуры;

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

  • плохой перформанс;

  • переплата за ресурсы.

Дальше покажу, как это решить за счёт перехода в Kubernetes.

Примечание: все перечисленные выше проблемы можно решить и без Kubernetes, например в AWS, если настроить скейлинг виртуалок с бэком и раскидывать трафик через loadbalancer. Также можно упаковать всё в Docker и доставлять какими-то видами автоматизации в ВМ. Такой подход будет рабочим, но надо помнить, что это: 

  • вендорлок;

  • кастом, который будет трудно поддерживать.

Едем в Kubernetes: сборка

Мы едем в Kubernetes в облаке. И начнём со сборки.

Тут всё по классике, пишем Docker-файл. На первый взгляд, можно было бы просто взять официальный образ Drupal с Docker Hub. Но это не лучший вариант: во-первых, образ слишком большой, во-вторых, он идёт в комплекте с Apache. А ещё там куча всего, что не нужно нам в финальном образе — PHPIZE_DEPS (gcc, composer). Это снижает безопасность образа:

FROM drupal:latest
COPY . /var/www/html/
RUN chown -R www-data:www-data /var/www/html/sites/default/files \
    && chmod -R 775 /var/www/html/sites/default/files
ENV APACHE_DOCUMENT_ROOT=/var/www/html/web
RUN sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf \
    /etc/apache2/conf-available/*.conf
EXPOSE 80
CMD ["apache2-foreground"]

Лучше использовать базовый контейнер, например Ubuntu или Debian, и всё необходимое установить при сборке образа. Размер сразу уменьшится, а бонусом мы получим контроль над необходимыми зависимостями.

FROM ubuntu:22.04
RUN apt-get update && apt-get install -y \
    php8.1-fpm \
    php8.1-cli \
    php8.1-mysql \
    php8.1-gd \
    php8.1-curl \
    php8.1-intl \
    php8.1-bcmath \
    php8.1-soap \
    php8.1-zip \
    php8.1-xml \
    curl \
    git \
    unzip \
    zip
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
COPY . /var/www/html/
RUN chown -R www-data:www-data /var/www/html/sites/default/files \
    && chmod -R 775 /var/www/html/sites/default/files

Однако если у клиента N сайтов, тогда нам придётся поддерживать N Docker-файлов. Когда в Docker-файл нужно что-то добавить, понадобится сходить в N репозиториев и внести одно и то же изменение, что повышает вероятность ошибки. Поэтому в качестве инструмента сборки мы выбрали утилиту werf: она позволяет использовать шаблоны и циклы в сборке.

{{- define "site_build" }}
  {{- $project := index . 1 }}
  {{- $branchtag := index . 2 }}
  {{- $arg_arr := . }}
  {{- with index . 0 }}
---
# Backend image. Drupal
image: drupal-{{ $project }}
fromImage: drupal-base-compiled

После того как у нас появился контейнер с Drupal, описываем Helm-чарты и получаем вот такой под:

Обязательно добавляем контейнер php_fpm_exporter в под, он нам пригодится. Также нужен nginx, чтобы раздавать статику и выполнять graceful shutdown бэкенда, ну и выполнять fastcgi_pass в контейнер с PHP.

Примечание: если вынести контейнер с nginx в отдельный под, который будет обслуживать несколько подов с PHP, то может возникнуть ситуация, когда на перекате приложения будут возникать 500-е ошибки вследствии того, что запрос обрабатывался бэкендом, когда происходил перекат.

После того как у нас появилась сборка Docker-образов и были описаны Helm-чарты, мы сразу закрываем несколько пунктов из списка проблем:

  • устранили неконсистентность — зависимости описаны в Dockerfile;

  • добавили Health Checks;

  • автоматизировали обновления;

  • внедрили CI.

werf существенно упрощает CI. Это инструмент не только сборки, но и деплоя. Вместо сложного CI/CD, включающего стадии docker build, helm install и другие, у нас работают три инструкции: werf build, werf deploy, werf cleanup — очень удобно. На последнем этапе werf сходит в registry и подчистит неиспользуемые в кластере Docker-образы.

Что делать со статикой

На том же сервере, где раньше жил Drupal, хранились файлы. Когда мы поедем в Kubernetes, они должны быть распределены на несколько реплик. Тут есть три варианта:

  1. Самый простой вариант — настроить NFS и монтировать его прямо в поды.

  2. Если вы готовы заморочиться, поставить Ceph и использовать CephFS.

  3. Если есть ресурсы у разработчиков, настроить модуль взаимодействия с S3.

Мы пошли по пути наименьшего сопротивления и выбрали NFS с прицелом на то, что в будущем утащим всё в S3.

Выкатываем в Kubernetes

Итак, мы заказали кластеры, настроили базы на ВМ либо взяли managed-решение, подключили NFS к подам прямо в деплойментах. Получилась такая схема:

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

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

Но всё не так плохо, мы устранили ещё несколько проблем:

  • устранили неконсистентность;

  • добавили Health Checks;

  • автоматизировали обновления;

  • внедрили CI;

  • реализовали горизонтальное масштабирование;

  • изолировали сервисы друг от друга;

  • повысили отказоустойчивость;

  • настроили автоматизацию инфраструктуры;

  • развернули тестовый контур, идентичный проду.

Остались две большие проблемы: перформанс и переплата за ресурсы. Поборемся и с ними!

Лечим перформанс: кэширование

Что делать, если сайт работает медленно? Внедрить кэши! Drupal «из коробки» не умеет работать с Redis Sentinel, поэтому нужно добавить прослойку-прокси redis-proxy.

Обычно мы выкатываем оператор Redis, например Spotahome Redis Operator, и потом ресурс Redis failover, а оператор выкатывает три пода Redis и настраивает репликацию:

Но вот что произойдёт, если Drupal начнёт активно использовать ресурсы на том же узле:

  1. Мастер начнёт тормозить и перестанет отвечать на пробы.

  2. Оператор запустит процедуру выбора нового мастера.

  3. Пока идёт переключение, бэкенд будет возвращать пятисотые ошибки.

Чтобы этого избежать, лучше вынести Redis в отдельную группу узлов:

Такая конфигурация хорошо работает до тех пор, пока по каким-то причинам не случится failover и не запустится процедура перевыбора мастера. Поэтому лучшим решением будет использовать managed Redis или self-hosted Redis рядом с кластером, а не тащить Redis внутрь кластера, особенно для Drupal. Потому что при каждом запросе к сайту Drupal делает 100500 обращений в Redis.

С внедрением Redis производительность улучшилась: поды Drupal теперь держат не один RPS, а два. Это классно, но недостаточно — внедряем кэш на nginx.

Кэширование в nginx

Самое очевидное решение — вставить кэш прямо в под с Drupal. Если сделать так, то при рестарте хотя бы одного из подов кэш очистится и 33 % трафика пойдёт мимо кэшей. Значит, треть запросов будет работать медленно:

Явный выход — добавить persistence. Очевидным способом было бы хранение кэша в NFS, ведь мы всё равно уже развернули его:

Проблема в том, что если вдруг NFS выйдет из строя или возникнут сетевые проблемы, то все поды бэка упадут:

Поэтому мы выкатим StatefulSet (STS) nginx-cache на отдельную группу узлов. Хранить кэш будем на Persistent Volume и добавим hard anti-affinity между подами кэша. Иначе, если кэш-поды окажутся на одном узле и тот выйдет из строя, мы получим полную недоступность:

Замечательно, теперь мы кэшируем все запросы, которые приходят. Но нужно решить проблему попадания редакторов контента в админские поды. И bypass кэша при обращении к ним, это делается в конфиге nginx:

—
#if cookie exist do not cache                                                                                                                                                                               
    map $cookie_{{ pluck .Values.werf.env .Values.drupal.cookie_name | first }} $no_cache {                                                                                                                     
        "" 0;                                                                                                                                                                                                     
        "~.+" 1; }   
—
        proxy_cache_bypass $no_cache;
—  

Теперь редакторы могут работать.

Примечание: cookie и другая чувствительная информация должны храниться в зашифрованном виде, например в secret-values.yaml, зашифрованном с помощью werf.

В итоге у нас получилась такая схема. Пройдёмся по ней:

Фронтовые узлы — это узлы с Ingress-контроллером. За ним запрос идёт в кэши. Если в кэше пусто (MISS), то запрос отправляется в поды PHP, где формируется ответ клиенту, а также происходит кэширование. На отдельных ВМ находятся Redis, NFS и другие необходимые сервисы. В итоге перформанс значительно улучшился.

И осталась последняя беда — переплата за ресурсы. Дело в том, что несмотря на кэши какая-то часть запросов всё равно долетает до бэка. И поэтому нам надо всегда иметь необходимое и достаточное количество подов бэка и, соответственно, узлов в кластере. А мы хотим экономить, чтобы, когда трафика нет, количество подов уменьшалось и кластер отказывался от незагруженных узлов. Поэтому будем бороться. Для этого внедряем Horizontal Pod Autoscaler (HPA).

Лечим переплату за ресурсы: автоскейлинг

Чтобы настроить автоматическое масштабирование, сначала определим, какие метрики использовать:

  • CPU;

  • RPS;

  • php-fpm-exporter.

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

Можно мерить RPS на Ingress-контроллер. Но мы же поставили STS nginx-cache, который будет обслуживать бóльшую часть запросов, и только небольшое их количество будет попадать в поды бэкенда. Эта метрика также не будет отражать необходимость скейлить поды с бэкэндом.

RPS также можно измерить в nginx в поде. Это логичнее, но тоже не совсем корректно, потому что какие-то запросы могут даже не долетать до PHP (например, когда nginx раздаёт картинку).

Поэтому лучше всего будет измерять прямо на уровне PHP. Для этого подойдёт php-fpm-exporter.

В настройках php-fpm мы выставили статичное количество воркеров (pm = static pm.max_children = n). Измеряем количество утилизированных, то есть занятых обработкой запроса в данный момент, воркеров из php-fpm-exporter, умножаем на 100, получаем проценты:

(php_fpm_process_total / php_fpm_process) × 100 > 60

Если больше 60 % воркеров утилизировано, запускаем скейлинг. Выглядит это так:

  1. HPA увидел, что утилизация больше 60 %, запустил ещё один под:

  1. Кластер автоскейлер дозаказал дополнительный узел:

В итоге вместе с HPA получается следующая схема:

Поскольку сайтов больше одного, разные пространства имён и метрики скейлят бэк каждого сайта независимо друг от друга. Это круто, мы перестали переплачивать за ресурсы. Но кажется, кое-что забыли…

CronJobs: как организовать в Kubernetes

Обычно про CronJobs вспоминают в самом конце. Важно, что когда CronJobs работают на той же машине, что и бэкенд, то они обязательно разрастутся и начнут потреблять память. И в неподходящий момент OOM Killer завершит какой-то важный процесс. Чтобы этого не случилось, выносим все CronJob в отдельные поды, используя kind CronJob:

Если CronJob начнёт потреблять весь CPU на узле, это скажется на работе бэкенда: сайт будет тормозить и медленно отдавать страницы.

Поэтому вытащим и его на отдельные узлы, а ещё лучше — используем спотовые узлы, чтобы экономить деньги:

В результате после всех манипуляций у нас добавилась ещё одна группа узлов, на которой будут запускаться CronJobs, и для каждого из сайтов будет отдельный узел:

Настройка CronJob — холиварная тема. По умолчанию мы не выставляем лимиты по CPU, потому что за этим надо следить и каждый раз тюнить вручную. А если CronJob достигнет лимита по CPU, Kubernetes начнёт тротлить процесс, что может привести к непредсказуемому поведению. Поэтому проще вытащить их на отдельную группу узлов.

По итогу мы закрыли все вопросы из списка: 

  • устранили неконсистентность;

  • добавили Health Checks;

  • автоматизировали обновления;

  • внедрили CI;

  • реализовали горизонтальное масштабирование;

  • изолировали сервисы друг от друга;

  • повысили отказоустойчивость;

  • настроили автоматизацию инфраструктуры;

  • развернули тестовый контур, идентичный проду;

  • улучшили перформанс системы;

  • оптимизировали затраты и устранили переплату за ресурсы.

Вроде бы всё круто, но однажды…

Защита от DDoS

Многие помнят вечер пятницы, когда во время отдыха прилетают алерты от мониторинга о недоступности сайтов. Бросаем всё, открываем Grafana, а там — ровная полка: 5000 RPS на Ingress-контроллерах. Это DDoS.

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

Помимо этого, HPA увидел, что до бэка дошло много запросов, количество подов увеличилось, и получается, что клиент платит ещё и за то, что его DDoS’ят.

Чтобы решить эти проблемы, залезаем под защиту от DDoS.

На первый взгляд, стало получше: пользователи получают доступ к сайту, но клиент всё равно переплачивает — теперь в том числе за полосу легитимного трафика на защите от DDoS:

Чтобы этого избежать, вводим два Ingress-контроллера: один нацелен на защиту от DDoS, второй — прямой (direct). Переключаемся между ними с помощью DNS:

К такому решению есть вопросы:

  1. Переключение через DNS — долго.

  2. Постоянно сидеть под защитой — дорого.

  3. Оставшийся коммунальный direct-контроллер всё ещё создаёт риски: если на один сайт идёт много трафика, это может затронуть остальные сайты.

Поэтому мы запускаем ещё Ingress-контроллеры, вешаем на них разные Ingress-классы и уводим каждый сайт за свой контроллер. Получается, у каждого сайта есть прямой контроллер, куда мы попадаем через load balancer в облаке, и ещё один для защиты от DDoS.

В итоге получается такая схема:

Здесь один на все сайты узел для защиты от DDoS и для каждого свои:

  • фронт-узлы с direct Ingress Controller;

  • кэши;

  • HPA;

  • хранилище (база данных, Redis и т. д.).

Итого

В результате переезда монолита на Kubernetes мы починили все проблемы, которые были в классической инсталляции Drupal:

  • устранили неконсистентность;

  • добавили Health Checks;

  • автоматизировали обновления;

  • внедрили CI;

  • реализовали горизонтальное масштабирование;

  • добились полной изоляции сервисов;

  • повысили отказоустойчивость;

  • настроили автоматизацию инфраструктуры;

  • развернули тестовый контур, идентичный проду;

  • улучшили перформанс системы;

  • оптимизировали затраты и устранили переплату за ресурсы;

  • добавили DDoS-защиту для всех сайтов.

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

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

Репутационные издержки снизились. Раньше, если сервер падал, клиенты ждали, пока придёт админ и всё восстановит. В Kubernetes система автоматически лечится, поды перезапускаются и время простоя минимально.

Стоимость поддержки выше. Чтобы поддерживать сервер с Drupal, хватало одного админа. Теперь нужны специалисты, которые разбираются в Helm, Kubernetes и других инструментах. Если у вас таких специалистов нет, вы можете обратиться к тем, кто этим занимается.

Схема, которую мы построили для Drupal, с небольшими доработками отлично ложится на любые монолиты: Laravel, Symfony, Java, Django, Ruby. Словно селёдка под шубой — универсальный рецепт, который всегда работает. Теперь готовим только так.

Теги:
Хабы:
Всего голосов 24: ↑24 и ↓0+27
Комментарии3

Публикации

Информация

Сайт
www.ontico.ru
Дата регистрации
Дата основания
Численность
51–100 человек
Местоположение
Россия