company_banner

Локальные файлы при переносе приложения в 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 {
            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, чтобы запустить генерацию новых. Однако даже после этого ссылки на старую статику будут неактуальными, что приведет к ошибкам отображения статики.

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

    • Отмасштабируем 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, которое принимает файлы через форму загрузки, что-то делает с ними в процессе работы и отдаёт обратно.

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

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


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

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


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

    Отчасти проблема решается включением stickySessions на ingress (фича поддерживается во всех популярных контроллерах ingress — подробнее см. в нашем обзоре), чтобы привязать пользователя к конкретному 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. Однако это — приведение приложения к stateless-виду — неизбежно означает, что потребуются изменения в коде, и тут важно найти баланс между возможностями/требованиями бизнеса и перспективами реализации и обслуживания выбранного пути.

    P.S.


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

    Флант
    716,68
    Специалисты по DevOps и Kubernetes
    Поделиться публикацией

    Комментарии 12

      –4
      Kubernetes ради kubernetes?
      Зачем изобретать велосипеды, если для решения этих задач можно использовать другие интрументы?
        0

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

          0

          Даже в рамках одного продукта, но построенного на базе SOA/MSA унификация полезна. Когда с десяток сервисов деплоятся и конфигурируются единообразно, то это очень упрощает жизнь, как в плане регулярных задач, так и плане человеческой ошибки.

          0
          Во введении не написано, что хранение файлов в Kubernetes является самоцелью. По сути статья описывает часть большей задачи — по контейнеризации и миграции приложения в Kubernetes. Если такой задачи нет, то начинать лучше с понимания, зачем оно (Kubernetes) вообще нужно. Может быть, действительно не нужно?.. Без понимания/согласия на этом верхнем уровне нет смысла говорить про велосипеды и сравнивать с другими инструментами (что бы вы под ними ни подразумевали).
            0

            Для многих задач k8s выглядит оверхедом, но альтернатива ему полумёртвый Docker Swarm — технически для многих задач подходит почти идеально, но выглядит как путь в никуда: рано или поздно потребуются какие-то фичи k8s.

          +1
          Складывается ощущение, что persistent storage это головная боль для k8s. При приличной нагрузке по iops решения вида Ceph, GlusterFS проигрывают «старомодным» решениям. Все-таки k8s в нынешнем виде очень тяжело готовить для statefull-приложений требовательных к дисковой системе.
            +2
            Да вроде бы, понятно, что делать с k8s + требованием по большому iops. Нужно использовать локальные диски серваков, и прибивать pod к ним.

            Как раз об этом Дмитрий рассказывал —

            0
            Есть ли какие-то рекомендации по поводу организации хранения для minio?
              0

              Как-то заметил в последнее время, что именно в k8s при формальных девизах 12f они нарушаются даже в примерах. Вот даже в посте: nginx конфигурируются через примонтированный конфиг, а не включением его в образ.


              P. S. В Symfony специально работают над воспроизводимостью билдов.:)

                +3
                а не включением его в образ.

                Нет. Включение конфига внутрь докер-образа будет прямым нарушением 3го фатора — про конфигурацию.

                В кубере есть kind: ConfigMap — конфиг хранится в etcd, и при старте пода он мапится в файл внутри пода. Это позволяет использовать один и тот же докер-образ с разными конфигами просто перезапуском пода
                0

                А как вариант с монтированием S3 в контейнеры через fuse? Буквально вчера столкнулся с идеей. А вообще был уверен, что для S3 в кубике если не из коробки есть драйвер, для storage class, то достаточно популярный. Увы :(

                  0

                  Ну если git volume диприкейтнули, то официального storage class под s3 наверное нет по схожим причинам… На практике же можно использовать инит контейнер с s3 клиентом и emptyDir

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое