- PVSM.RU - https://www.pvsm.ru -
При построении процесса CI/CD с использованием Kubernetes порой возникает проблема несовместимости требований новой инфраструктуры и переносимого в неё приложения. В частности, на этапе сборки приложения важно получить один образ, который будет использоваться во всех окружениях и кластерах проекта. Такой принцип лежит в основе правильного по мнению Google [1] управления контейнерами (не раз об этом говорил [2] и наш техдир).
Однако никого не увидишь ситуациями, когда в коде сайта используется готовый фреймворк, использование которого накладывает ограничения на его дальнейшую эксплуатацию. И если в «обычной среде» с этим легко справиться, в 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 {
listen 80;
server_name _;
charset utf-8;
root /var/www;
access_log /dev/stdout;
error_log /dev/stderr;
location / {
index index.php;
try_files $uri $uri/ /index.php?$args;
}
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, чтобы запустить генерацию новых. Однако даже после этого ссылки на старую статику будут неактуальными, что приведет к ошибкам отображения статики.
Кроме того, у нас скорее всего более-менее нагруженный проект, а значит — одной копии приложения не будет достаточно:
RollingUpdate
и в данный момент делаем деплой.В общем, итог — снова ошибки.
Чтобы не терять старые ассеты, можно изменить emptyDir
на hostPath
, складывая статику физически на узел кластера. Данный подход плох тем, что мы фактически должны привязаться к конкретному узлу кластера своим приложением, потому что — в случае переезда на другие узлы — директория не будет содержать необходимых файлов. Либо же требуется некая фоновая синхронизация директории между узлами.
Какие есть пути решения?
Помимо всего прочего, для всех вариантов создания постоянного хранилища потребуется фоновая очистка устаревших наборов файлов, накопленных за некий промежуток времени. Перед контейнерами с PHP можно поставить DaemonSet из кэширующих nginx, которые будут хранить копии ассетов ограниченное время. Это поведение легко настраивается с помощью proxy_cache
с глубиной хранения в днях или гигабайтах дискового пространства.
Объединение этого метода с упомянутыми выше распределенными файловыми системами даёт огромное поле для фантазий, ограничение лишь в бюджете и техническом потенциале тех, кто это будет реализовать и поддерживать. По опыту же скажем, что чем проще система, тем стабильнее она работает. При добавлении подобных слоёв поддерживать инфраструктуру становится гораздо сложнее, а вместе с этим увеличивается и время, затрачиваемое на диагностику и восстановление при любых отказах.
Если реализация предлагаемых вариантов хранилищ вам тоже кажется неоправданной (сложной, дорогой…), то стоит посмотреть на ситуацию с другой стороны. А именно — копнуть в архитектуру проекта и искоренить проблему в коде, привязавшись к какой-то статической структуре данных в образе, однозначное определение содержимого или процедуры «прогрева» и/или прекомпиляции ассетов на этапе сборки образа. Так мы получаем абсолютно предсказуемое поведение и одинаковый набор файлов для всех окружений и реплик запущенного приложения.
Если вернуться к конкретному примеру с фреймворком Yii и не углубляться в его устройство (что не является целью статьи), достаточно указать на два популярных подхода:
Другой кейс, который обязательно выстрелит при переносе приложения в кластер Kubernetes, — хранение пользовательских файлов в файловой системе. Например, у нас снова приложение на PHP, которое принимает файлы через форму загрузки, что-то делает с ними в процессе работы и отдаёт обратно.
Место, куда эти файлы должны помещаться, в реалиях Kubernetes должно быть общим для всех реплик приложения. В зависимости от сложности приложения и необходимости организации персистивности этих файлов, таким местом могут быть упомянутые выше варианты shared-устройств, но, как мы видим, у них есть свои минусы.
Одним из вариантов решения является использование S3-совместимого хранилища (пусть даже какую-то разновидность категории self-hosted вроде minio). Переход на работу с S3 потребует изменений на уровне кода, а как будет происходить отдача контента на фронтенде, мы уже писали [7].
Отдельно стоит отметить организацию хранения пользовательских сессий. Нередко это тоже файлы на диске, что в разрезе Kubernetes приведёт к постоянным запросам авторизации у пользователя, если его запрос попадёт в другой контейнер.
Отчасти проблема решается включением stickySessions
на ingress (фича поддерживается во всех популярных контроллерах ingress — подробнее см. в нашем обзоре [8]), чтобы привязать пользователя к конкретному pod’у с приложением:
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, но не должны «пустить корни».
Общий же рекомендуемый путь сводится к тому, чтобы избавиться от них в пользу архитектурной доработки приложения в соответствии с уже хорошо многим известным 12-Factor App [9]. Однако это — приведение приложения к stateless-виду — неизбежно означает, что потребуются изменения в коде, и тут важно найти баланс между возможностями/требованиями бизнеса и перспективами реализации и обслуживания выбранного пути.
Читайте также в нашем блоге:
Автор: xandr0s
Источник [12]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/sistemnoe-administrirovanie/333371
Ссылки в тексте:
[1] по мнению Google: https://habr.com/ru/company/flant/blog/425085/
[2] говорил: https://habr.com/ru/company/flant/blog/322686/
[3] cephfs: https://github.com/kubernetes/examples/blob/master/volumes/cephfs/cephfs.yaml
[4] Официальная документация: https://docs.ceph.com/docs/jewel/start/hardware-recommendations/
[5] yii2-static-assets: https://www.yiiframework.com/extension/sam-it/yii2-static-assets
[6] этой презентации: https://www.slideshare.net/petrabarus/scaling-yii2-app
[7] писали: https://habr.com/ru/company/flant/blog/426739/
[8] нашем обзоре: https://habr.com/ru/company/flant/blog/447180/
[9] 12-Factor App: https://12factor.net/ru/
[10] 7 принципов проектирования приложений, основанных на контейнерах: https://habr.com/ru/company/flant/blog/353272/
[11] 7 недостающих факторов в подходе 12 Factor App: https://habr.com/ru/company/flant/blog/460363/
[12] Источник: https://habr.com/ru/post/471582/?utm_campaign=471582&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.