Arquivos locais ao migrar um aplicativo para Kubernetes

Arquivos locais ao migrar um aplicativo para Kubernetes

Ao construir um processo de CI/CD usando Kubernetes, às vezes surge o problema de incompatibilidade entre os requisitos da nova infraestrutura e a aplicação que está sendo transferida para ela. Em particular, na fase de construção da aplicação é importante obter um imagem que será usada em todos ambientes de projeto e clusters. Este princípio fundamenta a correta de acordo com o Google gerenciamento de contêineres (mais de uma vez sobre isso говорил e nosso departamento técnico).

Porém, você não verá ninguém em situações em que o código do site utilize uma estrutura pronta, cujo uso impõe restrições ao seu uso posterior. E embora em um “ambiente normal” isso seja fácil de lidar, no Kubernetes esse comportamento pode se tornar um problema, especialmente quando você o encontra pela primeira vez. Embora uma mente inventiva possa apresentar soluções de infraestrutura que pareçam óbvias ou mesmo boas à primeira vista... é importante lembrar que a maioria das situações pode e deve ser resolvido arquitetonicamente.

Vejamos soluções alternativas populares para armazenar arquivos que podem levar a consequências desagradáveis ​​​​ao operar um cluster e também apontar um caminho mais correto.

Armazenamento estático

Para ilustrar, considere uma aplicação web que utiliza algum tipo de gerador estático para obter um conjunto de imagens, estilos e outras coisas. Por exemplo, o framework Yii PHP possui um gerenciador de ativos integrado que gera nomes de diretórios exclusivos. Assim, a saída é um conjunto de caminhos para o site estático que obviamente não se cruzam (isso foi feito por vários motivos - por exemplo, para eliminar duplicatas quando vários componentes usam o mesmo recurso). Portanto, imediatamente, na primeira vez que você acessa um módulo de recurso da Web, os arquivos estáticos (na verdade, geralmente links simbólicos, mas falaremos mais sobre isso mais tarde) são formados e dispostos com um diretório raiz comum exclusivo para esta implantação:

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

O que isso significa em termos de cluster?

Exemplo mais simples

Vejamos um caso bastante comum, quando o PHP é precedido pelo nginx para distribuir dados estáticos e processar solicitações simples. A maneira mais fácil - desenvolvimento com dois recipientes:

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

De forma simplificada, a configuração do nginx se resume ao seguinte:

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 você acessa o site pela primeira vez, os ativos aparecem no contêiner PHP. Mas no caso de dois contêineres dentro de um pod, o nginx não sabe nada sobre esses arquivos estáticos, que (de acordo com a configuração) devem ser fornecidos a eles. Como resultado, o cliente verá um erro 404 para todas as solicitações de arquivos CSS e JS. A solução mais simples aqui seria organizar um diretório comum para contêineres. Opção primitiva - geral 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

Agora os arquivos estáticos gerados no contêiner são servidos corretamente pelo nginx. Mas deixe-me lembrar que esta é uma solução primitiva, o que significa que está longe do ideal e tem suas próprias nuances e deficiências, que serão discutidas a seguir.

Armazenamento mais avançado

Agora imagine uma situação em que um usuário visitou o site, carregou uma página com os estilos disponíveis no container e, enquanto ele lia essa página, reimplantamos o container. O catálogo de ativos ficou vazio e é necessária uma solicitação ao PHP para começar a gerar novos. No entanto, mesmo depois disso, os links para estatísticas antigas serão irrelevantes, o que levará a erros na exibição das estatísticas.

Além disso, provavelmente temos um projeto mais ou menos carregado, o que significa que uma cópia do aplicativo não será suficiente:

  • Vamos aumentar desenvolvimento até duas réplicas.
  • Quando o site foi acessado pela primeira vez, os ativos foram criados em uma réplica.
  • Em algum momento, o ingresso decidiu (para fins de balanceamento de carga) enviar uma solicitação para a segunda réplica, e esses ativos ainda não estavam lá. Ou talvez eles não estejam mais lá porque usamos RollingUpdate e no momento estamos fazendo implantação.

Em geral, o resultado são novamente erros.

Para evitar a perda de ativos antigos, você pode alterar emptyDir em hostPath, adicionando estática fisicamente a um nó de cluster. Esta abordagem é má porque na verdade temos que vincular a um nó de cluster específico sua aplicação, pois - em caso de mudança para outros nós - o diretório não conterá os arquivos necessários. Ou é necessário algum tipo de sincronização de diretório em segundo plano entre nós.

Quais são as soluções?

  1. Se o hardware e os recursos permitirem, você pode usar cephfs para organizar um diretório igualmente acessível para necessidades estáticas. Documentação oficial recomenda unidades SSD, replicação pelo menos tripla e uma conexão “espessa” estável entre os nós do cluster.
  2. Uma opção menos exigente seria organizar um servidor NFS. Porém, então é preciso levar em consideração o possível aumento no tempo de resposta para processamento de solicitações pelo servidor web, e a tolerância a falhas deixará muito a desejar. As consequências do fracasso são catastróficas: a perda da montagem condena o aglomerado à morte sob a pressão da carga de LA que corre para o céu.

