Fișierele locale la mutarea aplicației în Kubernetes

Fișierele locale la mutarea aplicației în Kubernetes

Atunci când construim un proces CI/CD folosind Kubernetes, uneori apare problema incompatibilității dintre cerințele noii infrastructuri și aplicația transferată în aceasta. În special, în etapa de construire a aplicației, este important să obținem una o imagine care va fi utilizată în tuturor medii și clustere de proiect. Acest principiu stă la baza unei corecte conform Google gestionări a containerelor (de multe ori s-a discutat despre aceasta a menționat și tehnicul nostru).

Însă nu poți să nu remarci situațiile în care în codul site-ului este utilizat un cadru gata făcut, utilizarea căruia impune restricții asupra exploatării sale ulterioare. Și dacă într-o "mediocre" mediu este ușor să gestionezi asta, în Kubernetes un astfel de comportament poate deveni o problemă, în special atunci când te confrunți cu aceasta pentru prima dată. Deși o minte ingenioasă poate propune soluții infrastructurale care par evidente și chiar bune la prima vedere… este important să ne amintim că cele mai multe situații pot și trebuie să fie rezolvate arhitectural.

Vom analiza soluții populare de workaround pentru stocarea fișierelor, care pot duce la consecințe neplăcute în exploatarea clusterei, precum și vom indica un drum mai corect.

Stocarea staticelor

Pentru ilustrarea acestui lucru, să analizăm o aplicație web care utilizează un anumit generator de statice pentru a obține un set de imagini, stiluri și altele. De exemplu, în cadrul PHP-ului Yii există un manager de resurse încorporat care generează denumiri unice pentru directoare. În consecință, la ieșire se obține un set de căi care, prin natura lor, nu se suprapun (acest lucru se face din mai multe motive — de exemplu, pentru a exclude duplicatele în utilizarea aceleași resurse de către mai multe componente). Astfel, din cutie, la prima accesare a modulului resursei web se face formarea și distribuirea staticelor (de fapt — adesea simboluri legate, dar despre aceasta vom discuta mai târziu) cu un director rădăcină comun unic pentru acest deployment:

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

Ce înseamnă aceasta în contextul clusterei?

Cel mai simplu exemplu

Să luăm un caz destul de răspândit, când nginx stă înainte de PHP pentru distribuirea staticelor și procesarea cererilor simple. Cel mai simplu mod este Deployment cu două containere:

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

În termeni simplificați, configurația nginx se rezumă la următoarele:

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;
        }
    }

La prima accesare a site-ului în containerul cu PHP apare resursele. Dar în cazul a două containere într-un singur pod — nginx nu știe nimic despre aceste fișiere statice, care (conform configurației) ar trebui să fie livrate de către el. Ca rezultat, la toate cererile pentru fișiere CSS și JS, clientul va vedea eroarea 404. Cea mai simplă soluție aici va fi organizarea unui director comun pentru containere. O variantă primitivă — un director comun 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

Acum fișierele statice generate în container sunt livrate corect de către nginx. Dar să reamintesc, că aceasta este o soluție primitivă, ceea ce înseamnă că este departe de a fi ideală și are nuanțe și neajunsuri, despre care voi vorbi mai jos.

Un depozit mai avansat

Să presupunem că utilizatorul accesează site-ul, încarcă pagina cu stilurile existente în container, iar în timp ce citește această pagină, reluăm desfășurarea containerului. În directorul de asseturi devine gol și este necesar un apel PHP pentru a genera altele noi. Totuși, chiar și după aceasta, linkurile către vechea statica vor fi nevalabile, ceea ce va duce la erori de afișare a staticii.

În plus, cel mai probabil avem un proiect oarecum solicitant, așa că o singură copie a aplicației nu va fi suficientă:

  • Scalăm Deployment la două replici.
  • La prima accesare a site-ului, în una dintre replici s-au creat asseturi.
  • La un moment dat, ingress-ul a decis (în scopul echilibrării sarcinii) să trimită un apel către a doua replică, și acolo aceste asseturi nu sunt încă disponibile. Sau poate că nu sunt disponibile, deoarece folosim RollingUpdate și în acest moment facem desfășurarea.

În concluzie, apare din nou erori.

Pentru a nu pierde asseturile vechi, putem schimba emptyDir pe hostPath, stocând statică fizic pe nodul clusterului. Această abordare este proastă deoarece trebuie să ne legăm de un anumit nod al clusterului cu aplicația noastră, deoarece – în cazul mutării pe alte noduri – directorul nu va conține fișierele necesare. Sau este necesară o oarecare sincronizare în fundal a directorului între noduri.

Ce soluții există?

  1. Dacă hardware-ul și resursele permit, putem utiliza cephfs pentru a organiza un director de acces egal pentru nevoile de statică. Documentația oficială recomandă discuri SSD, replicare de cel puțin trei ori și o conexiune robustă „groasă” între nodurile clusterului.
  2. O variantă mai puțin solicitantă ar fi organizarea unui server NFS. Totuși, atunci trebuie să luăm în considerare posibila creștere a timpului de răspuns la procesarea cererilor de către serverul web, iar rezistența la erori va lăsa de dorit. Consecințele unei defecțiuni sunt catastrofale: pierderea montării condamnă clusterul la moarte sub atacul sarcinii LA, care se îndreaptă spre cer.

