Локальні файли при перенесенні програми в Kubernetes

Локальні файли при перенесенні програми в Kubernetes

При побудові процесу CI/CD з використанням Kubernetes часом виникає проблема несумісності вимог нової інфраструктури та програми, що переноситься до неї. Зокрема, на етапі складання програми важливо отримати один образ, який використовуватиметься в всіх оточення та кластери проекту. Такий принцип лежить в основі правильного на думку Google управління контейнерами (не раз про це говорив і наш техдир).

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

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

Зберігання статики

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

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

Чим це загрожує у розрізі кластера?

Найпростіший приклад

Візьмемо досить поширений кейс, коли перед PHP стоїть nginx для роздачі статики та обробки простих запитів. Найпростіший спосіб - розгортання з двома контейнерами:

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, щоб запустити генерацію нових. Однак, навіть після цього посилання на стару статику будуть неактуальними, що призведе до помилок відображення статики.

Крім того, у нас швидше за все більш-менш навантажений проект, а отже — однієї копії програми не буде достатньо:

  • Відмасштабуємо розгортання до двох реплік.
  • При першому зверненні до сайту в одній репліці утворилися асети.
  • Якогось моменту 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-виду — неминуче означає, що будуть потрібні зміни в коді, і тут важливо знайти баланс між можливостями/вимогами бізнесу та перспективами реалізації та обслуговування обраного шляху.

PS

Читайте також у нашому блозі:

Джерело: habr.com

Додати коментар або відгук