Ejecuta aplicaciones web en GKE mediante PVM con optimización de costos

En este instructivo, se muestra cómo controlar las interrupciones mientras se ejecutan VM interrumpibles (PVM) en Google Kubernetes Engine (GKE) que entrega a una aplicación web. Las PVM son instancias de procesamiento de corta duración, asequibles y adecuadas para cargas de trabajo tolerantes a errores. Ofrecen los mismos tipos y opciones de máquinas que las instancias de procesamiento comunes y duran hasta 24 horas.

Este instructivo está dirigido a desarrolladores de aplicaciones, ingenieros DevOps y arquitectos de sistemas que definen e implementan aplicaciones web, y que quieren usar PVM en implementaciones de producción. En el instructivo, se da por sentado que comprendes los conceptos fundamentales de Kubernetes y varios componentes en el balanceo de cargas de HTTP(S).

Segundo plano

Una PVM se limita a un tiempo de ejecución de 24 horas y recibe una advertencia de apagado de 30 segundos cuando la instancia está a punto de interrumpirse. Primero, PVM envía un aviso de interrupción a la instancia en forma de una señal ACPI G2 Soft Off (SIGTERM). Después de 30 segundos, se envía una señal ACPI G3 Mechanical Off (SIGKILL) al sistema operativo de la instancia. Luego, la VM pasa la instancia a un estado TERMINATED.

Las PVM son una buena opción para cargas de trabajo distribuidas tolerantes a errores que no requieren disponibilidad continua de una sola instancia. Algunos ejemplos de este tipo de carga de trabajo incluyen la codificación de videos, la renderización de efectos visuales, el análisis de datos, la simulación y la genómica. Sin embargo, debido a las limitaciones de disponibilidad y las interrupciones frecuentes que se generan a partir de las interrupciones, por lo general, las PVM no se recomiendan para aplicaciones web y de usuario.

En este instructivo, se explica cómo configurar una implementación que usa una combinación de PVM y VM estándar en GKE para ayudar a entregar de manera confiable el tráfico de la aplicación web sin interrupciones.

Desafíos del uso de las PVM

El mayor desafío de usar PVM en la entrega de tráfico orientado al usuario es garantizar que las solicitudes de los usuarios no se interrumpan. En la interrupción, debes abordar lo siguiente:

  • ¿Cómo aseguras la disponibilidad de una aplicación cuando se ejecuta en PVM? Las PVM no tienen disponibilidad garantizada y se excluyen de forma explícita de los Acuerdos de Nivel de Servicio de Compute Engine.
  • ¿Cómo controlas la finalización ordenada de la aplicación para que las siguientes afirmaciones sean verdaderas?:
    • El balanceador de cargas detiene las solicitudes de reenvío a los Pods que se ejecutan en una instancia que se interrumpe.
    • Las solicitudes en tránsito se controlan sin problemas, se completan o se cierran.
    • Las conexiones a las bases de datos y a la aplicación se cierran o se agotan antes de que se cierre la instancia.
  • ¿Cómo controlas las solicitudes, como las transacciones fundamentales para el negocio, que pueden requerir garantías de tiempo de actividad o que no son tolerantes a errores?

Considera los desafíos de cerrar de forma correcta los contenedores que se ejecutan en PVM. Desde el punto de vista de la implementación, la forma más fácil de escribir la lógica de limpieza cuando una instancia se cierra es a través de una secuencia de comandos de apagado. Sin embargo, las secuencias de comandos de apagado no son compatibles si ejecutas cargas de trabajo en contenedores en GKE.

Como alternativa, puedes usar un controlador SIGTERM en tu aplicación para escribir la lógica de limpieza. Los contenedores también proporcionan hooks de ciclo de vida, como preStop, que se activa justo antes de que se apague el contenedor. En Kubernetes, Kubelet es responsable de ejecutar los eventos de ciclo de vida de los contenedores. Dentro de un clúster de Kubernetes, Kubelet se ejecuta en la VM y observa las especificaciones de los Pods a través del servidor de la API de Kubernetes.

Cuando expulsas un Pod mediante una línea de comandos o una API, Kubelet observa que el Pod se marcó como finalizado y comienza el proceso de apagado. La duración del proceso está limitada por el “período de gracia”, que se define como una cantidad determinada de segundos después de que Kubelet envía una señal SIGKILL a los contenedores. Como parte del cierre correcto, si algún contenedor que se ejecuta en el Pod definió un hook preStop, Kubelet ejecuta el hook dentro del contenedor. A continuación, Kubelet activa una señal SIGTERM en process-ID 1 dentro de cada contenedor. Si la aplicación usa un controlador SIGTERM, este se ejecuta. Cuando el controlador se completa, Kubelet envía una señal SIGKILL a todos los procesos que aún se ejecutan en el Pod.

