Archivos locales al migrar una aplicación a Kubernetes

Archivos locales al migrar una aplicación a Kubernetes

Al construir un proceso CI/CD utilizando Kubernetes, a veces surge el problema de la incompatibilidad entre los requisitos de la nueva infraestructura y la aplicación que se le transfiere. En particular, en la etapa de creación de la aplicación es importante obtener uno imagen que será utilizada en todos entornos y clusters de proyectos. Este principio subyace a la correcta según google gestión de contenedores (más de una vez sobre esto говорил y nuestro departamento técnico).

Sin embargo, no verá a nadie en situaciones en las que el código del sitio utiliza un marco ya preparado, cuyo uso impone restricciones a su uso posterior. Y mientras que en un “entorno normal” esto es fácil de solucionar, en Kubernetes este comportamiento puede convertirse en un problema, especialmente cuando lo encuentras por primera vez. Si bien una mente inventiva puede generar soluciones de infraestructura que parecen obvias o incluso buenas a primera vista... es importante recordar que la mayoría de las situaciones pueden y deben resolverse arquitectónicamente.

Veamos soluciones alternativas populares para almacenar archivos que pueden tener consecuencias desagradables al operar un clúster y también señalemos una ruta más correcta.

Almacenamiento estático

A modo de ejemplo, considere una aplicación web que utiliza algún tipo de generador estático para obtener un conjunto de imágenes, estilos y otras cosas. Por ejemplo, el marco PHP Yii tiene un administrador de activos incorporado que genera nombres de directorio únicos. En consecuencia, el resultado es un conjunto de rutas para el sitio estático que obviamente no se cruzan entre sí (esto se hizo por varias razones, por ejemplo, para eliminar duplicados cuando varios componentes usan el mismo recurso). Entonces, la primera vez que accede a un módulo de recursos web, los archivos estáticos (de hecho, a menudo enlaces simbólicos, pero hablaremos de eso más adelante) se forman y se distribuyen con un directorio raíz común único para esta implementación:

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

¿Qué significa esto en términos de un clúster?

Ejemplo más simple

Tomemos un caso bastante común, cuando PHP va precedido de nginx para distribuir datos estáticos y procesar solicitudes simples. La forma más fácil - Despliegue con dos contenedores:

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

En forma simplificada, la configuración de nginx se reduce a lo siguiente:

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

Cuando accede por primera vez al sitio, los activos aparecen en el contenedor PHP. Pero en el caso de dos contenedores dentro de un pod, nginx no sabe nada acerca de estos archivos estáticos, los cuales (según la configuración) se les deben proporcionar. Como resultado, el cliente verá un error 404 para todas las solicitudes de archivos CSS y JS. La solución más sencilla aquí sería organizar un directorio común para contenedores. Opción primitiva - general 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

Ahora nginx sirve correctamente los archivos estáticos generados en el contenedor. Pero permítanme recordarles que se trata de una solución primitiva, lo que significa que está lejos de ser ideal y tiene sus propios matices y deficiencias, que se analizan a continuación.

Almacenamiento más avanzado

Ahora imagine una situación en la que un usuario visitó el sitio, cargó una página con los estilos disponibles en el contenedor y, mientras leía esta página, volvimos a implementar el contenedor. El catálogo de activos quedó vacío y se requiere una solicitud a PHP para comenzar a generar otros nuevos. Sin embargo, incluso después de esto, los enlaces a estadísticas antiguas serán irrelevantes, lo que provocará errores en la visualización de estadísticas.

Además, lo más probable es que tengamos un proyecto más o menos cargado, lo que significa que una copia de la aplicación no será suficiente:

  • Vamos a ampliarlo Despliegue hasta dos réplicas.
  • Cuando se accedió al sitio por primera vez, los activos se crearon en una réplica.
  • En algún momento, el ingreso decidió (para fines de equilibrio de carga) enviar una solicitud a la segunda réplica y estos activos aún no estaban allí. O tal vez ya no están ahí porque usamos RollingUpdate y en este momento estamos haciendo despliegue.

En general, el resultado son nuevamente errores.

Para evitar perder activos antiguos, puede cambiar emptyDir en hostPath, agregando estática físicamente a un nodo del clúster. Este enfoque es malo porque en realidad tenemos que unirse a un nodo de clúster específico su aplicación, porque, en caso de pasar a otros nodos, el directorio no contendrá los archivos necesarios. O se requiere algún tipo de sincronización de directorios en segundo plano entre nodos.

¿Cuáles son las soluciones?

  1. Si el hardware y los recursos lo permiten, puede utilizar cephfs para organizar un directorio igualmente accesible para necesidades estáticas. Documentación oficial recomienda unidades SSD, al menos una replicación triple y una conexión "gruesa" estable entre los nodos del clúster.
  2. Una opción menos exigente sería organizar un servidor NFS. Sin embargo, es necesario tener en cuenta el posible aumento en el tiempo de respuesta para procesar las solicitudes por parte del servidor web, y la tolerancia a fallos dejará mucho que desear. Las consecuencias del fracaso son catastróficas: la pérdida de la montura condena al cúmulo a morir bajo la presión de la carga de Los Ángeles que se precipita hacia el cielo.

Entre otras cosas, todas las opciones para crear almacenamiento persistente requerirán limpieza de fondo conjuntos de archivos obsoletos acumulados durante un cierto período de tiempo. Delante de los contenedores con PHP puedes poner Conjunto de demonios del almacenamiento en caché de nginx, que almacenará copias de los activos por un tiempo limitado. Este comportamiento es fácilmente configurable usando proxy_cache con profundidad de almacenamiento en días o gigabytes de espacio en disco.

La combinación de este método con los sistemas de archivos distribuidos mencionados anteriormente proporciona un enorme campo para la imaginación, limitado únicamente por el presupuesto y el potencial técnico de quienes lo implementarán y respaldarán. Por experiencia podemos decir que cuanto más simple es el sistema, más estable funciona. Cuando se agregan dichas capas, resulta mucho más difícil mantener la infraestructura y, al mismo tiempo, aumenta el tiempo dedicado a diagnosticar y recuperarse de cualquier falla.

Recomendación

Si la implementación de las opciones de almacenamiento propuestas también le parece injustificada (complicada, cara...), entonces vale la pena mirar la situación desde el otro lado. Es decir, profundizar en la arquitectura del proyecto y solucionar el problema en el código, vinculado a alguna estructura de datos estática en la imagen, una definición inequívoca del contenido o procedimiento para "calentar" y/o precompilar recursos en la etapa de ensamblaje de la imagen. De esta manera obtenemos un comportamiento absolutamente predecible y el mismo conjunto de archivos para todos los entornos y réplicas de la aplicación en ejecución.

Si volvemos al ejemplo específico con el framework Yii y no profundizamos en su estructura (que no es el propósito del artículo), basta señalar dos enfoques populares:

  1. Cambie el proceso de creación de imágenes para colocar los recursos en una ubicación predecible. Esto se sugiere/implementa en extensiones como yii2-activos-estáticos.
  2. Defina hashes específicos para directorios de activos, como se analiza, por ejemplo, en esta presentación (a partir de la diapositiva número 35). Por cierto, el autor del informe finalmente (¡y no sin razón!) aconseja que después de ensamblar los activos en el servidor de compilación, los cargue en un almacenamiento central (como S3), frente al cual coloque una CDN.

Descargas

Otro caso que definitivamente entrará en juego al migrar una aplicación a un clúster de Kubernetes es el almacenamiento de archivos de usuario en el sistema de archivos. Por ejemplo, nuevamente tenemos una aplicación PHP que acepta archivos a través de un formulario de carga, hace algo con ellos durante la operación y los devuelve.

En Kubernetes, la ubicación donde se deben colocar estos archivos debe ser común para todas las réplicas de la aplicación. Dependiendo de la complejidad de la aplicación y de la necesidad de organizar la persistencia de estos archivos, las opciones de dispositivo compartido mencionadas anteriormente pueden ser ese lugar, pero, como vemos, tienen sus inconvenientes.

Recomendación

Una solución es usando almacenamiento compatible con S3 (incluso si se trata de algún tipo de categoría autohospedada como minio). Cambiar a S3 requerirá cambios a nivel de códigoy cómo se entregará el contenido en el front-end, ya hemos escribió.

Sesiones de usuario

Por otra parte, cabe destacar la organización del almacenamiento de las sesiones de los usuarios. A menudo también se trata de archivos en el disco, lo que en el contexto de Kubernetes dará lugar a constantes solicitudes de autorización por parte del usuario si su solicitud termina en otro contenedor.

El problema se soluciona en parte encendiendo stickySessions en el ingreso (la función es compatible con todos los controladores de ingreso populares; para obtener más detalles, consulte nuestra reseña)para vincular al usuario a un pod específico con la aplicación:

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

Pero esto no eliminará los problemas con implementaciones repetidas.

Recomendación

Una forma más correcta sería transferir la aplicación a almacenar sesiones en memcached, Redis y soluciones similares - en general, abandone por completo las opciones de archivo.

Conclusión

Las soluciones de infraestructura discutidas en el texto son dignas de ser utilizadas sólo en el formato de “muletas” temporales (que suena más hermoso en inglés como solución alternativa). Pueden ser relevantes en las primeras etapas de la migración de una aplicación a Kubernetes, pero no deberían echar raíces.

La ruta general recomendada es deshacerse de ellos en favor de una modificación arquitectónica de la aplicación de acuerdo con lo que muchos ya conocen bien. Aplicación de 12 factores. Sin embargo, esto (llevar la aplicación a una forma sin estado) significa inevitablemente que se requerirán cambios en el código, y aquí es importante encontrar un equilibrio entre las capacidades/requisitos del negocio y las perspectivas de implementar y mantener el camino elegido. .

PS

Lea también en nuestro blog:

Fuente: habr.com

Añadir un comentario