Archivos locales al portar una aplicación a Kubernetes



Cuando se crea un proceso de CI / CD con Kubernetes, a veces hay un problema de incompatibilidad de los requisitos de la nueva infraestructura y la aplicación que se le transfiere. En particular, en la etapa de ensamblaje de la aplicación, es importante obtener una imagen que se utilizará en todos los entornos y clústeres del proyecto. Este principio subyace a la correcta gestión de contenedores en la opinión de Google (nuestro techdir ha hablado repetidamente de esto).

Sin embargo, no sorprenderá a nadie con situaciones en las que se utiliza un marco listo para usar en el código del sitio, cuyo uso impone restricciones en su funcionamiento posterior. Y si es fácil de manejar en un "entorno normal", en Kubernetes, este tipo de comportamiento puede ser un problema, especialmente cuando lo encuentras por primera vez. Aunque una mente ingeniosa puede ofrecer soluciones de infraestructura que parecen obvias e incluso bastante buenas a primera vista ... es importante recordar que la mayoría de las situaciones pueden y deben resolverse arquitectónicamente .

Analicemos las soluciones alternativas populares para almacenar archivos, que pueden tener consecuencias desagradables durante el funcionamiento del clúster, y también señalar una ruta más correcta.

Almacenamiento estático


Para ilustrar, considere una aplicación web que utiliza un generador estático para obtener un conjunto de imágenes, estilos y más. Por ejemplo, el framework Yii PHP tiene un administrador de activos incorporado que genera nombres de directorio únicos. En consecuencia, la salida es un conjunto de rutas obviamente no intersectadas para las estadísticas del sitio (esto se hizo por varias razones, por ejemplo, para excluir duplicados cuando se usa el mismo recurso con muchos componentes). Entonces, fuera de la caja, cuando accede por primera vez al módulo de recursos web, se forman y presentan estadísticas (de hecho, a menudo enlaces simbólicos, pero más sobre eso más adelante) con un directorio raíz común que es único para esta implementación:

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

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

Ejemplo más simple


Tomemos un caso bastante común cuando PHP se enfrenta a nginx para distribuir estadísticas y manejar consultas simples. La forma más fácil es la implementación 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 una 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 en un contenedor con PHP, aparecen los activos. Pero en el caso de dos contenedores dentro del mismo pod, nginx no sabe nada acerca de estos archivos estáticos, que (de acuerdo con la configuración) se les debe dar. Como resultado, el cliente verá el error 404 para todas las solicitudes de archivos CSS y JS. La solución más simple aquí es organizar un directorio común para contenedores. Una opción primitiva es el genérico 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 los archivos estáticos generados en el contenedor son dados por nginx correctamente. Pero permítame recordarle que esta es 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ó un sitio, cargó una página con los estilos disponibles en el contenedor y, mientras estaba leyendo esta página, volvimos a implementar el contenedor. El directorio de activos se ha quedado vacío y requiere una solicitud a PHP para comenzar a generar nuevos. Sin embargo, incluso después de esto, los enlaces a las estadísticas antiguas estarán desactualizadas, lo que provocará errores al mostrar las 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:

  • Escale la implementación a dos réplicas.
  • Cuando accede por primera vez al sitio en una réplica, se crearon activos.
  • En algún momento, la entrada decidió (para equilibrar la carga) enviar una solicitud de una segunda réplica, y estos activos aún no están allí. O tal vez ya no están allí, porque usamos RollingUpdate y actualmente estamos haciendo un despliegue.

En general, el resultado son errores nuevamente.

Para no perder los activos antiguos, puede cambiar emptyDir a hostPath , agregando físicamente las estadísticas al nodo del clúster. Este enfoque es malo porque debemos vincularnos a un nodo de clúster específico con nuestra aplicación, porque, en caso de pasar a otros nodos, el directorio no contendrá los archivos necesarios. O bien, se requiere una sincronización en segundo plano del directorio entre nodos.

¿Cuales son las soluciones?

  1. Si el hardware y los recursos lo permiten, puede usar cephfs para organizar un directorio igualmente accesible para las necesidades de las estadísticas. La documentación oficial recomienda SSD, al menos una triple replicación y una conexión robusta y "gruesa" entre nodos del clúster.
  2. Una opción menos exigente sería organizar un servidor NFS. Sin embargo, debe considerar el posible aumento en el tiempo de respuesta a las solicitudes de procesamiento por parte del servidor web, y la tolerancia a fallas dejará mucho que desear. Las consecuencias de la falla son catastróficas: la pérdida de montura destruye el grupo hasta la muerte bajo el ataque de la carga de Los Ángeles que se precipita hacia el cielo.

Entre otras cosas, para todas las opciones para crear almacenamiento persistente, se requerirá la limpieza en segundo plano de los conjuntos de archivos obsoletos acumulados durante un cierto período de tiempo. Antes de los contenedores con PHP, puede poner DaemonSet en el almacenamiento en caché de nginx, que almacenará copias de activos por un tiempo limitado. Este comportamiento se puede configurar fácilmente 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 campo enorme para la imaginación, una limitación solo en el presupuesto y el potencial técnico de quienes lo implementarán y respaldarán. Por experiencia, decimos que cuanto más simple es el sistema, más estable funciona. Con la adición de tales capas, se hace mucho más difícil mantener la infraestructura y, al mismo tiempo, aumenta el tiempo dedicado al diagnóstico y la recuperación en caso de fallas.

Recomendación


Si la implementación de las opciones de almacenamiento propuestas también le parece injustificada (complicada, costosa ...), entonces debe mirar la situación desde el otro lado. Es decir, profundizar en la arquitectura del proyecto y erradicar el problema en el código mediante el enlace a alguna estructura de datos estática en la imagen, proporciona una definición inequívoca de los contenidos o el procedimiento de "calentamiento" y / o precompilación de activos en la etapa de ensamblaje de la imagen. Por lo tanto, 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 a un ejemplo específico con el marco Yii y no profundizamos en su estructura (que no es el propósito del artículo), es suficiente señalar dos enfoques populares:

  1. Modifique el proceso de ensamblar la imagen para que los activos se coloquen en un lugar predecible. Ofrezca / implemente en extensiones como yii2-static-assets .
  2. Defina hashes específicos para directorios de activos, como se describe, por ejemplo, en esta presentación (comenzando con la diapositiva 35). Por cierto, el autor del informe en última instancia (¡y no sin razón!) Aconseja después de ensamblar los activos en el servidor de compilación para cargarlos en un repositorio central (como S3), frente al cual colocar el CDN.

Archivos descargables


Otro caso que seguramente se disparará al transferir una aplicación a un clúster de Kubernetes es almacenar archivos de usuario en el sistema de archivos. Por ejemplo, nuevamente tenemos una aplicación PHP que acepta archivos a través del formulario de carga, hace algo con ellos en el proceso y lo devuelve.

El lugar donde deberían ubicarse estos archivos en las realidades de Kubernetes debería ser común a todas las réplicas de aplicaciones. Dependiendo de la complejidad de la aplicación y la necesidad de organizar la persistencia de estos archivos, tal lugar puede ser las opciones para dispositivos compartidos mencionados anteriormente, pero, como vemos, tienen sus inconvenientes.

Recomendación


Una solución es usar un almacenamiento compatible con S3 (incluso si algún tipo de categoría autohospedada como minio). La transición para trabajar con S3 requerirá cambios a nivel de código , y ya escribimos cómo se devolverá el contenido en la interfaz.

Sesiones personalizadas


Por separado, vale la pena señalar la organización del almacenamiento de las sesiones de los usuarios. A menudo, estos también son archivos en el disco que, en el contexto de Kubernetes, generarán solicitudes de autorización constantes del usuario si su solicitud cae en otro contenedor.

Parte del problema se resuelve mediante la inclusión de stickySessions en el ingreso (la función es compatible con todos los controladores de ingreso populares; consulte nuestra revisión para más detalles) 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 lo salvará de implementaciones repetidas.

Recomendación


Una forma más correcta sería transferir la aplicación para almacenar sesiones en Memcached, Redis y soluciones similares , en general, abandonar completamente las opciones de archivo.

Conclusión


Las soluciones de infraestructura consideradas en el texto son dignas de aplicación solo en el formato de "muletas" temporales (que suena más hermoso en inglés como una solución alternativa). Pueden ser relevantes en las primeras etapas de la migración de aplicaciones a Kubernetes, pero no deben "enraizarse".

La forma general recomendada es deshacerse de ellos en favor del refinamiento arquitectónico de la aplicación de acuerdo con la ya conocida 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 es importante encontrar un equilibrio entre las capacidades / requisitos del negocio y las perspectivas para implementar y mantener la ruta elegida.

PS


Lea también en nuestro blog:

Source: https://habr.com/ru/post/471582/


All Articles