Imagina que un nodo de Kubernetes se encuentra en interrupción. Necesitas un mecanismo para detectar el aviso de interrupción y comenzar el proceso de expulsión del Pod. Supongamos que ejecutas un programa que escucha un aviso de interrupción y expulsa a los Pods en ejecución cuando recibe un evento. Cuando se produce la expulsión, se activa la secuencia de cierre del Pod descrita antes. Sin embargo, en este caso, el nodo también está experimentando un cierre, que lo controla el sistema operativo (SO) del nodo. Este cierre puede interferir en el control de ciclo de vida del contenedor de Kubelet, lo que significa que el contenedor se puede cerrar de forma repentina, incluso si está en medio de la ejecución de un hook preStop.

Además, desde el punto de vista de la disponibilidad y la administración del tráfico, que la ejecución de tu aplicación web de realice de forma exclusiva en las PVM también puede representar varios desafíos. Antes de usar las PVM, ten en cuenta las siguientes preguntas:

  • ¿Qué sucede si la mayoría de las PVM se interrumpen a la vez? En el caso de las aplicaciones que entregan miles de solicitudes por segundo, ¿cómo realizan la conmutación por error sin interrupciones?
  • ¿Qué sucede si la capacidad de PVM no está disponible? ¿Cómo se escala verticalmente o se mantiene un estado estable de implementación para tu aplicación en ese caso?

Arquitectura

Para resolver los desafíos del uso de las PVM, debes hacer todo lo siguiente:

  1. Ejecuta un clúster de GKE con dos grupos de nodos: uno que ejecute PVM y otro que ejecute VM estándar. Esto te permite dividir el tráfico y tener una conmutación por error activa para controlar solicitudes nuevas y en tránsito en caso de una interrupción. Este enfoque también te permite dividir el tráfico entre las VM estándar y las PVM según tus requisitos de garantía de tiempo de actividad y tolerancia a errores. Para obtener más información sobre cómo decidir el tamaño de los grupos de nodos, consulta Consideraciones.
  2. Detecta avisos de interrupción en las PVM y los Pods expulsados que se ejecutan en el nodo.
  3. Usa un hook preStop o un controlador SIGTERM para ejecutar la lógica de limpieza.
  4. Asegúrate de que Kubelet pueda controlar el ciclo de vida de finalización del Pod y que no se cierre de forma repentina.
  5. Aplica un taint al nodo para que no se programen Pods nuevos en él mientras se interrumpe.

En el siguiente diagrama, se muestra una vista de alto nivel de la arquitectura que implementas en este instructivo.

Arquitectura de alto nivel

Como se muestra en el diagrama, se crean dos grupos de nodos: default-pool se ejecuta en VM estándar y pvm-pool se ejecuta en PVM. El programador de GKE predeterminado intenta distribuir de manera uniforme los Pods en las instancias en los grupos de nodos. Por ejemplo, si implementas cuatro réplicas y tienes dos nodos en ejecución en cada grupo de nodos, el programador aprovisiona un Pod en cada uno en los cuatro nodos. Sin embargo, cuando usas PVM, recomendamos dirigir el tráfico hacia pvm-pool para obtener un uso mayor de PVM y, por lo tanto, ahorro de costos. Por ejemplo, se recomienda aprovisionar tres Pods en pvm-pool y un Pod en default-pool para la conmutación por error, lo que reduce la cantidad de VM estándar en el clúster.

Para lograr este tipo de control sobre la programación, puedes escribir tu propio programador o dividir la aplicación en dos implementaciones. Cada implementación se fijará en un grupo de nodos según las reglas de afinidad de nodo. En este ejemplo, se crean dos implementaciones, web-std y web-pvm, en las que web-std se fija en default-pool, y web-pvm se fija en pvm-pool.

Para los nodos pvm-pool, debes detectar un aviso de interrupción y, luego de recibirlo, debes iniciar la expulsión de Pods. Puedes escribir tu propia lógica para detectar el aviso de interrupción y expulsar los Pods que se ejecutan en el nodo. Como alternativa, como en este instructivo, puedes crear un agente mediante el controlador de eventos k8s-node-termination-handler.

