File locali nella migrazione dell'applicazione su Kubernetes

File locali nella migrazione dell'applicazione su Kubernetes

Nella costruzione del processo CI/CD utilizzando Kubernetes, a volte si presenta un problema di incompatibilità tra i requisiti della nuova infrastruttura e l'applicazione che deve essere portata in essa. In particolare, durante la fase di build dell'applicazione è importante ottenere uno snapshot che verrà utilizzato nei tutti ambienti e nei cluster del progetto. Questo principio è alla base della corretta gestione dei contenitori, secondo Google e il nostro CTO l'ha ribadito in più occasioni. diceva Tuttavia, non è raro imbattersi in situazioni in cui nel codice del sito viene utilizzato un framework preconfezionato che impone restrizioni sul suo successivo utilizzo. Se in un “ambiente convenzionale” questo è facilmente gestibile, in Kubernetes tale comportamento può diventare un problema, soprattutto se ci si imbatte in esso per la prima volta. Anche se una mente ingegnosa è in grado di proporre soluzioni infrastrutturali che sembrano evidenti e persino buone a prima vista… è importante ricordare che la maggior parte delle situazioni può e deve

Tuttavia, non ci si può aspettare di vedere situazioni in cui nel codice del sito venga utilizzato un framework predefinito, la cui applicazione impone limiti sul suo ulteriore utilizzo. E se in un "ambiente normale" questo è facilmente gestibile, in Kubernetes un comportamento simile può diventare un problema, specialmente quando ci si imbatte in esso per la prima volta. Sebbene una mente ingegnosa possa offrire soluzioni infrastrutturali che sembrano evidenti e persino buone a prima vista... è importante ricordare che la maggior parte delle situazioni può e deve essere affrontata a livello architetturale..

Esamineremo le soluzioni workaround più popolari per l'archiviazione dei file, che possono portare a conseguenze indesiderate durante l'uso di un cluster, e indicheremo un percorso più corretto.

Archiviazione della statica

Per illustrare, consideriamo un'app web che utilizza un generatore di statica per ottenere un insieme di immagini, stili e altro. Ad esempio, nel framework PHP Yii è presente un gestore di asset integrato che genera nomi univoci per le directory. Di conseguenza, si ottiene un insieme di percorsi per la statica del sito che non si sovrappongono mai (questo è stato fatto per diverse ragioni — ad esempio, per escludere duplicati nel caso in cui molteplici componenti utilizzino la stessa risorsa). Così, di default, alla prima richiesta del modulo della risorsa web avviene la creazione e il posizionamento della statica (in realtà, spesso dei symlink, ma ne parleremo più avanti) con una directory radice comune unica per questo deployment:

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

Quali sono le implicazioni nel contesto di un cluster?

Un esempio semplice

Consideriamo un caso piuttosto comune in cui Nginx gestisce la distribuzione dei file statici e l'elaborazione delle richieste semplici davanti a PHP. Il modo più semplice è Deployment con due contenitori:

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

In forma semplificata, la configurazione di Nginx si riduce al seguente:

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

Quando si accede per la prima volta al sito in un contenitore con PHP, vengono generati asset. Ma nel caso di due contenitori all'interno di uno stesso pod, nginx non sa nulla di questi file statici che (secondo la configurazione) dovrebbero essere gestiti proprio da lui. Di conseguenza, per tutte le richieste ai file CSS e JS, il cliente visualizzerà un errore 404. La soluzione più semplice è organizzare una directory condivisa tra i contenitori. Una versione primitiva è comune. 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

Ora i file statici generati nel contenitore vengono serviti correttamente da nginx. Ma ricordo che questa è una soluzione primitiva e, quindi, è lontana dall'essere ideale e ha le sue sfide e lacune che discuterò di seguito.

Storage più avanzato

Immaginiamo adesso una situazione in cui l'utente accede al sito, carica la pagina con gli stili presenti nel contenitore e, mentre legge questa pagina, ridistribuiamo il contenitore. La cartella degli asset diventa vuota e necessita di una richiesta a PHP per avviare la generazione di nuovi. Tuttavia, anche dopo ciò, i collegamenti alla vecchia statica saranno obsoleti, causando errori nella visualizzazione della statica.

Inoltre, molto probabilmente abbiamo un progetto piuttosto carico, il che significa che una sola copia dell'applicazione non sarà sufficiente:

  • Scalaremo Deployment fino a due repliche.
  • Al primo accesso al sito, in una replica vengono creati gli asset.
  • A un certo punto, l'ingress ha deciso (per bilanciare il carico) di inviare la richiesta alla seconda replica, e lì quegli asset non ci sono ancora. O forse non ci sono già più, perché stiamo usando RollingUpdate e al momento stiamo effettuando il deploy.

Insomma, il risultato è di nuovo errori.

Per non perdere i vecchi asset, possiamo modificare emptyDir con hostPath, archiviando fisicamente la statica sul nodo del cluster. Questo approccio è problematico perché dobbiamo di fatto collegarci a un nodo specifico del cluster. alla tua applicazione, perché — in caso di migrazione su altri nodi — la directory non conterrà i file necessari. È necessaria una sincronizzazione in background della directory tra i nodi.

Quali sono le possibili soluzioni?

  1. Se l'hardware e le risorse lo permettono, si può utilizzare cephfs per organizzare una directory condivisa per le esigenze di statico. Documentazione ufficiale raccomanda dischi SSD, un minimo di tre repliche e una connessione ‘spessa’ stabile tra i nodi del cluster.
  2. Una variante meno esigente sarà l'organizzazione di un server NFS. Tuttavia, in questo caso, bisogna considerare un possibile aumento dei tempi di risposta nella gestione delle richieste da parte del server web, e la resilienza ai guasti lascerà a desiderare. Le conseguenze di un guasto possono essere catastrofiche: la perdita del mount condanna il cluster a un crollo sotto la pressione della carico LA, che tende a salire alle stelle.

Oltre a tutto, per tutte le opzioni di creazione di archiviazione persistente sarà necessaria una pulizia in background dei set di file obsoleti, accumulati in un certo periodo di tempo. Prima dei contenitori PHP, puoi configurare DaemonSet da cache nginx che conserveranno copie degli asset per un periodo di tempo limitato. Questo comportamento è facilmente configurabile tramite proxy_cache con una profondità di archiviazione in giorni o gigabyte di spazio su disco.

Combinare questo metodo con i sistemi di file distribuiti menzionati sopra offre enormi possibilità, con l'unico limite rappresentato dal budget e dal potenziale tecnico di coloro che lo implementeranno e supporteranno. Dalla nostra esperienza, possiamo dire che più è semplice il sistema, più stabile è il suo funzionamento. Aggiungere strati come questi rende la manutenzione dell'infrastruttura molto più complicata, e di conseguenza aumenta il tempo necessario per la diagnosi e il ripristino in caso di guasti.

Raccomandazione

Se l'implementazione delle soluzioni di storage proposte vi sembra ingiustificata (complessa, costosa...), vale la pena guardare la situazione da un'altra angolazione. Avere in mente l'architettura del progetto e eliminare il problema nel codice, associandosi a una certa struttura dati statica nell'immagine, la definizione univoca del contenuto o della procedura di «riscaldamento» e/o precompilazione delle risorse nella fase di creazione dell'immagine. In questo modo otteniamo un comportamento completamente prevedibile e un set di file identico per tutti gli ambienti e le repliche dell'applicazione in esecuzione.

Tornando a un esempio specifico con il framework Yii, e senza approfondire la sua architettura (che non è l'obiettivo dell'articolo), è sufficiente menzionare due approcci popolari:

  1. Modificare il processo di creazione dell'immagine in modo da posizionare le risorse in un luogo prevedibile. Questo è suggerito/implementato in estensioni come yii2-static-assets.
  2. Definire hash specifici per le cartelle delle risorse, come spiegato, ad esempio, in di questa presentazione. (a partire dalla diapositiva n. 35). A proposito, l'autore della presentazione consiglia infine (e non senza ragione!) di caricare le risorse in uno storage centrale (come S3) dopo la creazione delle risorse sul server di build, davanti al quale posizionare un CDN.

File caricati

Un altro caso che sicuramente funzionerà quando si trasferisce un'applicazione in un cluster Kubernetes è lo stoccaggio dei file degli utenti nel filesystem. Ad esempio, abbiamo di nuovo un'applicazione PHP che riceve file tramite un modulo di upload, elabora qualcosa e restituisce i file.

Il luogo dove questi file devono essere collocati, nella realtà di Kubernetes, deve essere condiviso tra tutte le repliche dell'applicazione. A seconda della complessità dell'applicazione e della necessità di garantire la persistenza di questi file, quel luogo potrebbe essere le soluzioni di stoccaggio condiviso menzionate in precedenza, ma, come possiamo vedere, hanno i loro svantaggi.

Raccomandazione

Una delle soluzioni è l'uso di uno storage compatibile con S3 (anche una qualche variante di self-hosted come minio). Passare a lavorare con S3 richiederà modifiche a livello di codice, e come verrà restituito il contenuto sul frontend è qualcosa che scritto.

Sessioni utente

È importante notare l'organizzazione della memorizzazione delle sessioni utente. Spesso si tratta di file su disco, il che, in un contesto Kubernetes, porterà a continue richieste di autorizzazione da parte dell'utente se la sua richiesta finisce in un altro contenitore.

In parte, il problema viene risolto attivando stickySessions sull'ingress (questa funzionalità è supportata in tutti i controller ingress più diffusi — maggiori dettagli possono essere trovati in la nostra recensione), per collegare l'utente a uno specifico pod con l'applicazione:

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

Ma questo non eliminerà i problemi durante i deploy ripetuti.

Raccomandazione

Un modo più corretto sarebbe quello di migrare l'applicazione a memorizzazione delle sessioni in memcached, Redis e soluzioni simili — in sostanza, abbandonare completamente le varianti basate su file.

Conclusione

Le soluzioni infrastrutturali considerate nel testo sono adatte esclusivamente come "transitori" (un termine più elegante in inglese è workaround). Possono essere utili nelle prime fasi della migrazione di un'applicazione in Kubernetes, ma non dovrebbero "affondare radici".

Il percorso raccomandato si riduce a eliminare queste soluzioni a favore di una ristrutturazione architettonica dell'applicazione in conformità con il già ben noto. 12-Factor App. Tuttavia, rendere l'applicazione stateless implica inevitabilmente modifiche al codice, e qui è importante trovare un equilibrio tra le esigenze/richieste aziendali e le prospettive di attuazione e manutenzione della strada scelta.

P.S.

Leggete anche nel nostro blog:

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, server VPS VDS 🔥 Acquista hosting affidabile per siti web con protezione DDoS, server VPS VDS | ProHoster