File locali durante la migrazione di un'applicazione a Kubernetes

File locali durante la migrazione di un'applicazione a Kubernetes

Quando si costruisce un processo CI/CD utilizzando Kubernetes, a volte sorge il problema dell'incompatibilità tra i requisiti della nuova infrastruttura e l'applicazione che vi viene trasferita. In particolare, nella fase di creazione dell'applicazione è importante ottenere uno immagine che verrà utilizzata in tutti ambienti e cluster di progetto. Questo principio è alla base del corretto secondo Google gestione dei contenitori (più di una volta su questo lui parlò e il nostro ufficio tecnico).

Tuttavia, non vedrai nessuno in situazioni in cui il codice del sito utilizza un framework già pronto, il cui utilizzo impone restrizioni sul suo ulteriore utilizzo. E mentre in un “ambiente normale” questo è facile da gestire, in Kubernetes questo comportamento può diventare un problema, soprattutto quando lo si incontra per la prima volta. Sebbene una mente creativa possa ideare soluzioni infrastrutturali che a prima vista sembrano ovvie e persino valide... è importante ricordare che nella maggior parte delle situazioni si può e si deve essere risolti architettonicamente.

Diamo un'occhiata alle soluzioni alternative più diffuse per l'archiviazione di file che possono portare a conseguenze spiacevoli durante il funzionamento di un cluster e indichiamo anche un percorso più corretto.

Archiviazione statica

Per illustrare, si consideri un'applicazione Web che utilizza una sorta di generatore statico per ottenere una serie di immagini, stili e altre cose. Ad esempio, il framework Yii PHP ha un asset manager integrato che genera nomi di directory univoci. Di conseguenza, l'output è un insieme di percorsi ovviamente non intersecanti per il sito statico (questo è stato fatto per diversi motivi, ad esempio per eliminare i duplicati quando più componenti utilizzano la stessa risorsa). Quindi, immediatamente, la prima volta che si accede a un modulo di risorse Web, i file statici (in effetti, spesso collegamenti simbolici, ma ne parleremo più avanti) vengono formati e disposti con una directory root comune unica per questa distribuzione:

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

Cosa significa questo in termini di cluster?

Esempio più semplice

Prendiamo un caso abbastanza comune, in cui PHP è preceduto da nginx per distribuire dati statici ed elaborare semplici richieste. La via più facile - Distribuzione 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 una forma semplificata, la configurazione di nginx si riduce a quanto segue:

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 accedi per la prima volta al sito, le risorse vengono visualizzate nel contenitore PHP. Ma nel caso di due contenitori all'interno di un pod, nginx non sa nulla di questi file statici, che (a seconda della configurazione) dovrebbero essere loro assegnati. Di conseguenza, il client vedrà un errore 404 per tutte le richieste di file CSS e JS. La soluzione più semplice in questo caso sarebbe organizzare una directory comune per i contenitori. Opzione primitiva - generale 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 lascia che ti ricordi che questa è una soluzione primitiva, il che significa che è tutt'altro che ideale e presenta le sue sfumature e carenze, che saranno discusse di seguito.

Archiviazione più avanzata

Immaginiamo ora una situazione in cui un utente ha visitato il sito, ha caricato una pagina con gli stili disponibili nel contenitore e, mentre leggeva questa pagina, abbiamo ridistribuito il contenitore. Il catalogo delle risorse è diventato vuoto ed è necessaria una richiesta a PHP per iniziare a generarne di nuove. Tuttavia, anche dopo questo, i collegamenti alle vecchie statistiche saranno irrilevanti, il che porterà a errori nella visualizzazione delle statistiche.

Inoltre, molto probabilmente abbiamo un progetto più o meno carico, il che significa che una copia dell'applicazione non sarà sufficiente:

  • Ingrandiamolo Distribuzione fino a due repliche.
  • Al primo accesso al sito, le risorse venivano create in un'unica replica.
  • Ad un certo punto, Ingress ha deciso (per scopi di bilanciamento del carico) di inviare una richiesta alla seconda replica e queste risorse non erano ancora arrivate. O forse non ci sono più perché li usiamo RollingUpdate e al momento stiamo effettuando la distribuzione.

In generale, il risultato sono ancora errori.

Per evitare di perdere vecchi asset, puoi cambiare emptyDir su hostPath, aggiungendo fisicamente statico a un nodo del cluster. Questo approccio è negativo perché in realtà dobbiamo farlo associarsi a uno specifico nodo del cluster la tua applicazione, perché - in caso di spostamento su altri nodi - la directory non conterrà i file necessari. Oppure è necessaria una sorta di sincronizzazione delle directory in background tra i nodi.

Quali sono le soluzioni?

  1. Se l'hardware e le risorse lo consentono, è possibile utilizzare cephfs organizzare una directory ugualmente accessibile per esigenze statiche. Documentazione ufficiale raccomanda unità SSD, replica almeno tripla e una connessione stabile e “spessa” tra i nodi del cluster.
  2. Un'opzione meno impegnativa sarebbe organizzare un server NFS. Tuttavia è necessario tenere conto del possibile aumento del tempo di risposta per l'elaborazione delle richieste da parte del server web e la tolleranza agli errori lascerà molto a desiderare. Le conseguenze del fallimento sono catastrofiche: la perdita della montatura condanna a morte l'ammasso sotto la pressione del carico di Los Angeles che si precipita nel cielo.