Pe lângă toate acestea, pentru toate opțiunile de creare a unui stocaj persistent va fi necesară curățarea în fundal seturilor de fișiere învechite, acumulate pe o anumită perioadă de timp. În fața containerelor cu PHP putem plasa DaemonSet de la nginx-uri cache, care vor stoca copii ale asseturilor pentru o perioadă limitată. Acest comportament se configurează ușor cu ajutorul proxy_cache cu o adâncime de stocare în zile sau gigaocteți de spațiu pe disc.

Combinarea acestei metode cu sistemele de fișiere distribuite menționate mai sus oferă un câmp vast de imaginație, limitați doar de buget și potențialul tehnic al celor care vor implementa și susține aceasta. Din experiență, putem spune că cu cât sistemul este mai simplu, cu atât funcționează mai stabil. Odată ce se adaugă astfel de straturi, întreținerea infrastructurii devine mult mai complicată, iar împreună cu aceasta crește și timpul necesar diagnosticării și recuperării în caz de eșec.

Recomandare

Dacă implementarea opțiunilor de stocare propuse vi se pare, de asemenea, nejustificată (complicată, costisitoare...), atunci merită să priviți situația dintr-o altă perspectiva. Adică — să săpăm în arhitectura proiectului și să înlăturăm problema din cod, legându-ne de o structură de date statică în imagine, definirea clară a conținutului sau a procedurii de „încălzire” și/sau precompilare a asset-urilor în etapa de construire a imaginii. Astfel obținem un comportament absolut predictibil și un set identic de fișiere pentru toate mediile și replicile aplicației lansate.

Dacă ne întoarcem la un exemplu specific cu framework-ul Yii și fără a adânci în structura sa (ceea ce nu este scopul acestui articol), este suficient să menționăm două abordări populare:

  1. Ajustați procesul de construire a imaginii pentru a plasa asset-urile într-o locație predictibilă. Aceasta este propusă/implementată în extensii precum yii2-static-assets.
  2. Definirea unor hash-uri specifice pentru directoarele de asset-uri, așa cum se explică, de exemplu, în această prezentare (începând cu diapozitivul nr. 35). Apropo, autorul prezentării recomandă în cele din urmă (și nu fără motive!) să încărcați asset-urile într-un depozit central (precum S3) după construirea acestora pe serverul de build, plasând un CDN în fața acestuia.

Fișierele încărcate

Un alt caz care va funcționa neapărat atunci când aplicația este transferată într-un cluster Kubernetes este stocarea fișierelor utilizatorilor în sistemul de fișiere. De exemplu, avem din nou o aplicație PHP care primește fișiere printr-un formular de încărcare, face ceva cu ele în procesul de funcționare și le returnează înapoi.

Locația în care aceste fișiere ar trebui să fie plasate, în realitatea Kubernetes, ar trebui să fie comună pentru toate replicile aplicației. În funcție de complexitatea aplicației și de necesitatea de a organiza persistența acestor fișiere, acele locuri pot fi variantele menționate mai sus de dispozitive partajate, dar, după cum vedem, acestea au propriile dezavantaje.

Recomandare

Una dintre soluții este utilizarea unui stocaj compatibil S3 (chiar și o variantă a categoriei self-hosted precum minio). Trecerea la lucrul cu S3 va necesita modificări la nivel de cod, iar modul în care va fi livrat conținutul pe frontend, deja scriau.

Sesiuni utilizator

Merită menționată organizarea stocării sesiunilor utilizatorilor. Deseori, acestea sunt, de asemenea, fișiere pe disc, ceea ce, în contextul Kubernetes, va duce la cereri constante de autorizare pentru utilizator, dacă cererea sa ajunge în alt container.

Parțial, problema se rezolvă prin activarea stickySessions pe ingress (funcționalitatea este suportată în toate controlerele de ingress populare — mai multe detalii în recenzia noastră), pentru a lega utilizatorul de un anumit pod cu aplicația:

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: /

Dar acest lucru nu va elimina problemele în cazul deploierilor repetate.

Recomandare

O modalitate mai corectă ar fi să se transfere aplicația la stocarea sesiunilor în memcached, Redis și soluții similare — în general, să se renunțe complet la variantele bazate pe fișiere.

Concluzie

Soluțiile infrastructurale discutate în text sunt demne de aplicare doar în formatul unor „cumpărarea temporară” (ceea ce sună mai bine în engleză ca workaround). Ele pot fi relevante în etapele inițiale ale migrației aplicației în Kubernetes, dar nu ar trebui să „prindă rădăcini”.

Calea recomandată este să se renunțe la ele în favoarea acestui tip de dezvoltare arhitecturală conform deja bine cunoscutului 12-Factor App. Totuși, aducerea aplicației la o formă stateless va necesita, inevitabil, modificări în cod, iar aici este important să găsim un echilibru între posibilitățile/cerințele afacerii și perspectivele de implementare și întreținere a căii alese.

P.S.

Citiți și în blogul nostru:

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster