¡Saludos a todos! Hoy nos gustaría compartir nuestra experiencia con Google Kubernetes Engine para administrar nuestros clústeres de Kubernetes. Lo hemos estado usando durante los últimos tres años en producción y nos complace que ya no tengamos que preocuparnos por administrar estos clústeres nosotros mismos.
Actualmente, tenemos todos nuestros entornos de prueba y clústeres de infraestructura únicos bajo el control de Kubernetes. Hoy, queremos hablar sobre cómo encontramos un problema en nuestro grupo de prueba y cómo esperamos que este artículo ahorre tiempo y esfuerzo a otros.
Debemos proporcionar información sobre nuestra infraestructura de prueba para comprender completamente nuestro problema. Tenemos más de cinco entornos de prueba permanentes y estamos implementando entornos para desarrolladores a pedido. El número de módulos entre semana alcanza los 6000 durante el día y sigue creciendo. Dado que la carga es inestable, empaquetamos los módulos muy apretados para ahorrar costos, y la reventa de recursos es nuestra mejor estrategia.
Esta configuración funcionó bien para nosotros hasta que un día recibimos una alerta y no pudimos eliminar un espacio de nombres. El mensaje de error que recibimos con respecto a la eliminación del espacio de nombres fue:
$ kubectl delete namespace arslanbekov Error from server (Conflict): Operation cannot be fulfilled on namespaces "arslanbekov": The system is ensuring all content is removed from this namespace. Upon completion, this namespace will automatically be purged by the system.
Incluso el uso de la opción de eliminación forzada no resolvió el problema:
$ kubectl get namespace arslanbekov -o yaml apiVersion: v1 kind: Namespace metadata: ... spec: finalizers: - kubernetes status: phase: Terminating
Para resolver el problema del espacio de nombres atascado, seguimos una guía . Aún así, esta solución temporal no era la ideal, ya que nuestros desarrolladores deberían haber podido crear y eliminar sus entornos a voluntad, utilizando la abstracción del espacio de nombres.
Decididos a encontrar una solución mejor, decidimos investigar más a fondo. La alerta indicó un problema de métricas, que confirmamos ejecutando un comando:
$ kubectl api-resources --verbs=list --namespaced -o name error: unable to retrieve the complete list of server APIs: metrics.k8s.io/v1beta1: the server is currently unable to handle the request
Descubrimos que el pod del servidor de métricas estaba experimentando un error de falta de memoria (OOM) y un error grave en los registros:
apiserver panic'd on GET /apis/metrics.k8s.io/v1beta1/nodes: killing connection/stream because serving request timed out and response had been started goroutine 1430 [running]:
La razón estaba en los límites de los recursos del pod:
El contenedor estaba encontrando estos problemas debido a su definición, que era la siguiente (bloque de límites):
resources: limits: cpu: 51m memory: 123Mi requests: cpu: 51m memory: 123Mi
El problema era que al contenedor se le asignaron solo 51 millones de CPU , lo que equivale aproximadamente a 0,05 de una CPU de un núcleo , y esto no fue suficiente para manejar las métricas de una cantidad tan grande de pods. Principalmente se utiliza el programador CFS.
Por lo general, solucionar estos problemas es sencillo e implica simplemente asignar más recursos al pod. Sin embargo, en GKE, esta opción no está disponible en la interfaz de usuario ni a través de la CLI de gcloud. Esto se debe a que Google protege los recursos del sistema para que no se modifiquen, lo cual es comprensible teniendo en cuenta que toda la administración se realiza por su parte.
Descubrimos que no éramos los únicos que enfrentamos este problema y encontramos un problema similar en el que el autor intentó cambiar la definición del pod manualmente. Él tuvo éxito, pero nosotros no. Cuando intentamos cambiar los límites de recursos en el archivo YAML, GKE los revirtió rápidamente.
Necesitábamos encontrar otra solución.
Nuestro primer paso fue comprender por qué los límites de recursos se establecieron en estos valores. El pod constaba de dos contenedores: el metrics-server
y el addon-resizer
. Este último era responsable de ajustar los recursos a medida que se agregaban o eliminaban nodos del clúster, actuando como un cuidador de la escala automática vertical del clúster.
Su definición de línea de comando era la siguiente:
command: - /pod_nanny - --config-dir=/etc/config - --cpu=40m - --extra-cpu=0.5m - --memory=35Mi - --extra-memory=4Mi ...
En esta definición, la CPU y la memoria representan los recursos básicos, mientras que extra-cpu
y extra-memory
representan recursos adicionales por nodo. Los cálculos para 180 nodos serían los siguientes:
0.5m * 180 + 40m=~130m
La misma lógica se aplica a los recursos de memoria.
Desafortunadamente, la única forma de aumentar los recursos era agregando más nodos, lo que no queríamos hacer. Entonces, decidimos explorar otras opciones.
A pesar de no poder resolver el problema por completo, queríamos estabilizar la implementación lo más rápido posible . Aprendimos que algunas propiedades en la definición de YAML se podían cambiar sin que GKE las revirtiera. Para abordar esto, aumentamos la cantidad de réplicas de 1 a 5 , agregamos una verificación de estado y ajustamos la estrategia de implementación de acuerdo con este artículo .
Estas acciones ayudaron a reducir la carga en la instancia del servidor de métricas y garantizaron que siempre tuviéramos al menos un módulo de trabajo que pudiera proporcionar métricas. Nos tomamos un tiempo para reconsiderar el problema y refrescar nuestros pensamientos. La solución terminó siendo simple y obvia en retrospectiva.
Profundizamos en los aspectos internos de addon-resizer y descubrimos que se podía configurar a través de un archivo de configuración y parámetros de línea de comandos. A primera vista, parecía que los parámetros de la línea de comandos deberían anular los valores de configuración, pero no fue así.
Al investigar, encontramos que el archivo de configuración estaba conectado al pod a través de los parámetros de la línea de comando del contenedor addon-resizer:
--config-dir=/etc/config
El archivo de configuración se asignó como un ConfigMap con el nombre metrics-server-config
en el espacio de nombres del sistema, ¡y GKE no revierte esta configuración!
Agregamos recursos a través de esta configuración de la siguiente manera:
apiVersion: v1 data: NannyConfiguration: |- apiVersion: nannyconfig/v1alpha1 kind: NannyConfiguration baseCPU: 100m cpuPerNode: 5m baseMemory: 100Mi memoryPerNode: 5Mi kind: ConfigMap metadata:
¡Y funcionó! Esto fue una victoria para nosotros.
Dejamos dos pods con comprobaciones de estado y una estrategia de tiempo de inactividad cero mientras se redimensionaba el clúster, y no recibimos más alertas después de realizar estos cambios.