Tra le altre cose, saranno necessarie tutte le opzioni per la creazione di archiviazione persistente pulizia dello sfondo insiemi di file obsoleti accumulati in un certo periodo di tempo. Di fronte ai contenitori con PHP puoi mettere Demon Set dalla memorizzazione nella cache di nginx, che memorizzerà copie delle risorse per un periodo limitato. Questo comportamento è facilmente configurabile utilizzando proxy_cache con profondità di archiviazione in giorni o gigabyte di spazio su disco.

La combinazione di questo metodo con i file system distribuiti sopra menzionati offre un vasto campo di immaginazione, limitato solo dal budget e dal potenziale tecnico di coloro che lo implementeranno e lo supporteranno. Per esperienza possiamo dire che più il sistema è semplice, più è stabile. Quando si aggiungono tali livelli, diventa molto più difficile mantenere l’infrastruttura e, allo stesso tempo, aumenta il tempo dedicato alla diagnosi e al ripristino da eventuali guasti.

raccomandazione

Se anche l’implementazione delle opzioni di storage proposte vi sembra ingiustificata (complicata, costosa...), allora vale la pena guardare la situazione da un altro lato. Vale a dire, scavare nell'architettura del progetto e risolvere il problema nel codice, legato ad una struttura dati statica nell'immagine, una definizione univoca dei contenuti o una procedura per il “riscaldamento” e/o la precompilazione delle risorse in fase di assemblaggio dell'immagine. In questo modo otteniamo un comportamento assolutamente prevedibile e lo stesso set di file per tutti gli ambienti e le repliche dell'applicazione in esecuzione.

Se torniamo all'esempio specifico del framework Yii e non approfondiamo la sua struttura (che non è lo scopo dell'articolo), è sufficiente evidenziare due approcci popolari:

  1. Modifica il processo di creazione dell'immagine per posizionare le risorse in una posizione prevedibile. Questo è suggerito/implementato in estensioni come yii2-static-assets.
  2. Definire hash specifici per le directory delle risorse, come discusso ad es. questa presentazione (a partire dalla diapositiva n. 35). A proposito, l'autore del rapporto alla fine (e non senza motivo!) consiglia di caricare gli asset sul server di compilazione dopo averli assemblati su un archivio centrale (come S3), davanti al quale posizionare una CDN.

Download

Un altro caso che entrerà sicuramente in gioco durante la migrazione di un'applicazione in un cluster Kubernetes è la memorizzazione dei file utente nel file system. Ad esempio, abbiamo ancora una volta un'applicazione PHP che accetta file tramite un modulo di caricamento, fa qualcosa con essi durante il funzionamento e li rimanda indietro.

In Kubernetes, la posizione in cui posizionare questi file dovrebbe essere comune a tutte le repliche dell'applicazione. A seconda della complessità dell'applicazione e della necessità di organizzare la persistenza di questi file, le opzioni di dispositivo condiviso sopra menzionate potrebbero essere una soluzione adeguata, ma, come vediamo, hanno i loro inconvenienti.

raccomandazione

Una soluzione è utilizzando uno spazio di archiviazione compatibile con S3 (anche se è una sorta di categoria self-hosted come Minio). Il passaggio a S3 richiederà modifiche a livello di codicee come verranno distribuiti i contenuti sul front-end, lo abbiamo già fatto ha scritto.

Sessioni utente

Separatamente, vale la pena notare l'organizzazione dell'archiviazione delle sessioni utente. Spesso si tratta anche di file su disco, il che nel contesto di Kubernetes porterà a continue richieste di autorizzazione da parte dell'utente se la sua richiesta finisce in un altro container.

Il problema si risolve in parte accendendo stickySessions all'ingresso (la funzionalità è supportata in tutti i controller di ingresso più diffusi; per ulteriori dettagli, vedere la nostra recensione)per associare l'utente a un pod specifico 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 ciò non eliminerà i problemi legati alle implementazioni ripetute.

raccomandazione

Un modo più corretto sarebbe trasferire l'applicazione in memorizzazione di sessioni in memcached, Redis e soluzioni simili - in generale, abbandona completamente le opzioni sui file.

conclusione

Le soluzioni infrastrutturali discusse nel testo sono degne di essere utilizzate solo sotto forma di “stampelle” temporanee (che suona più bello in inglese come soluzione alternativa). Potrebbero essere rilevanti nelle prime fasi della migrazione di un'applicazione a Kubernetes, ma non dovrebbero mettere radici.

Il percorso generale consigliato è quello di eliminarli in favore di una modifica architetturale dell'applicazione secondo quanto già noto a molti Applicazione a 12 fattori. Tuttavia, questo - portare la domanda in una forma stateless - significa inevitabilmente che saranno necessarie modifiche al codice, e qui è importante trovare un equilibrio tra le capacità/requisiti dell'azienda e le prospettive di implementazione e mantenimento del percorso scelto .

PS

Leggi anche sul nostro blog:

Fonte: habr.com

Aggiungi un commento