Entre outras coisas, todas as opções para criar armazenamento persistente exigirão limpeza de fundo conjuntos de arquivos desatualizados acumulados durante um determinado período de tempo. Na frente dos containers com PHP você pode colocar DaemonSet do cache do nginx, que armazenará cópias de ativos por tempo limitado. Este comportamento é facilmente configurável usando proxy_cache com profundidade de armazenamento em dias ou gigabytes de espaço em disco.

A combinação deste método com os sistemas de arquivos distribuídos mencionados acima oferece um enorme campo de imaginação, limitado apenas pelo orçamento e potencial técnico de quem irá implementá-lo e apoiá-lo. Pela experiência, podemos dizer que quanto mais simples o sistema, mais estável ele funciona. Quando essas camadas são adicionadas, torna-se muito mais difícil manter a infraestrutura e, ao mesmo tempo, aumenta o tempo gasto no diagnóstico e na recuperação de quaisquer falhas.

Recomendação

Se a implementação das opções de armazenamento propostas também lhe parece injustificada (complicada, cara...), então vale a pena olhar a situação do outro lado. Ou seja, aprofundar a arquitetura do projeto e corrigir o problema no código, vinculado a alguma estrutura de dados estáticos na imagem, uma definição inequívoca do conteúdo ou procedimento para “aquecer” e/ou pré-compilar ativos na fase de montagem da imagem. Desta forma obtemos um comportamento absolutamente previsível e o mesmo conjunto de arquivos para todos os ambientes e réplicas da aplicação em execução.

Se voltarmos ao exemplo específico do framework Yii e não nos aprofundarmos em sua estrutura (o que não é o objetivo do artigo), basta apontar duas abordagens populares:

  1. Altere o processo de criação de imagem para colocar os ativos em um local previsível. Isso é sugerido/implementado em extensões como yii2-static-assets.
  2. Defina hashes específicos para diretórios de ativos, conforme discutido, por exemplo. esta apresentação (a partir do slide nº 35). A propósito, o autor do relatório em última análise (e não sem razão!) aconselha que após montar os ativos no servidor de construção, carregue-os para um armazenamento central (como o S3), na frente do qual coloque um CDN.

Transferências

Outro caso que certamente entrará em ação ao migrar um aplicativo para um cluster Kubernetes é o armazenamento de arquivos do usuário no sistema de arquivos. Por exemplo, temos novamente um aplicativo PHP que aceita arquivos por meio de um formulário de upload, faz algo com eles durante a operação e os envia de volta.

No Kubernetes, o local onde esses arquivos devem ser colocados deve ser comum a todas as réplicas da aplicação. Dependendo da complexidade da aplicação e da necessidade de organizar a persistência desses arquivos, as opções de dispositivos compartilhados acima mencionadas podem ser um desses locais, mas, como vemos, têm suas desvantagens.

Recomendação

Uma solução é usando armazenamento compatível com S3 (mesmo que seja algum tipo de categoria auto-hospedada como minio). Mudar para S3 exigirá mudanças no nível do código, e como o conteúdo será entregue no front-end, já писали.

Sessões de usuário

Separadamente, vale destacar a organização do armazenamento das sessões do usuário. Freqüentemente, esses também são arquivos em disco, o que no contexto do Kubernetes levará a constantes solicitações de autorização do usuário se sua solicitação terminar em outro contêiner.

O problema é parcialmente resolvido ligando stickySessions na entrada (o recurso é compatível com todos os controladores de entrada populares - para obter mais detalhes, consulte nossa revisão)para vincular o usuário a um pod específico com o aplicativo:

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

Mas isto não eliminará problemas com implantações repetidas.

Recomendação

Uma forma mais correta seria transferir o pedido para armazenando sessões em memcached, Redis e soluções similares - em geral, abandone completamente as opções de arquivo.

Conclusão

As soluções de infraestrutura discutidas no texto são dignas de uso apenas no formato de “muletas” temporárias (que soa mais bonito em inglês como solução alternativa). Eles podem ser relevantes nos primeiros estágios da migração de uma aplicação para o Kubernetes, mas não devem criar raízes.

O caminho geral recomendado é livrar-se deles em favor da modificação arquitetônica da aplicação de acordo com o que já é bem conhecido por muitos Aplicativo de 12 fatores. No entanto, isto - trazer a aplicação para um formato sem estado - significa inevitavelmente que serão necessárias alterações no código, e aqui é importante encontrar um equilíbrio entre as capacidades/requisitos do negócio e as perspectivas de implementação e manutenção do caminho escolhido. .

PS

Leia também em nosso blog:

Fonte: habr.com

Adicionar um comentário