Лакальныя файлы пры пераносе прыкладання ў Kubernetes

Лакальныя файлы пры пераносе прыкладання ў Kubernetes

Пры пабудове працэсу CI/CD з выкарыстаннем Kubernetes часам узнікае праблема несумяшчальнасці патрабаванняў новай інфраструктуры і пераноснага ў яе прыкладання. У прыватнасці, на этапе зборкі дадатку важна атрымаць 1 вобраз, які будзе выкарыстоўвацца ва ўсіх акружэннях і кластарах праекта. Такі прынцып ляжыць у аснове правільнага на думку 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

Дадаць каментар