k8s-node-termination-handler usa un daemonset de Kubernetes para crear un agente en cada instancia en pvm-pool. El agente detecta un evento de finalización de nodo mediante las API de metadatos de Compute Engine. Cada vez que se detecta un evento de finalización, el agente inicia el proceso de expulsión del Pod. En este ejemplo, se asignan 20 segundos como un período de gracia para Pods normales y 10 segundos para Pods del sistema. Esto significa que si hay un hook preStop o un controlador SIGTERM configurado para el Pod, puede ejecutarse hasta 20 segundos antes de que SIGKILL cierre el Pod. El período de gracia es un parámetro configurable en k8s-node-termination-handler. El período de gracia total de los Pods regulares y del sistema no puede exceder el del aviso de interrupción (30 segundos).

El agente también habilita un taint en el nodo para evitar que se programen Pods nuevos.

Cuando expulsas los Pods, se activa el hook preStop. En este ejemplo, preStop está configurado para lo siguiente:

  • Produce fallas en la verificación de estado de la aplicación. Esto se hace para indicar al balanceador de cargas que debe quitar el Pod de la ruta de entrega de la solicitud.
  • Se suspende durante el período de gracia (20 segundos) asignado por k8s-node-termination-handler. El Pod permanece activo durante 20 segundos para procesar cualquier solicitud en tránsito.

Mientras se ejecuta el hook preStop, para asegurarte de que el SO del nodo no cierre Kubelet de manera repentina, debes crear un servicio systemd que bloquee el cierre. Este enfoque ayuda a garantizar que Kubelet pueda administrar el ciclo de vida de los Pods durante el cierre sin la interferencia del SO del nodo. Debes usar un daemonset de Kubernetes para crear el servicio. Este daemonset se ejecutará en cada instancia en pvm-pool.

En este ejemplo, se usa Traffic Director para administrar el tráfico. Ten en cuenta que puedes usar cualquier solución de proxy, como OSS Envoy, Istio, Nginx o HAProxy, a fin de administrar el tráfico siempre que cumplas con los lineamientos del subsistema que se usan en este ejemplo, en el que se configura Traffic Director para que haga lo siguiente:

  • Habilita el enrutamiento ponderado de las solicitudes. En este ejemplo, se crean tres réplicas de aplicación en pvm-pool y una en default-pool. Traffic Director está configurado para dividir el tráfico en 75% y 25% entre pvm-pool y default-pool. En el caso de la interrupción, todas las solicitudes se conmutan por error de forma automática en default-pool.

    En este instructivo, se proporciona un ejemplo simple de la división del tráfico. También puedes establecer condiciones de coincidencia en puertos de tráfico, campos de encabezado, URI, etc. para enrutar la solicitud a un grupo de nodos específico. Para obtener más información, consulta las técnicas avanzadas de enrutamiento de tráfico con Traffic Director.

  • En caso de interrupción, si la solicitud da como resultado un código de estado 5xx (por ejemplo, debido a que una puerta de enlace no está disponible o se agota el tiempo de espera de una conexión ascendente), Traffic Director reintenta las solicitudes hasta tres veces.

  • Usa un interruptor de circuitos para limitar la cantidad máxima de reintentos que pueden quedar pendientes en un momento determinado.

  • Usa la detección de valores atípicos para expulsar los extremos en mal estado de la ruta de entrega del balanceador de cargas.

Traffic Director usa el modelo de proxy de sidecar. En este modelo, ocurren los siguientes eventos:

  1. Los clientes envían solicitudes a un balanceador de cargas administrado por Google Cloud.
  2. El balanceador de cargas envía el tráfico a un proxy perimetral configurado por Traffic Director.
  3. El proxy perimetral aplica políticas predefinidas (como la distribución de solicitudes entre extremos diferentes) y las políticas de interrupción de circuitos y reintentos, y balancea las cargas de las solicitudes a los servicios en el clúster de GKE.
  4. El proxy de sidecar intercepta el tráfico y lo reenvía a la aplicación.

Para obtener más información, consulta el funcionamiento de la intercepción y el reenvío de tráfico con Traffic Director.

Objetivos

  • Implementar un clúster de GKE con dos grupos de nodos mediante VM estándar y PVM
  • Implementar una aplicación de muestra
  • Configurar los recursos del clúster para controlar de forma ordenada la interrupción
  • Configurar Traffic Director para controlar el tráfico en los servicios de GKE
  • Simular la interrupción de una PVM
  • Verificar que los Pods se apaguen de forma correcta
  • Verificar que las solicitudes nuevas no se reenvíen a los Pods que están en interrupción
  • Verificar que las solicitudes en tránsito y las nuevas se entreguen sin interrupciones

Costos

En este instructivo, se usan los siguientes componentes facturables de Google Cloud:

Para generar una estimación de costos en función del uso previsto, usa la calculadora de precios. Es posible que los usuarios nuevos de Google Cloud sean aptos para obtener una prueba gratuita.

Cuando finalices este instructivo, podrás borrar los recursos creados para evitar que se te siga facturando. Para obtener más información, consulta cómo hacer una limpieza.

Antes de comenzar

  1. En Google Cloud Console, ve a la página del selector de proyectos.

    Ir al selector de proyectos

  2. Selecciona o crea un proyecto de Google Cloud.

  3. Comprueba que la facturación esté habilitada en tu proyecto.

    Descubre cómo puedes habilitar la facturación

  4. En Cloud Console, activa Cloud Shell.

    Activar Cloud Shell

    En la parte inferior de Cloud Console, se inicia una sesión de Cloud Shell en la que se muestra una ventana de línea de comandos. Cloud Shell es un entorno de shell que tiene el SDK de Cloud preinstalado, incluida la herramienta de línea de comandos de gcloud, y valores ya establecidos para el proyecto actual. La inicialización de la sesión puede tomar unos minutos.

  5. Habilita la API Compute Engine, GKE, Container Analysis, Cloud Build, Container Registry, and Traffic Director.

    Habilita la API

  6. Busca el ID del proyecto y establécelo en Cloud Shell. Reemplaza YOUR_PROJECT_ID por el ID del proyecto.
    gcloud config set project YOUR_PROJECT_ID
    
  7. Exporta las siguientes variables de entorno:
    export PROJECT=$(gcloud config get-value project)
    export CLUSTER=$PROJECT-gke
    export REGION="us-central1"
    export ZONE="us-central1-c"
    export TERMINATION_HANDLER="https://github.com/GoogleCloudPlatform/k8s-node-termination-handler"
    

Crea un clúster de GKE

  1. En Cloud Shell, crea un clúster de GKE que tenga un grupo de nodos predeterminado, default-pool, con instancias estándar, y un grupo de nodos personalizado, pvm-pool, con PVM:

    gcloud beta container clusters create $CLUSTER \
       --zone=$ZONE \
       --num-nodes="1" \
       --enable-ip-alias \
       --machine-type="n1-standard-4" \
       --scopes=https://www.googleapis.com/auth/cloud-platform
    gcloud beta container node-pools create "pvm-pool" \
       --cluster=$CLUSTER \
       --zone=$ZONE \
       --preemptible \
       --machine-type="n1-standard-4" \
       --scopes=https://www.googleapis.com/auth/cloud-platform \
       --num-nodes="1"
    
  2. Clona el repositorio de código solutions-gke-pvm-preemption-handler que usarás para este instructivo:

    git clone https://github.com/GoogleCloudPlatform/solutions-gke-pvm-preemption-handler && \
    cd solutions-gke-pvm-preemption-handler
    
  3. Crea un daemonset que se ejecute en instancias de PVM en el clúster de GKE y crea un servicio systemd que bloquee el cierre del proceso de Kubelet:

    kubectl apply -f daemonset.yaml
    
  4. Obtén el nombre de la instancia de PVM implementada como parte del grupo de nodos pvm-pool:

    PVM=$(kubectl get no \
        -o=jsonpath='{range .items[*]} \
        {.metadata.name}{"\n"}{end}' | grep pvm)
    
  5. Verifica que el servicio esté implementado de forma correcta:

    1. Crea una regla de firewall a fin de usar SSH para conectarte a la PVM a través del redireccionamiento de IAP:

      gcloud compute firewall-rules create allow-ssh-ingress-from-iap \
          --direction=INGRESS \
          --action=allow \
          --rules=tcp:22 \
          --source-ranges=35.235.240.0/20
      
    2. Usa SSH para conectarte a la PVM:

      gcloud compute ssh $PVM --tunnel-through-iap --zone=$ZONE
      
    3. En la terminal de PVM, verifica el estado del servicio implementado:

      systemctl status delay.service
      

      Verás el estado del servicio como Active (exited).

      ...
      delay.service - Delay GKE shutdown
         Loaded: loaded (/etc/systemd/system/delay.service; enabled; vendor preset: disabled)
         Active: active (exited) since Tue 2020-07-21 04:48:33 UTC; 1h 17min ago
      ...
      
    4. Para salir de la terminal de PVM, escribe exit.

  6. Implementa k8s-node-termination-handler:

    1. Clone el repositorio:

      git clone $TERMINATION_HANDLER
      
    2. En un editor de texto, abre el archivo k8s-node-termination-handler/deploy/k8s.yaml y busca la siguiente línea:

      args: ["--logtostderr", "--exclude-pods=$(POD_NAME):$(POD_NAMESPACE)", "-v=10", "--taint=cloud.google.com/impending-node-termination::NoSchedule"]
      
    3. Reemplaza la línea anterior por la siguiente línea, que asigna un período de gracia de 10 segundos para cerrar los Pods del sistema. Los 20 segundos restantes en el período de gracia se asignan de forma automática a los Pods normales.

      args: ["--logtostderr", "--exclude-pods=$(POD_NAME):$(POD_NAMESPACE)", "-v=10", "--taint=cloud.google.com/impending-node-termination::NoSchedule", "--system-pod-grace-period=10s"]
      
    4. Implementa el controlador:

      kubectl apply \
          -f k8s-node-termination-handler/deploy/k8s.yaml \
          -f k8s-node-termination-handler/deploy/rbac.yaml
      
  7. Verifica que el controlador de finalización del nodo se haya implementado de forma correcta:

    kubectl get ds node-termination-handler -n kube-system
    

    El resultado es similar al siguiente:

    NAME                       DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
    node-termination-handler   1         1         1       1            1           <none>          101s
    

