
Ao criar um processo de CI / CD usando o Kubernetes, às vezes há um problema de incompatibilidade dos requisitos da nova infraestrutura e do aplicativo transferido para ela. Em particular, no estágio de montagem do aplicativo, é importante obter
uma imagem que será usada em
todos os ambientes e clusters do projeto. Esse princípio é subjacente ao gerenciamento correto de contêineres na
opinião do Google (nosso técnico já
falou várias vezes sobre isso).
No entanto, você não surpreenderá ninguém com situações em que uma estrutura pronta é usada no código do site, cuja utilização impõe restrições à sua operação posterior. E se é fácil lidar com um "ambiente normal" no Kubernetes, esse comportamento pode ser um problema, especialmente quando você o encontra pela primeira vez. Embora uma mente engenhosa seja capaz de oferecer soluções de infraestrutura que parecem óbvias e até muito boas à primeira vista ... é importante lembrar que a maioria das situações pode e deve
ser resolvida arquitetonicamente .
Vamos analisar as soluções alternativas populares para armazenar arquivos, que podem levar a conseqüências desagradáveis durante a operação do cluster e também apontar para um caminho mais correto.
Armazenamento estático
Para ilustrar, considere um aplicativo Web que usa um gerador estático para obter um conjunto de imagens, estilos e muito mais. Por exemplo, a estrutura PHP do Yii possui um gerenciador de ativos interno que gera nomes de diretório exclusivos. Consequentemente, a saída é um conjunto de caminhos obviamente sem interseção para a estática do site (isso foi feito por vários motivos - por exemplo, para excluir duplicatas ao usar o mesmo recurso com muitos componentes). Portanto, quando você acessa o módulo de recursos da Web pela primeira vez, as estatísticas são formadas e dispostas (na verdade, geralmente links simbólicos, mas mais sobre isso posteriormente) 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 preocupante em termos de um cluster?
Exemplo mais simples
Vamos considerar um caso bastante comum quando o PHP enfrenta o nginx para distribuir estática e manipular consultas simples. A maneira mais fácil é a
implantação com dois contêineres:
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 uma 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 em um contêiner com PHP, os ativos são exibidos. Porém, no caso de dois contêineres no mesmo 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á o erro 404 para todas as solicitações de arquivos CSS e JS. A solução mais simples aqui é organizar um diretório comum para contêineres. Uma opção primitiva é o
emptyDir
genérico:
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 fornecidos pelo nginx corretamente. Mas deixe-me lembrá-lo de que esta é uma solução primitiva, o que significa que está longe do ideal e tem suas próprias nuances e deficiências, discutidas abaixo.
Armazenamento mais avançado
Agora imagine uma situação em que um usuário visitou um site, carregou uma página com os estilos disponíveis no contêiner e, enquanto ele estava lendo esta página, reimplantamos o contêiner. O diretório de ativos ficou vazio e requer uma solicitação ao PHP para começar a gerar novos. No entanto, mesmo depois disso, os links para estatísticas antigas ficarão desatualizados, o que levará a erros na exibição de estatísticas.
Além disso, é provável que tenhamos um projeto mais ou menos carregado, o que significa que uma cópia do aplicativo não será suficiente:
- Dimensione a implantação para duas réplicas.
- Quando você acessa o site pela primeira vez em uma réplica, os ativos foram criados.
- Em algum momento, a entrada decidiu (para equilibrar a carga) enviar uma solicitação para uma segunda réplica, e esses ativos ainda não estão lá. Ou talvez eles não estejam mais lá, porque usamos o
RollingUpdate
e atualmente estamos implementando.
Em geral, o resultado são erros novamente.
Para não perder os ativos antigos, você pode alterar
emptyDir
para
hostPath
, adicionando a estatística fisicamente ao nó do cluster. Essa abordagem é ruim porque devemos realmente
vincular a um nó de cluster específico com o nosso aplicativo, porque - no caso de mudar para outros nós - o diretório não conterá os arquivos necessários. Ou, é necessária alguma sincronização em segundo plano do diretório entre os nós.
Quais são as soluções?
- Se o hardware e os recursos permitirem, você pode usar o cephfs para organizar um diretório igualmente acessível para as necessidades da estática. A documentação oficial recomenda SSDs, pelo menos replicação tripla e uma conexão "grossa" robusta entre os nós do cluster.
- Uma opção menos exigente seria organizar um servidor NFS. No entanto, é necessário considerar o possível aumento no tempo de resposta ao processamento de solicitações pelo servidor da Web, e a tolerância a falhas deixará muito a desejar. As conseqüências do fracasso são catastróficas: a perda da montaria destrói o aglomerado até a morte sob o ataque da carga de Los Angeles que corre para o céu.
Entre outras coisas, para todas as opções de criação de armazenamento persistente, será necessária a
limpeza em segundo plano de conjuntos de arquivos desatualizados acumulados durante um certo período de tempo. Antes dos contêineres com PHP, você pode colocar o
DaemonSet no cache do nginx, que armazenará cópias dos ativos por um tempo limitado. Esse comportamento pode ser facilmente configurado usando
proxy_cache
com profundidade de armazenamento em dias ou gigabytes de espaço em disco.
A combinação desse método com os sistemas de arquivos distribuídos mencionados acima fornece um enorme campo para a imaginação, uma limitação apenas no orçamento e no potencial técnico daqueles que o implementarão e darão suporte. Por experiência, dizemos que quanto mais simples o sistema, mais estável ele funciona. Com a adição de tais camadas, fica muito mais difícil manter a infraestrutura e, ao mesmo tempo, o tempo gasto em diagnósticos e recuperação em caso de aumento de falhas.
Recomendação
Se a implementação das opções de armazenamento propostas também lhe parecer injustificada (complicada, cara ...), você deverá considerar a situação do outro lado. Ou seja, cavar na arquitetura do projeto e
erradicar o problema no código , vinculando a alguma estrutura de dados estática na imagem, fornece uma definição inequívoca do conteúdo ou o procedimento de “aquecimento” e / ou pré-compilação de ativos no estágio de montagem da imagem. Portanto, obtemos um comportamento absolutamente previsível e o mesmo conjunto de arquivos para todos os ambientes e réplicas do aplicativo em execução.
Se retornarmos a um exemplo específico com a estrutura Yii e não nos aprofundarmos em sua estrutura (que não é o objetivo do artigo), basta apontar duas abordagens populares:
- Modifique o processo de montagem da imagem para que os ativos sejam colocados em um local previsível. Portanto, ofereça / implemente em extensões como yii2-static-assets .
- Defina hashes específicos para diretórios de ativos, conforme descrito, por exemplo, nesta apresentação (começando no slide 35). A propósito, o autor do relatório, em última análise (e não sem motivo!), Aconselha, após a montagem dos ativos no servidor de compilação, fazer o upload deles em um repositório central (como o S3), na frente do qual coloca a CDN.
Arquivos para Download
Outro caso que certamente será acionado ao transferir um aplicativo para um cluster Kubernetes é o armazenamento de arquivos do usuário no sistema de arquivos. Por exemplo, novamente temos um aplicativo PHP que aceita arquivos por meio do formulário de upload, faz algo com eles no processo e devolve.
O local em que esses arquivos devem ser colocados nas realidades do Kubernetes deve ser comum a todas as réplicas de aplicativos. Dependendo da complexidade do aplicativo e da necessidade de organizar a persistência desses arquivos, esse local pode ser as opções para dispositivos compartilhados mencionados acima, mas, como vemos, eles têm suas desvantagens.
Recomendação
Uma solução é
usar um armazenamento compatível com S3 (mesmo que algum tipo de categoria auto-hospedada, como minio). A transição para o trabalho com o S3 exigirá alterações
no nível do código , e já
escrevemos como o conteúdo será retornado no frontend.
Sessões personalizadas
Separadamente, vale a pena notar a organização do armazenamento de sessões do usuário. Freqüentemente, esses também são arquivos em disco, que, no contexto do Kubernetes, levam a solicitações de autorização constantes do usuário, se a solicitação cair em outro contêiner.
Parte do problema é resolvida com a inclusão de
stickySessions
na entrada
(o recurso é suportado em todos os controladores de entrada populares - consulte nossa análise para obter detalhes) 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 isso não o salvará de implantações repetidas.
Recomendação
Uma maneira mais correta seria transferir o aplicativo para
armazenar 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 consideradas no texto são válidas para aplicação somente no formato de "muletas" temporárias (que parece mais bonita em inglês como solução alternativa). Eles podem ser relevantes nos estágios iniciais da migração de aplicativos para o Kubernetes, mas não devem ser "enraizados".
A maneira geral recomendada é se livrar deles em favor do refinamento arquitetônico do aplicativo, de acordo com o já conhecido
aplicativo de 12 fatores . No entanto, isso - levar o aplicativo a um estado sem estado - inevitavelmente significa que serão necessárias alterações no código, e é importante encontrar um equilíbrio entre os recursos / requisitos da empresa e as perspectivas de implementar e manter o caminho escolhido.
PS
Leia também em nosso blog: