Хабрахабр

Локальные файлы при переносе приложения в Kubernetes

В частности, на этапе сборки приложения важно получить один образ, который будет использоваться во всех окружениях и кластерах проекта. При построении процесса CI/CD с использованием Kubernetes порой возникает проблема несовместимости требований новой инфраструктуры и переносимого в неё приложения. Такой принцип лежит в основе правильного по мнению Google управления контейнерами (не раз об этом говорил и наш техдир).

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

Разберем популярные workaround-решения для хранения файлов, которые могут привести к неприятным последствиям при эксплуатации кластера, а также укажем на более правильный путь.

Хранение статики

Для иллюстрации рассмотрим веб-приложение, которое использует некий генератор статики для получения набора картинок, стилей и прочего. Например, в PHP-фреймворке Yii есть встроенный менеджер ассетов, который генерирует уникальные названия директорий. Соответственно, на выходе получается набор заведомо не пересекающихся между собой путей для статики сайта (сделано это по нескольким причинам — например, для исключения дубликатов при использовании одного и того же ресурса множеством компонентов). Так, из коробки, при первом обращении к модулю веб-ресурса происходит формирование и раскладывание статики (на самом деле — зачастую симлинков, но об этом позже) с уникальным для данного деплоя общим корневым каталогом:

  • webroot/assets/2072c2df/css/…
  • webroot/assets/2072c2df/images/…
  • webroot/assets/2072c2df/js/…

Чем это чревато в разрезе кластера?

Простейший пример

Возьмем довольно распространенный кейс, когда перед PHP стоит nginx для раздачи статики и обработки простых запросов. Самый простой способ — Deployment с двумя контейнерами:

apiVersion: apps/v1
kind: Deployment
metadata: name: site
spec: selector: matchLabels: component: backend template: metadata: labels: component: backend spec: volumes: - name: nginx-config configMap: name: nginx-configmap containers: - name: php image: own-image-with-php-backend:v1.0 command: ["/usr/local/sbin/php-fpm","-F"] workingDir: /var/www - name: nginx image: nginx:1.16.0 command: ["/usr/sbin/nginx", "-g", "daemon off;"] volumeMounts: - name: nginx-config mountPath: /etc/nginx/conf.d/default.conf subPath: nginx.conf

В упрощенном виде конфиг nginx сводится к следующему:

apiVersion: v1
kind: ConfigMap
metadata: name: "nginx-configmap"
data: nginx.conf: | server location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; include fastcgi_params; } }

При первом обращении к сайту в контейнере с PHP появляются ассеты. Но в случае с двумя контейнерами в рамках одного pod’а — nginx ничего не знает об этих файлах статики, которые (согласно конфигурации) должны отдаваться именно им. В результате, на все запросы к CSS- и JS-файлам клиент увидит ошибку 404. Самым простым решением тут будет организовать общую директорию к контейнерам. Примитивный вариант — общий emptyDir:

apiVersion: apps/v1
kind: Deployment
metadata: name: site
spec: selector: matchLabels: component: backend template: metadata: labels: component: backend spec: volumes: - name: assets emptyDir: {} - name: nginx-config configMap: name: nginx-configmap containers: - name: php image: own-image-with-php-backend:v1.0 command: ["/usr/local/sbin/php-fpm","-F"] workingDir: /var/www volumeMounts: - name: assets mountPath: /var/www/assets - name: nginx image: nginx:1.16.0 command: ["/usr/sbin/nginx", "-g", "daemon off;"] volumeMounts: - name: assets mountPath: /var/www/assets - name: nginx-config mountPath: /etc/nginx/conf.d/default.conf subPath: nginx.conf

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

Более продвинутое хранилище

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

Кроме того, у нас скорее всего более-менее нагруженный проект, а значит — одной копии приложения не будет достаточно:

  • Отмасштабируем Deployment до двух реплик.
  • При первом обращении к сайту в одной реплике создались ассеты.
  • В какой-то момент ingress решил (в целях балансировки нагрузки) отправить запрос на вторую реплику, и там этих ассетов еще нет. А может быть, их там уже нет, потому что мы используем RollingUpdate и в данный момент делаем деплой.

В общем, итог — снова ошибки.

Данный подход плох тем, что мы фактически должны привязаться к конкретному узлу кластера своим приложением, потому что — в случае переезда на другие узлы — директория не будет содержать необходимых файлов. Чтобы не терять старые ассеты, можно изменить emptyDir на hostPath, складывая статику физически на узел кластера. Либо же требуется некая фоновая синхронизация директории между узлами.