Implementa la aplicación

  1. En Cloud Shell, configura las reglas de firewall para las verificaciones de estado:

    gcloud compute firewall-rules create fw-allow-health-checks \
        --action=ALLOW \
        --direction=INGRESS \
        --source-ranges=35.191.0.0/16,130.211.0.0/22 \
        --rules tcp
    
  2. Implementa la aplicación:

    kubectl apply -f deploy.yaml
    
  3. Verifica que haya dos implementaciones diferentes que se ejecuten en default-pool y pvm-pool:

    1. En default-pool, ejecuta el siguiente comando:

      kubectl get po -l app=web-std \
          -o=custom-columns=NAME:.metadata.name,Node:.spec.nodeName
      

      El resultado es similar al siguiente:

      NAME                       Node
      web-std-695b5fb6c4-55gcc   gke-vital-octagon-109612-default-pool-dcdb8fe5-2tc7
      
    2. En pvm-pool, ejecuta el siguiente comando:

      kubectl get po -l app=web-pvm \
          -o=custom-columns=NAME:.metadata.name,Node:.spec.nodeName
      

      El resultado es similar al siguiente.

      NAME                       Node
      web-pvm-6f867bfc54-nm6fb   gke-vital-octagon-109612-gke-pvm-pool-664ec4ff-2cgc
      

      Para cada servicio, se crea un NEG independiente que contiene extremos que son los puertos y las direcciones IP del Pod. Para obtener más información y ejemplos, consulta Grupos de extremos de red independientes.

  4. Confirma que se creó el NEG independiente:

    gcloud beta compute network-endpoint-groups list
    

    El resultado es similar al siguiente.

    NAME                                       LOCATION       ENDPOINT_TYPE   SIZE
    k8s1-be35f81e-default-web-pvm-80-7c99357f  us-central1-c  GCE_VM_IP_PORT  1
    k8s1-be35f81e-default-web-std-80-f16dfcec  us-central1-c  GCE_VM_IP_PORT  1
    

    Para administrar la aplicación mediante Traffic Director, debes realizar las implementaciones como grupos de extremos de red (NEG). Como se analizó en la sección Arquitectura, debes usar un proxy de sidecar para crear las implementaciones.

  5. Por último, verifica que las implementaciones se creen con un proxy de sidecar:

    kubectl get pods -l app=web-std \
        -o jsonpath={.items[*].spec.containers[*].name}
    

    El resultado es similar al siguiente.

    hello-app istio-proxy
    

    Si quieres ver resultados similares para Pods que se ejecutan en pvm-pool, puedes ejecutar lo siguiente:

    kubectl get pods -l app=web-pvm \
        -o jsonpath={.items[*].spec.containers[*].name}
    

Crea el servicio de Traffic Director

En Traffic Director, se usa una configuración similar a la de otros productos de Cloud Load Balancing. En otras palabras, debes configurar los siguientes componentes para Traffic Director:

  1. En Cloud Shell, busca los NEG que creaste antes y almacena los nombres en una variable:

    1. Para el servicio default-pool, usa lo siguiente:

      NEG_NAME_STD=$(gcloud beta compute network-endpoint-groups list \
                     | grep web-std | awk '{print $1}')
      
    2. Para el servicio pvm-pool, usa lo siguiente:

      NEG_NAME_PVM=$(gcloud beta compute network-endpoint-groups list \
                     | grep web-pvm | awk '{print $1}')
      
  2. Crea la verificación de estado:

      gcloud compute health-checks create http td-gke-health-check \
          --request-path=/health \
          --use-serving-port \
          --healthy-threshold=1 \
          --unhealthy-threshold=2 \
          --check-interval=2s \
          --timeout=2s
    
  3. Reemplaza los marcadores de posición en los archivos de manifiesto:

    sed -i -e "s/\[PROJECT_ID\]/$PROJECT/g" td-gke-service-config.yaml
    sed -i -e "s/\[ZONE\]/$ZONE/g" td-gke-service-config.yaml
    sed -i -e "s/\[NEG_NAME_STD\]/$NEG_NAME_STD/g" td-gke-service-config.yaml
    sed -i -e "s/\[NEG_NAME_PVM\]/$NEG_NAME_PVM/g" td-gke-service-config.yaml
    sed -i -e "s/\[PROJECT_ID\]/$PROJECT/g" td-urlmap.yaml
    
  4. Crea un servicio de Traffic Director:

    gcloud compute backend-services import td-gke-service \
        --source=td-gke-service-config.yaml --global
    

    El servicio está configurado para dividir el tráfico mediante el escalador de capacidad entre los dos NEG que creaste antes. El servicio también se configura con la detección de valores atípicos:

    • Para la detección de valores atípicos, establece los Errores consecutivos (o fallas de la puerta de enlace antes de que un host se expulse del servicio) en 2.
    • Para la interrupción de circuitos, establece el Máximo de reintentos en 3.
  5. Verifica que el servicio de Traffic Director se haya implementado de forma correcta:

    gcloud compute backend-services get-health td-gke-service --global
    

    El resultado es similar al siguiente:

    ‐‐‐
    backend: default-pool-service-NEG
    status:
      healthStatus:
      ‐ healthState: HEALTHY
    ...
    ‐‐‐
    backend: pvm-pool-service-NEG
    status:
      healthStatus:
      ‐ healthState: HEALTHY
    ...
    

    Es posible que debas esperar unos minutos y ejecutar el comando varias veces, antes de que el backend se muestre como HEALTHY.

  6. Crea un mapa de URL que use el servicio que creaste:

    gcloud compute url-maps import web-service-urlmap \
        --source=td-urlmap.yaml
    

    El mapa de URL configura el enrutamiento de tráfico. Todas las solicitudes a la ruta “/*” se redireccionan al servicio de Traffic Director que creaste. Además, el mapa también configura una política para reintentar solicitudes (3 veces como máximo) que da como resultado un código de estado 5xx.

  7. Crea el proxy HTTP de destino:

    gcloud compute target-http-proxies create td-gke-proxy \
        --url-map=web-service-urlmap
    
  8. Crea la regla de reenvío que usa la dirección IP virtual (VIP) 0.0.0.0:

    gcloud compute forwarding-rules create td-gke-forwarding-rule \
        --global \
        --load-balancing-scheme=INTERNAL_SELF_MANAGED \
        --address=0.0.0.0 \
        --target-http-proxy=td-gke-proxy \
        --ports=80
    

    En este punto, se puede acceder a los servicios de GKE en default-pool y pvm-pool a través de la VIP del servicio en la que Traffic Director balancea las cargas.

Crea el balanceador de cargas

En esta sección, debes configurar un balanceador de cargas y un proxy perimetral para el tráfico de usuarios. El balanceador de cargas actúa como una puerta de enlace a la configuración que acabas de crear.

  1. En Cloud Shell, crea un proxy perimetral administrado por Traffic Director:

    kubectl apply -f edge-proxy.yaml
    
  2. Verifica que el balanceador de cargas esté en buen estado y listo para entregar tráfico:

    kubectl describe ingress gateway-proxy-ingress
    

    El resultado es similar al siguiente:

    ...
      Host        Path  Backends
      ‐‐‐‐        ‐‐‐‐  ‐‐‐‐‐‐‐‐
      *           *     gateway-proxy-svc:80 (10.20.0.14:80)
    
    Annotations:  ingress.kubernetes.io/backends: {"k8s1-da0dd12b-default-gateway-proxy-svc-80-b3b7b808":"HEALTHY"}
    ...
    

    El backend debe tener el estado HEALTHY. Pueden transcurrir varios minutos, y varios reintentos del comando, hasta que el balanceador de cargas esté listo para aceptar tráfico.

  3. Registra la dirección IP para usarla más adelante:

    IPAddress=$(kubectl get ingress gateway-proxy-ingress \
                -o jsonpath="{.status.loadBalancer.ingress[*].ip}")
    

Genera tráfico

Ahora que se completó la configuración, es hora de probarla.

  1. En Cloud Shell, haz clic en Abrir una pestaña nueva + para iniciar una sesión nueva de Cloud Shell.

  2. En la shell nueva, establece el ID del proyecto:

    gcloud config set project YOUR_PROJECT_ID
    
  3. Instala Kubetail para observar varios registros de Pods de aplicación a la vez:

    sudo apt-get update
    sudo apt-get install kubetail
    kubetail web
    
  4. En la shell original, simula el tráfico:

    seq 1 100 | xargs -I{} -n 1 -P 10 curl -I http://$IPAddress
    

    Mediante este comando, se generan 100 solicitudes; 10 solicitudes paralelas a la vez.

    En la shell nueva, verás registros similares a los siguientes:

    ---
    [web-pvm-6f867bfc54-nm6fb hello-app] Received request at: 2020-07-20 20:26:23.393
    [web-pvm-6f867bfc54-nm6fb hello-app] Received request at: 2020-07-20 20:26:23.399
    [web-std-6f867bfc54-55gcc hello-app] Received request at: 2020-07-20 20:26:24.001
    ...
    

    Verifica que las solicitudes se distribuyan entre los grupos de nodos según la división del 75% y el 25% que configuraste antes.

  5. En la shell original, genera tráfico mediante httperf:

    sudo apt-get install httperf && \
    httperf --server=$IPAddress --port=80 --uri=/ \
            --num-conns=5000 --rate=20 --num-calls=1 \
            --print-reply=header
    

    Mediante este comando, se instala httperf y se generan 5,000 solicitudes en la aplicación de ejemplo con una frecuencia de 20 solicitudes por segundo. Esta prueba se ejecuta durante ~250 segundos aproximadamente.

Simula la interrupción

  1. En la shell nueva, presiona Ctrl+C para salir del comando Kubetail.
  2. Obtén el nombre de la instancia de PVM implementada como parte de pvm-pool:

    PVM=$(kubectl get no \
          -o=jsonpath='{range .items[*]} \
          {.metadata.name}{"\n"}{end}' | grep pvm)
    
  3. Mientras la prueba httperf está en curso, activa un evento de mantenimiento en la instancia de PVM para simular la interrupción:

    gcloud compute instances simulate-maintenance-event $PVM \
        --zone=us-central1-c
    
  4. Observa los registros de la aplicación en la nueva shell:

    kubectl logs -f deploy/web-pvm -c hello-app
    

    Cuando el Pod recibe una señal SIGTERM, el estado de la verificación de estado de la aplicación informa de forma explícita fail:

    ...
    Health status fail at: 2020-07-21 04:45:43.742
    ...
    

    El Pod continúa recibiendo solicitudes hasta que se quita de la ruta de entrega de la solicitud debido a verificaciones de estado con errores. Esta eliminación puede llevar unos segundos en propagarse.

    ...
    Received request at: 2020-07-21 04:45:45.735
    Received request at: 2020-07-21 04:45:45.743
    Health status fail at: 2020-07-21 04:45:45.766
    ...
    

    Después de unos segundos, el Pod deja de recibir solicitudes nuevas. El Pod continúa activo durante 20 segundos. El controlador preStop se suspende durante 20 segundos para permitir actividades de limpieza, incluidas las solicitudes en tránsito. Luego, el Pod se cierra.

    ...
    Health status fail at: 2020-07-21 04:46:01.796
    2020-07-21 04:46:02.303  INFO 1 --- [       Thread-3] ConfigServletWebServerApplicationContext : Closing org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@27ddd392: startup date [Tue Jul 21 04:39:44 UTC 2020]; root of context hierarchy
    Exiting PreStop hook
    ...
    
  5. De manera opcional, ejecuta el comando kubetail web para que se muestre la parte final de los registros de todos los Pods de la aplicación que se ejecutan en ambos grupos de nodos. El resultado muestra las solicitudes que se enrutan a default-pool a medida que la PVM se encuentra en interrupción:

    ...
    [web-pvm-6f867bfc54-nm6fb] Received request at: 2020-07-20 20:45:45.743
    [web-pvm-6f867bfc54-nm6fb] Health status fail at: 2020-07-21 04:45:45.766
    [web-std-6f867bfc54-55gcc] Received request at: 2020-07-20 04:45:45.780
    [web-std-6f867bfc54-55gcc] Received request at: 2020-07-20 04:45:45.782
    ...
    

Validaciones posteriores a la interrupción

  1. En la shell original, espera a que se complete la prueba Httperf. Cuando se complete, el resultado será similar al siguiente.

    ...
    Reply status: 1xx=0 2xx=5000 3xx=0 4xx=0 5xx=0
    ...
    Errors: total 0 client-timo 0 socket-timo 0 connrefused 0 connreset 0
    ...
    

    El resultado indica que después de la interrupción, default-pool entregó las solicitudes y pvm-pool se cerró de forma correcta.

  2. Verifica que la PVM se vuelva a activar y que el estado original del clúster se restablezca:

    kubectl get po -l app=web-pvm \
        -o=custom-columns=NAME:.metadata.name,Node:.spec.nodeName
    

    Observa que dos réplicas se aprovisionan en pvm-pool:

    NAME                     Node
    web-pvm-6f867bfc54-9z2cp gke-vital-octagon-109612-gke-pvm-pool-664ec4ff-49lx
    

Consideraciones

Antes de ejecutar esta solución en producción, considera estas calificaciones:

  1. En este instructivo, no se emplean políticas de ajuste de escala automático de Pods o clústeres. Para los entornos de producción, asegúrate de tener el ajuste de escala automático correcto a fin de controlar los aumentos de tráfico.
  2. Considera con atención la división entre las VM estándar y las PVM en el clúster. Por ejemplo, supongamos que ejecutas 100 PVM y solo 1 VM estándar, y el 50% de las PVM se someten a una interrupción a la vez. En este caso, el escalamiento horizontal del grupo de VM estándar llevará algo de tiempo para compensar los recursos interrumpidos. Mientras tanto, el tráfico de usuarios se ve afectado. Si deseas mitigar las interrupciones grandes, puedes usar aplicaciones de terceros, como Spot y estafette-gke-preemptible-killer, para distribuir las interrupciones a fin de evitar que varias instancias se interrumpan a la vez.
  3. En función de tu caso de uso, prueba con cuidado el período de gracia que se asigna a los Pods regulares y del sistema mediante k8s-node-termination-handler. Debido al estado crítico de la aplicación, es posible que debas asignar más de 20 segundos a los Pods regulares. Una posible desventaja de este enfoque es que puede no dejar suficiente tiempo para que los Pods del sistema se cierren de forma correcta. Esto puede generar una posible pérdida de registros y métricas de supervisión que controlan los Pods del sistema.

Limpia

A fin de evitar que se apliquen cargos a la cuenta de Google Cloud por los recursos que se usaron en este instructivo, puedes borrar el proyecto de Cloud que creaste o borrar los recursos asociados con este instructivo.

Borra el proyecto de Cloud

La manera más fácil de eliminar la facturación es borrar el proyecto que creaste para el instructivo.

  1. En Cloud Console, ve a la página Administrar recursos.

    Ir a Administrar recursos

  2. En la lista de proyectos, elige el proyecto que quieres borrar y haz clic en Borrar.
  3. En el diálogo, escribe el ID del proyecto y, luego, haz clic en Cerrar para borrar el proyecto.

Borra los recursos

Si deseas conservar el proyecto que usaste en este instructivo, borra los recursos individuales.

  1. En Cloud Shell, borra el clúster de GKE:

    gcloud container clusters delete $CLUSTER --zone=$ZONE --async
    
  2. Borra las reglas de firewall:

    gcloud compute firewall-rules delete allow-ssh-ingress-from-iap && \
    gcloud compute firewall-rules delete fw-allow-health-checks
    
  3. Borra el servicio de Traffic Director:

    gcloud compute forwarding-rules delete td-gke-forwarding-rule \
        --global && \
    gcloud compute target-http-proxies delete td-gke-proxy \
        --global && \
    gcloud compute url-maps delete web-service-urlmap \
        --global && \
    gcloud compute backend-services delete td-gke-service \
        --global && \
    gcloud compute health-checks delete td-gke-health-check \
        --global
    
  4. Borra todos los NEG:

    NEG_NAME_EDGE=$(gcloud beta compute network-endpoint-groups list \
                    | grep gateway-proxy | awk '{print $1}') && \
    gcloud beta compute network-endpoint-groups delete $NEG_NAME_EDGE \
        --zone=$ZONE && \
    gcloud beta compute network-endpoint-groups delete $NEG_NAME_STD \
        --zone=$ZONE && \
    gcloud beta compute network-endpoint-groups delete $NEG_NAME_PVM \
        --zone=$ZONE
    
  5. Borra el código descargado, los artefactos y otras dependencias:

    cd .. && rm -rf solutions-gke-pvm-preemption-handler
    

¿Qué sigue?