Какие есть пути решения?

  1. Если железо и ресурсы позволяют, можно воспользоваться cephfs для организации равнодоступной директории под нужды статики. Официальная документация рекомендует SSD-диски, как минимум трёхкратную репликацию и устойчивое «толстое» подключение между узлами кластера.
  2. Менее требовательным вариантом будет организация NFS-сервера. Однако тогда нужно учитывать возможное повышение времени отклика на обработку запросов веб-сервером, да и отказоустойчивость оставит желать лучшего. Последствия же отказа катастрофичны: потеря mount’а обрекает кластер на гибель под натиском нагрузки LA, устремляющейся в небо.

Помимо всего прочего, для всех вариантов создания постоянного хранилища потребуется фоновая очистка устаревших наборов файлов, накопленных за некий промежуток времени. Перед контейнерами с PHP можно поставить DaemonSet из кэширующих nginx, которые будут хранить копии ассетов ограниченное время. Это поведение легко настраивается с помощью proxy_cache с глубиной хранения в днях или гигабайтах дискового пространства.

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

Рекомендация

Если реализация предлагаемых вариантов хранилищ вам тоже кажется неоправданной (сложной, дорогой…), то стоит посмотреть на ситуацию с другой стороны. А именно — копнуть в архитектуру проекта и искоренить проблему в коде, привязавшись к какой-то статической структуре данных в образе, однозначное определение содержимого или процедуры «прогрева» и/или прекомпиляции ассетов на этапе сборки образа. Так мы получаем абсолютно предсказуемое поведение и одинаковый набор файлов для всех окружений и реплик запущенного приложения.

Если вернуться к конкретному примеру с фреймворком Yii и не углубляться в его устройство (что не является целью статьи), достаточно указать на два популярных подхода:

  1. Изменить процесс сборки образа с тем, чтобы размещать ассеты в предсказуемом месте. Так предлагают/реализуют в расширениях вроде yii2-static-assets.
  2. Определять конкретные хэши для каталогов ассетов, как рассказывается, например, в этой презентации (начиная со слайда №35). Кстати, автор доклада в конечном счёте (и не без оснований!) советует после сборки ассетов на build-сервере загружать их в центральное хранилище (вроде S3), перед которым поставить CDN.

Загружаемые файлы

Другой кейс, который обязательно выстрелит при переносе приложения в кластер Kubernetes, — хранение пользовательских файлов в файловой системе. Например, у нас снова приложение на PHP, которое принимает файлы через форму загрузки, что-то делает с ними в процессе работы и отдаёт обратно.

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

Рекомендация

Одним из вариантов решения является использование S3-совместимого хранилища (пусть даже какую-то разновидность категории self-hosted вроде minio). Переход на работу с S3 потребует изменений на уровне кода, а как будет происходить отдача контента на фронтенде, мы уже писали.

Пользовательские сессии

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

в нашем обзоре), чтобы привязать пользователя к конкретному pod’у с приложением: Отчасти проблема решается включением stickySessions на ingress (фича поддерживается во всех популярных контроллерах ingress — подробнее см.

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata: name: nginx-test annotations: nginx.ingress.kubernetes.io/affinity: "cookie" nginx.ingress.kubernetes.io/session-cookie-name: "route" nginx.ingress.kubernetes.io/session-cookie-expires: "172800" nginx.ingress.kubernetes.io/session-cookie-max-age: "172800" spec: rules: - host: stickyingress.example.com http: paths: - backend: serviceName: http-svc servicePort: 80 path: /

Но это не избавит от проблем при повторных деплоях.

Рекомендация

Более правильным способом будет перевод приложения на хранение сессий в memcached, Redis и подобных решениях — в общем, полностью отказаться от файловых вариантов.

Заключение

Рассматриваемые в тексте инфраструктурные решения достойны применения только в формате временных «костылей» (что более красиво звучит на английском как workaround). Они могут быть актуальны на первых этапах миграции приложения в Kubernetes, но не должны «пустить корни».

Однако это — приведение приложения к stateless-виду — неизбежно означает, что потребуются изменения в коде, и тут важно найти баланс между возможностями/требованиями бизнеса и перспективами реализации и обслуживания выбранного пути. Общий же рекомендуемый путь сводится к тому, чтобы избавиться от них в пользу архитектурной доработки приложения в соответствии с уже хорошо многим известным 12-Factor App.

P.S.

Читайте также в нашем блоге:

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»