Durante esta serie de posts hemos hablado de la ingeniería del caos, por qué es una disciplina que debe tenerse en cuenta y por qué cada vez es más importante en organizaciones de todos los tamaños, cómo abordar la planificación y la estrategia y cuáles son sus herramientas y frameworks más extendidos.

En este post vamos a entrar en el aspecto más técnico, viendo el funcionamiento de uno de los frameworks más extendidos: Chaos toolkit.

Vamos a implementar los experimentos definidos en el post anterior, actuando tanto a nivel de infraestructura sobre nuestro cluster de kubernetes como a nivel de aplicación, según los componentes señalados en él.

Manos a la obra

Vamos a comenzar con la instalación de Chaos Toolkit. Podemos realizar la instalación de dos formas diferentes. Primero, debemos elegir cuál es el que más se adapta a nuestro caso de uso:

¿Cuál debemos elegir? Depende de nuestro caso de uso. Cada vez tenemos más tendencia a desplegar la mayor parte de aplicaciones sobre kubernetes. Si es tu caso, mediante el operador simplificaremos su ejecución. Es importante tener en cuenta en este caso que la imagen que utiliza el operador no lleva extensiones para diferentes proveedores, solamente trae el framework de chaostoolkit como tal, por lo que añadir extensiones adicionales nos implicará mantener nuestro propio Dockerfile para incorporar a la imagen aquellas que necesitemos.

Por otro lado, si queremos mantener de forma aislada el entorno de ejecución de chaos respecto a la misma plataforma donde viven nuestras aplicaciones, nos encajaría mejor la primera opción. En este caso, la adición de nuevas extensiones a nuestro framework será muy sencilla, como veremos a continuación.

En nuestro caso, vamos a proceder a la instalación fuera de nuestra plataforma de kubernetes.

Instalación de Chaos Toolkit

Lo primero es instalarnos Chaos Toolkit (versión 1.7.1 en el momento de crear este post):

pip3 install chaostoolkit

Collecting chaostoolkit
Using cached chaostoolkit-1.7.1-py3-none-any.whl (18 kB)
...
Installing collected packages: PyYAML, click, zipp, importlib-metadata, python-json-logger, logzero, chaostoolkit-lib, click-plugins, chaostoolkit
Successfully installed PyYAML-5.4.1 chaostoolkit-1.7.1 chaostoolkit-lib-1.16.0 click-7.1.2 click-plugins-1.1.1 importlib-metadata-3.4.0 logzero-1.6.3 python-json-logger-2.0.1 zipp-3.4.0

Con esta operación, tendríamos nuestro framework de Chaos Toolkit funcionando. Por defecto, solamente tenemos disponible el framework de Chaos, pero para poder empezar a ejecutar nuestros experimentos, necesitamos instalar el driver disponible para atacar a la plataforma que en nuestro caso vamos a utilizar. A día de hoy, estos son todos los drivers que incluye Chaos Toolkit:

Como podemos observar están incluidos los 3 principales Cloud Provider del mercado y kubernetes. También tenemos drivers para ejecutar experimentos a nivel de framework de desarrollo, como Spring, así como software de Networking como Istio e incluso plataformas de observabilidad.

En nuestro caso, procedemos con kubernetes (versión 0.25.0 en el momento de crear el post):

pip3 install chaostoolkit-kubernetes

Collecting chaostoolkit-kubernetes
Using cached chaostoolkit_kubernetes-0.25.0-py3-none-any.whl (32 kB)
...
Installing collected packages: oauthlib, requests-oauthlib, python-dateutil, pyasn1, rsa, cachetools, pyasn1-modules, google-auth, websocket-client, kubernetes, regex, pytz, tzlocal, dateparser, chaostoolkit-kubernetes
Successfully installed cachetools-4.2.1 chaostoolkit-kubernetes-0.25.0 dateparser-1.0.0 google-auth-1.24.0 kubernetes-12.0.1 oauthlib-3.1.0 pyasn1-0.4.8 pyasn1-modules-0.2.8 python-dateutil-2.8.1 pytz-2020.5 regex-2020.11.13 requests-oauthlib-1.3.0 rsa-4.7 tzlocal-2.1 websocket-client-0.57.0

Aquí vemos la conexión de Chaos Toolkit a nuestro cluster.

Solo nos falta configurar Chaos Toolkit para poder conectar a nuestro cluster de kubernetes. Si tenemos un KUBECONFIG configurado en la máquina donde hemos realizado la instalación, no tenemos que hacer nada adicional. Chaos Toolkit lo utilizará para conectarse a nuestro cluster. Si tenemos varios contextos en él, podemos incluir el contexto en cuestión en cada uno de los experimentos que definamos. Por último, en cada experimento podemos definir también los datos de conexión a nuestro cluster si no queremos hacer uso del KUBECONFIG.

El ciclo de vida de los experimentos

A la hora de definir nuestro experimento, tenemos que pensar al menos en 2 cosas:

Este es el flujo de ejecución que seguirá chaos toolkit:

Como podemos observar, hay cuatro fases de ejecución:

1. Se comprueba que la “steady state hypothesis” se cumple.

2. Se ejecuta el experimento.

3. Se comprueba de nuevo si la “steady state hypothesis” se sigue cumpliendo.

4. Se ejecuta la estrategia de rollback (si la definimos en nuestro experimento) y no ha habido ningún error en su ejecución, aunque este comportamiento lo podemos cambiar para que el rollback se ejecute siempre si así nos interesa.

Ejecutando los experimentos

Esta es la arquitectura de nuestra aplicación vista desde Kiali (describimos la funcionalidad de los componentes en el post anterior, a los cuales hemos añadido un nuevo servicio de precios):

Hemos introducido un nuevo servicio respecto al post anterior, priceconsumer. A través de Apache Kafka vamos a consumir un servicio que nos dará el precio de los libros. Mediante Istio, haremos que se llame a este servicio solo en el 50% de las llamadas. Durante este post no haremos uso de él, por lo que no entraremos en detalle.

Vamos a comenzar definiendo el primero de los experimentos señalados en el anterior post: drenaje de uno de los nodos.

Experimento 1: Drenaje de un nodo

Generamos un experimento en formato json, que es el que utiliza chaos toolkit, que tendrá un aspecto similar a este:

{
 "title": "Is the application available when a node is drained?",
 "description": "We expect Kubernetes to handle the situation gracefully when a node goes down",
 "tags": ["kubernetes"],
 "steady-state-hypothesis": {
     "title": "Verifying service remains healthy",
     "probes": [
         {
             "name": "all-microservices-should-be-healthy",
             "type": "probe",
             "tolerance": true,
             "provider": {
                 "type": "python",
                 "module": "chaosk8s.probes",
                 "func": "all_pods_healthy"
             }
         }
     ]
 },
 "method": [
     {
         "type": "action",
         "name": "drain-nodes",
         "provider": {
             "type": "python",
             "module": "chaosk8s.node.actions",
             "func": "drain_nodes",
             "arguments": {
               "delete_pods_with_local_storage": true,
               "count": 1             
             }
         },
         "pauses": {
             "after": 20
         }
     }
 ]
}

En nuestra hipótesis, definimos el estado que esperamos, en este caso utilizamos el probe “all_pods_healthy” del driver de kubernetes para Chaos Toolkit.

En cuanto al método, utilizaremos “drain_nodes”, con un count: 1 para que haga el drain únicamente de un nodo. Por el momento no añadimos rollback, y vamos a ver qué sucede.

Partiremos de 3 nodos worker en estado “Ready”:

kubectl get nodes

NAME STATUS ROLES AGE VERSION
ip-192-168-10-19.eu-west-1.compute.internal Ready 9h v1.18.9-eks-d1db3c
ip-192-168-50-122.eu-west-1.compute.internal Ready 9h v1.18.9-eks-d1db3c
ip-192-168-80-25.eu-west-1.compute.internal Ready 9h v1.18.9-eks-d1db3c

Procedemos a ejecutar el experimento, en este caso en modo verbose, para ver bien qué es lo que ocurre:

chaos --verbose run drain-nodes.json

[2021-02-16 18:57:22 DEBUG] [cli:74] ###############################################################################
[2021-02-16 18:57:22 DEBUG] [cli:75] Running command 'run'
[2021-02-16 18:57:22 DEBUG] [cli:79] Using settings file '/Users/roman-work/.chaostoolkit/settings.yaml'
[2021-02-16 18:57:22 DEBUG] [settings:26] The Chaos Toolkit settings file could not be found at '/Users/roman-work/.chaostoolkit/settings.yaml'.
[2021-02-16 18:57:22 DEBUG] [init:355] No controls to apply on 'loader'
[2021-02-16 18:57:22 DEBUG] [init:355] No controls to apply on 'loader'
[2021-02-16 18:57:22 DEBUG] [caching:25] Building activity cache...
[2021-02-16 18:57:22 DEBUG] [caching:35] Cached 2 activities
[2021-02-16 18:57:22 INFO] [experiment:47] Validating the experiment's syntax
[2021-02-16 18:57:22 DEBUG] [configuration:54] Loading configuration...
[2021-02-16 18:57:22 DEBUG] [secret:75] Loading secrets...
[2021-02-16 18:57:22 DEBUG] [secret:92] Secrets loaded
[2021-02-16 18:57:23 INFO] [experiment:96] Experiment looks valid
[2021-02-16 18:57:23 DEBUG] [caching:42] Clearing activities cache
[2021-02-16 18:57:23 DEBUG] [caching:25] Building activity cache...
[2021-02-16 18:57:23 DEBUG] [caching:35] Cached 2 activities
[2021-02-16 18:57:23 DEBUG] [configuration:54] Loading configuration...
[2021-02-16 18:57:23 DEBUG] [secret:75] Loading secrets...
[2021-02-16 18:57:23 DEBUG] [secret:92] Secrets loaded
[2021-02-16 18:57:23 INFO] [run:323] Running experiment: Is the application available when a node is drained?
[2021-02-16 18:57:23 DEBUG] [init:39] Initializing controls
[2021-02-16 18:57:23 INFO] [run:342] Steady-state strategy: default
[2021-02-16 18:57:23 INFO] [run:345] Rollbacks strategy: default
[2021-02-16 18:57:23 DEBUG] [init:355] No controls to apply on 'experiment'
[2021-02-16 18:57:23 DEBUG] [run:466] Running steady-state hypothesis before the method
[2021-02-16 18:57:23 INFO] [hypothesis:183] Steady state hypothesis: Verifying service remains healthy
[2021-02-16 18:57:23 DEBUG] [init:355] No controls to apply on 'hypothesis'
[2021-02-16 18:57:23 DEBUG] [init:355] No controls to apply on 'activity'
[2021-02-16 18:57:23 INFO] [activity:161] Probe: all-microservices-should-be-healthy
[2021-02-16 18:57:23 DEBUG] [python:34] Activity 'all-microservices-should-be-healthy' loaded from '/opt/homebrew/lib/python3.9/site-packages/chaosk8s/pod/probes.py'
[2021-02-16 18:57:23 DEBUG] [init:65] Using Kubernetes context: default
[2021-02-16 18:57:23 DEBUG] [probes:277] Found 0 failed and 0 not ready pods
[2021-02-16 18:57:23 DEBUG] [activity:180] => succeeded with 'True'
[2021-02-16 18:57:23 DEBUG] [init:355] No controls to apply on 'activity'
[2021-02-16 18:57:23 DEBUG] [hypothesis:211] allowed tolerance is True
[2021-02-16 18:57:23 INFO] [hypothesis:221] Steady state hypothesis is met!
[2021-02-16 18:57:23 DEBUG] [init:355] No controls to apply on 'hypothesis'
[2021-02-16 18:57:23 INFO] [run:553] Playing your experiment's method now...
[2021-02-16 18:57:23 DEBUG] [init:355] No controls to apply on 'method'
[2021-02-16 18:57:23 DEBUG] [init:355] No controls to apply on 'activity'
[2021-02-16 18:57:23 INFO] [activity:161] Action: drain-nodes
[2021-02-16 18:57:23 DEBUG] [python:34] Activity 'drain-nodes' loaded from '/opt/homebrew/lib/python3.9/site-packages/chaosk8s/node/actions.py'
[2021-02-16 18:57:23 DEBUG] [init:65] Using Kubernetes context: default
[2021-02-16 18:57:23 DEBUG] [init:65] Using Kubernetes context: default
[2021-02-16 18:57:24 DEBUG] [actions:86] Picked nodes 'ip-192-168-50-122.eu-west-1.compute.internal'
[2021-02-16 18:57:24 DEBUG] [init:65] Using Kubernetes context: default
[2021-02-16 18:57:24 DEBUG] [init:65] Using Kubernetes context: default
[2021-02-16 18:57:24 DEBUG] [actions:49] Filtering nodes by name ip-192-168-50-122.eu-west-1.compute.internal
[2021-02-16 18:57:24 DEBUG] [actions:51] Found 1 nodes
[2021-02-16 18:57:24 DEBUG] [actions:86] Picked nodes 'ip-192-168-50-122.eu-west-1.compute.internal'
[2021-02-16 18:57:25 DEBUG] [actions:265] Found 5 pods on node 'ip-192-168-50-122.eu-west-1.compute.internal'
[2021-02-16 18:57:25 DEBUG] [actions:305] Pod 'prometheus-node-exporter-xws4b' on node 'ip-192-168-50-122.eu-west-1.compute.internal' is owned by a DaemonSet. Will not evict it
[2021-02-16 18:57:25 DEBUG] [actions:286] Pod 'my-cluster-kafka-0' on node 'ip-192-168-50-122.eu-west-1.compute.internal' has a volume made of a local storage
[2021-02-16 18:57:25 DEBUG] [actions:292] Deleting anyway due to flag
[2021-02-16 18:57:25 DEBUG] [actions:286] Pod 'my-cluster-zookeeper-0' on node 'ip-192-168-50-122.eu-west-1.compute.internal' has a volume made of a local storage
[2021-02-16 18:57:25 DEBUG] [actions:292] Deleting anyway due to flag
[2021-02-16 18:57:25 DEBUG] [actions:305] Pod 'aws-node-6xxhs' on node 'ip-192-168-50-122.eu-west-1.compute.internal' is owned by a DaemonSet. Will not evict it
[2021-02-16 18:57:25 DEBUG] [actions:305] Pod 'kube-proxy-zswwr' on node 'ip-192-168-50-122.eu-west-1.compute.internal' is owned by a DaemonSet. Will not evict it
[2021-02-16 18:57:25 DEBUG] [actions:318] Found 2 pods to evict
[2021-02-16 18:57:25 DEBUG] [actions:338] Waiting for 2 pods to go
[2021-02-16 18:57:25 DEBUG] [actions:355] Pod 'my-cluster-kafka-0' still around in phase: Running
[2021-02-16 18:57:25 DEBUG] [actions:355] Pod 'my-cluster-zookeeper-0' still around in phase: Running
[2021-02-16 18:57:35 DEBUG] [actions:338] Waiting for 2 pods to go
[2021-02-16 18:57:35 DEBUG] [actions:363] Evicted all pods we could
[2021-02-16 18:57:35 DEBUG] [activity:180] => succeeded with 'True'
[2021-02-16 18:57:35 INFO] [activity:198] Pausing after activity for 20s...
[2021-02-16 18:57:55 DEBUG] [init:355] No controls to apply on 'activity'
[2021-02-16 18:57:55 DEBUG] [init:355] No controls to apply on 'method'
[2021-02-16 18:57:55 DEBUG] [run:496] Running steady-state hypothesis after the method
[2021-02-16 18:57:55 INFO] [hypothesis:183] Steady state hypothesis: Verifying service remains healthy
[2021-02-16 18:57:55 DEBUG] [init:355] No controls to apply on 'hypothesis'
[2021-02-16 18:57:55 DEBUG] [init:355] No controls to apply on 'activity'
[2021-02-16 18:57:55 INFO] [activity:161] Probe: all-microservices-should-be-healthy
[2021-02-16 18:57:55 DEBUG] [python:34] Activity 'all-microservices-should-be-healthy' loaded from '/opt/homebrew/lib/python3.9/site-packages/chaosk8s/pod/probes.py'
[2021-02-16 18:57:55 DEBUG] [init:65] Using Kubernetes context: default
[2021-02-16 18:57:56 DEBUG] [probes:277] Found 0 failed and 0 not ready pods
[2021-02-16 18:57:56 DEBUG] [activity:180] => succeeded with 'True'
[2021-02-16 18:57:56 DEBUG] [init:355] No controls to apply on 'activity'
[2021-02-16 18:57:56 DEBUG] [hypothesis:211] allowed tolerance is True
[2021-02-16 18:57:56 INFO] [hypothesis:221] Steady state hypothesis is met!
[2021-02-16 18:57:56 DEBUG] [init:355] No controls to apply on 'hypothesis'
[2021-02-16 18:57:56 INFO] [run:809] Let's rollback...
[2021-02-16 18:57:56 DEBUG] [init:355] No controls to apply on 'rollback'
[2021-02-16 18:57:56 INFO] [rollback:25] No declared rollbacks, let's move on.
[2021-02-16 18:57:56 DEBUG] [init:355] No controls to apply on 'rollback'
[2021-02-16 18:57:56 INFO] [run:421] Experiment ended with status: completed
[2021-02-16 18:57:56 DEBUG] [init:355] No controls to apply on 'experiment'
[2021-02-16 18:57:56 DEBUG] [init:72] Cleaning up controls
[2021-02-16 18:57:56 DEBUG] [caching:42] Clearing activities cache

Como vemos, lo primero que hace Chaos Toolkit antes de ejecutar el experimento es comprobar que la hipótesis que cumple. Todos los pods están funcionando correctamente, por lo que procede a ejecutar el experimento. Selecciona un nodo y fuerza el “evict” de los 5 pods que se encuentran en ejecución en él. Una vez realizado, vuelve a verificar la hipótesis y comprueba que no existen pods en “failed”.

¿Qué ocurre si vemos el estado de los nodos?

kubectl get nodes

NAME STATUS ROLES AGE VERSION
ip-192-168-10-19.eu-west-1.compute.internal Ready 9h v1.18.9-eks-d1db3c
ip-192-168-50-122.eu-west-1.compute.internal Ready,SchedulingDisabled 9h v1.18.9-eks-d1db3c
ip-192-168-80-25.eu-west-1.compute.internal Ready 9h v1.18.9-eks-d1db3c

Como podemos ver, el nodo drenado se mantiene en “SchedulingDisabled”. Aquí es donde cobra fuerza el rollback.

De forma opcional, mediante chaos toolkit podemos definir un tercer bloque en nuestro experimento para realizar esta operación. Si lo incluimos, su contenido se ejecutará cuando la steady state hypothesis no se cumpla después de haber ejecutado el experimento, con el objetivo de revertir los cambios realizados en nuestro sistema/aplicación. O bien, como en este caso, lo ejecutaremos siempre aunque la hipótesis se siga cumpliendo.

Debemos definir nuestra operación de rollback de tal forma que podamos realizar el “uncordon” de nuevo del nodo una vez finalizado el experimento. De lo contrario, el nodo permanecerá en este estado y el scheduler de kubernetes no arrancará pods en él. Modificamos nuestro experimento para incluir el rollback:

{
 "title": "Is the application available when a node is drained?",
 "description": "We expect Kubernetes to handle the situation gracefully when a node goes down",
 "tags": ["kubernetes"],
 "steady-state-hypothesis": {
     "title": "Verifying service remains healthy",
     "probes": [
         {
             "name": "all-microservices-should-be-healthy",
             "type": "probe",
             "tolerance": true,
             "provider": {
                 "type": "python",
                 "module": "chaosk8s.probes",
                 "func": "all_pods_healthy"
             }
         }
     ]
 },
 "method": [
     {
         "type": "action",
         "name": "drain-nodes",
         "provider": {
             "type": "python",
             "module": "chaosk8s.node.actions",
             "func": "drain_nodes",
             "arguments": {
               "delete_pods_with_local_storage": true,
               "count": 1             
             }
         },
         "pauses": {
             "after": 20
         }
     }
 ],
 "rollbacks": [
   {
       "type": "action",
       "name": "uncordon-node",
       "provider": {
           "type": "python",
           "module": "chaosk8s.node.actions",
           "func": "uncordon_node"
       }
   }
]

}

Volvemos a ejecutarlo de nuevo, esta vez sin la opción de debug:

chaos run drain-nodes.json

[2021-02-16 19:10:59 INFO] Validating the experiment's syntax
[2021-02-16 19:11:00 INFO] Experiment looks valid
[2021-02-16 19:11:00 INFO] Running experiment: Is the application available when a node is drained?
[2021-02-16 19:11:00 INFO] Steady-state strategy: default
[2021-02-16 19:11:00 INFO] Rollbacks strategy: default
[2021-02-16 19:11:00 INFO] Steady state hypothesis: Verifying service remains healthy
[2021-02-16 19:11:00 INFO] Probe: all-microservices-should-be-healthy
[2021-02-16 19:11:00 INFO] Steady state hypothesis is met!
[2021-02-16 19:11:00 INFO] Playing your experiment's method now...
[2021-02-16 19:11:00 INFO] Action: drain-nodes
[2021-02-16 19:11:23 INFO] Pausing after activity for 20s...
[2021-02-16 19:11:43 INFO] Steady state hypothesis: Verifying service remains healthy
[2021-02-16 19:11:43 INFO] Probe: all-microservices-should-be-healthy
[2021-02-16 19:11:43 INFO] Steady state hypothesis is met!
[2021-02-16 19:11:43 INFO] Let's rollback...
[2021-02-16 19:11:43 INFO] Rollback: uncordon-node
[2021-02-16 19:11:43 INFO] Action: uncordon-node
[2021-02-16 19:11:44 INFO] Experiment ended with status: completed

Si consultamos el estado de los nodos:

kubectl get nodes

NAME STATUS ROLES AGE VERSION
ip-192-168-10-19.eu-west-1.compute.internal Ready 10h v1.18.9-eks-d1db3c
ip-192-168-50-122.eu-west-1.compute.internal Ready 10h v1.18.9-eks-d1db3c
ip-192-168-80-25.eu-west-1.compute.internal Ready 10h v1.18.9-eks-d1db3c

Esta vez sí podemos ver que nuestros nodos quedan correctos después de la ejecución de nuestro experimento.

Experimento 2: Eliminación del 50% de los pods de un servicio

Pasamos al segundo experimento: vamos a eliminar la mitad de los pods de la aplicación ratings-v2.

Nuestro experimento quedaría así:

{
   "title": "Is the application available when half the pods are down?",
   "description": "We expect Kubernetes to handle the situation gracefully when a pod goes down",
   "tags": ["kubernetes"],
   "steady-state-hypothesis": {
       "title": "Verifying service remains healthy",
       "probes": [
           {
               "name": "all-our-ratings-v2-microservices-should-be-healthy",
               "type": "probe",
               "tolerance": true,
               "provider": {
                   "type": "python",
                   "module": "chaosk8s.probes",
                   "func": "deployment_available_and_healthy",
                   "arguments": {
                       "ns": "bookinfo",
                       "name": "ratings-v2"
                   }
               }
           }
       ]
   },
   "method": [
       {
           "type": "action",
           "name": "terminate-ratings-pod",
           "provider": {
               "type": "python",
               "module": "chaosk8s.pod.actions",
               "func": "terminate_pods",
               "arguments": {
           "ns": "bookinfo",
           "label_selector": "app=ratings",
           "name_pattern": "ratings-v2",
           "rand": true,
           "mode": "percentage",
           "qty": 50
               }
           },
           "pauses": {
               "after": 20
           }
       }
   ]
}

En nuestro caso, vamos a actuar sobre el microservicio v2 de ratings. Nuestro steady state va a ser comprobar mediante el método “chaosk8s.probes” que el Deployment ratings-v2 está en estado “Healthy” mediante el uso de la función “deployment_available_and_healthy”.

En cuanto al método, vamos a utilizar la función “terminate_pods” del método “chaosk8s.pod.actions” que parará de forma ordenada la mitad ("mode": "percentage", "qty": 50) de los pods de ratings ("label_selector": "app=ratings"), pero solo de la versión 2 ("name_pattern": "ratings-v2"). Hay que tener en cuenta que, mientras los pods existentes son eliminados y kubernetes levanta los nuevos, hacen falta unos segundos para que la aplicación esté en estado “Healthy” de nuevo. Para ello, podemos establecer unos segundos de pausa después de ejecutar el experimento. Hasta que este tiempo no pase, no se comprobará la steady state hypothesis de nuevo.

En este caso el rollback es innecesario, ya que k8s levantará nuevos pods sin necesidad de hacer nosotros nada.

Esta es nuestra aplicación desplegada actualmente:

kubectl get pods -n bookinfo

NAME READY STATUS RESTARTS AGE
details-v1-6fc55d65c9-nvfst 2/2 Running 2 6d16h
mongodb-v1-76b6db77d-kg9nd 2/2 Running 2 6d16h
productpage-v1-7f44c4d57c-rw9fm 2/2 Running 2 6d16h
ratings-v1-6f855c5fff-bp8nq 2/2 Running 0 4d
ratings-v2-7f5576697d-28np9 2/2 Running 0 40m
ratings-v2-7f5576697d-8lg9q 2/2 Running 0 40m
ratings-v2-7f5576697d-c6hpv 2/2 Running 0 40m
ratings-v2-7f5576697d-fs6dw 2/2 Running 0 40m
ratings-v2-7f5576697d-gw6s9 2/2 Running 0 40m
ratings-v2-7f5576697d-prl47 2/2 Running 0 40m
reviews-v1-54b8794ddf-hrfzq 2/2 Running 2 6d16h
reviews-v2-c4d6568f9-8ss54 2/2 Running 2 6d16h
reviews-v3-7f66977689-vfx6l 2/2 Running 2 6d16h

Tenemos 6 réplicas corriendo del pod ratings-v2. Si el experimento funciona correctamente, 3 pods serán eliminados y creados de nuevo por kubernetes.

Generamos carga sobre nuestra aplicación, por ejemplo utilizando Jmeter, y a continuación podemos proceder a ejecutar el experimento:

chaos run experiment-kill-pod.json

[2021-01-25 08:54:32 INFO] Validating the experiment's syntax
[2021-01-25 08:54:32 INFO] Experiment looks valid
[2021-01-25 08:54:32 INFO] Running experiment: Is the application available when half the pods are down?
[2021-01-25 08:54:32 INFO] Steady-state strategy: default
[2021-01-25 08:54:32 INFO] Rollbacks strategy: default
[2021-01-25 08:54:32 INFO] Steady state hypothesis: Verifying service remains healthy
[2021-01-25 08:54:32 INFO] Probe: all-our-ratings-v2-microservices-should-be-healthy
[2021-01-25 08:54:32 INFO] Steady state hypothesis is met!
[2021-01-25 08:54:32 INFO] Playing your experiment's method now...
[2021-01-25 08:54:32 INFO] Action: terminate-ratings-pod
[2021-01-25 08:54:32 INFO] Pausing after activity for 20s...
[2021-01-25 08:54:52 INFO] Steady state hypothesis: Verifying service remains healthy
[2021-01-25 08:54:52 INFO] Probe: all-our-ratings-v2-microservices-should-be-healthy
[2021-01-25 08:54:52 INFO] Steady state hypothesis is met!
[2021-01-25 08:54:52 INFO] Let's rollback...
[2021-01-25 08:54:52 INFO] No declared rollbacks, let's move on.
[2021-01-25 08:54:52 INFO] Experiment ended with status: completed

Podemos comprobar el ciclo de ejecución del experimento. En primer lugar, chaos toolkit comprueba que nuestra steady state hypothesis se cumple. A continuación, ejecuta el método descrito en nuestro json y pausa la ejecución durante los 20 segundos que hemos descrito. Por último, vuelve a comprobar la steady state hypothesis para asegurarse de que nuestra aplicación sigue en estado “Ready”.

Durante la ejecución, podemos comprobar que nuestra aplicación efectivamente sigue funcionando correctamente:

Si miramos los pod existentes, veremos una salida como esta:

kubectl get pods -n bookinfo

NAME READY STATUS RESTARTS AGE
details-v1-6fc55d65c9-nvfst 2/2 Running 2 6d16h
mongodb-v1-76b6db77d-kg9nd 2/2 Running 2 6d16h
productpage-v1-7f44c4d57c-rw9fm 2/2 Running 2 6d16h
ratings-v1-6f855c5fff-bp8nq 2/2 Running 0 4d
ratings-v2-7f5576697d-28np9 2/2 Running 0 42m
ratings-v2-7f5576697d-5s8hv 2/2 Running 0 25s
ratings-v2-7f5576697d-7d9gf 2/2 Running 0 25s
ratings-v2-7f5576697d-8lg9q 2/2 Terminating 0 42m
ratings-v2-7f5576697d-9jm8g 2/2 Running 0 25s
ratings-v2-7f5576697d-c6hpv 2/2 Terminating 0 42m
ratings-v2-7f5576697d-fs6dw 2/2 Running 0 42m
ratings-v2-7f5576697d-gw6s9 2/2 Terminating 0 42m
ratings-v2-7f5576697d-prl47 2/2 Running 0 42m
reviews-v1-54b8794ddf-hrfzq 2/2 Running 2 6d16h
reviews-v2-c4d6568f9-8ss54 2/2 Running 2 6d16h
reviews-v3-7f66977689-vfx6l 2/2 Running 2 6d16h

Como podemos observar, chaos toolkit ha eliminado 3 de los pod de ratings-v2, y kubernetes ha creado otros 3 nuevos que están en estado 2/2 Running. Es por ello que nuestra aplicación vuelve a encontrarse en estado “Ready” y el experimento concluye de forma satisfactoria.

Durante la ejecución del experimento, acudimos a nuestro dashboard en Grafana para ver si tenemos algún error en nuestra aplicación:

El success rate en ningún momento baja del 100%, por lo que podemos concluir que, a pesar de perder la mitad de los pods, la aplicación no pierde servicio en ningún caso.

Experimento 3: Pérdida de servicio

Procedemos con el tercer experimento: vamos a eliminar el servicio ratings completamente.

Vamos a realizar modificaciones sobre nuestro experimento para eliminar el 100% de pods del microservicio ratings de ambas versiones:

{
   "title": "Is the application available when half the pods are down?",
   "description": "We expect Kubernetes to handle the situation gracefully when a pod goes down",
   "tags": ["kubernetes"],
   "steady-state-hypothesis": {
       "title": "Verifying service remains healthy",
       "probes": [
           {
               "name": "all-our-ratings-v2-microservices-should-be-healthy",
               "type": "probe",
               "tolerance": true,
               "provider": {
                   "type": "python",
                   "module": "chaosk8s.probes",
                   "func": "deployment_available_and_healthy",
                   "arguments": {
           "ns": "bookinfo",
               "name": "ratings-v2"
                   }
               }
           }
       ]
   },
   "method": [
       {
           "type": "action",
           "name": "terminate-ratings-pod",
           "provider": {
               "type": "python",
               "module": "chaosk8s.pod.actions",
               "func": "terminate_pods",
               "arguments": {
           "ns": "bookinfo",
           "label_selector": "app=ratings",
           "rand": true,
           "mode": "percentage",
           "qty": 100
               }
           },
           "pauses": {
               "after": 20
           }
       }
   ]
}

Y lo ejecutamos. En este caso, vemos que durante unos instantes el servicio completo cae, reduciendo las peticiones a 0 y provocando errores 500 en nuestra aplicación. Es un caso extremo con el que contábamos que el servicio caería, pero nos ayuda a poner a prueba que nuestro dashboard está funcionando correctamente.

Experimento 4: Incremento de la latencia

Y, por último, vamos a por el cuarto experimento: generar indisponibilidad en el servicio reviews en base a incrementar latencia.

Ya que disponemos de Istio en nuestro entorno, vamos a ver también cómo se ejecutan experimentos con el driver de Istio para chaos toolkit. Al estar utilizando la aplicación bookinfo, podemos aprovechar para descubrir mediante chaos toolkit un bug que tiene la aplicación introducido de manera intencionada. Desde el servicio reviews existe un timeout de 10 segundos hacia el servicio ratings. Vamos a probar primero a crear un experimento que simule la conexión de un usuario a la aplicación, creando un delay de 5s mediante un VirtualService de Istio, pero estableciendo un timeout al conectar de 1s (usaremos curl en este caso). Al ejecutar el experimento, revelaremos este problema en la aplicación:

cat experiment-network-latency.json
{
   "title": "Network latency does not impact our users",
   "description": "Using Istio fault injection capability, let's explore how latency impacts a single user",
   "configuration": {
       "product_page_url": {
           "type": "env",
           "key": "PRODUCT_PAGE_SERVICE_BASE_URL"
       }
   },
   "steady-state-hypothesis": {
       "title": "Our service should respond under 1 second",
       "probes": [
           {
               "type": "probe",
               "name": "sign-in-as-jason",
               "tolerance": 0,
               "provider": {
                   "type": "process",
                   "path": "curl",
                   "arguments": "-v -X POST -d 'username=jason&passwd=' -c /tmp/cookie.txt --silent ${product_page_url}/login"
               }
           },
           {
               "type": "probe",
               "name": "fetch-productpage-for-jason-in-due-time",
               "tolerance": 0,
               "provider": {
                   "type": "process",
                   "path": "curl",
                   "arguments": "-v --connect-timeout 1 --max-time 1 -b /tmp/cookie.txt --silent ${product_page_url}/productpage"
               }
           }
       ]
   },
   "method": [
       {
           "type": "action",
           "name": "inject-fault-for-reviews-service",
           "provider": {
               "type": "python",
               "module": "chaosistio.fault.actions",
               "func": "add_delay_fault",
               "secrets": ["istio"],
               "arguments": {
           "ns": "bookinfo",
                   "virtual_service_name": "reviews",
                   "fixed_delay": "5s",
                   "percentage": 100.0,
                   "routes": [
                       {
                           "destination": {
                               "host": "reviews",
                               "subset": "v3"
                           }
                       }
                   ]
               }
           },
           "pauses": {
               "after": 2
           }
       }
   ],
   "rollbacks": [
       {
           "type": "action",
           "name": "remove-fault-for-reviews-service",
           "provider": {
               "type": "python",
               "module": "chaosistio.fault.actions",
               "func": "remove_delay_fault",
               "secrets": ["istio"],
               "arguments": {
           "ns": "bookinfo",
                   "virtual_service_name": "reviews",
                   "routes": [
                       {
                           "destination": {
                               "host": "reviews",
                               "subset": "v3"
                           }
                       }
                   ]
               }
           }
       }
   ]
}

Para indicar el endpoint donde es accesible nuestra aplicación, haremos uso de la variable de entorno ”PRODUCT_PAGE_SERVICE_BASE_URL”.

Como probe vamos a utilizar el provider “process” para ver otro ejemplo de uso. En este caso, nos va a permitir valorar el exit code de dicho proceso, y utilizaremos un simple curl para tal fin. Para este mismo caso podríamos también utilizar el provider type “http” para evaluar la respuesta HTTP del servicio. Queremos que nuestra aplicación conteste como máximo en 1 segundo, por lo que añadimos esta opción al curl.

Para este experimento, utilizaremos el método “add_delay_fault” de Istio, que nos permitirá añadir un delay al servicio reviews, en nuestro caso de 5s al 100% de peticiones.

Cabe destacar que, a nivel de Istio (VirtualService), podríamos incluir mediante el atributo “match” que este delay afecte a un usuario completo examinando los “headers”. Pero, en este momento, el método “add_delay_fault” no implementa esta funcionalidad, por lo que vamos a afectar a todos los usuarios del servicio.

Procedemos a ejecutarlo:

chaos run experiment-network-latency.json

[2021-01-27 14:55:17 INFO] Validating the experiment's syntax
[2021-01-27 14:55:18 INFO] Experiment looks valid
[2021-01-27 14:55:18 INFO] Running experiment: Network latency does not impact our users
[2021-01-27 14:55:18 INFO] Steady-state strategy: default
[2021-01-27 14:55:18 INFO] Rollbacks strategy: default
[2021-01-27 14:55:18 INFO] Steady state hypothesis: Our service should respond under 1 second
[2021-01-27 14:55:18 INFO] Probe: sign-in-as-jason
[2021-01-27 14:55:18 INFO] Probe: fetch-productpage-for-jason-in-due-time
[2021-01-27 14:55:18 INFO] Steady state hypothesis is met!
[2021-01-27 14:55:18 INFO] Playing your experiment's method now...
[2021-01-27 14:55:18 INFO] Action: inject-fault-for-reviews-service
[2021-01-27 14:55:18 INFO] Pausing after activity for 2s...
[2021-01-27 14:55:20 INFO] Steady state hypothesis: Our service should respond under 1 second
[2021-01-27 14:55:20 INFO] Probe: sign-in-as-jason
[2021-01-27 14:55:20 INFO] Probe: fetch-productpage-for-jason-in-due-time
[2021-01-27 14:55:21 CRITICAL] Steady state probe 'fetch-productpage-for-jason-in-due-time' is not in the given tolerance so failing this experiment
[2021-01-27 14:55:21 INFO] Experiment ended with status: deviated
[2021-01-27 14:55:21 INFO] The steady-state has deviated, a weakness may have been discovered

En este caso, la comprobación de nuevo de la hipótesis después de ejecutar el experimento falla, debido a que al lanzar el curl en el probe, hemos especificado un timeout de 1s, y nuestro delay es de 5 segundos.

Comprobando la aplicación en este momento, veremos que sigue disponible pero sin el servicio de reviews (ni el de ratings, ya que depende de él):

Es importante tener en cuenta en este caso que hemos modificado el VirtualService de Istio, por lo que, al ejecutar el experimento, el delay seguirá establecido. De esta forma, nos permite diagnosticar qué es lo que está ocurriendo en nuestra aplicación para poder corregir el problema. Para estos casos, debemos utilizar también la funcionalidad de rollback de chaos toolkit.

Si examinamos el contenido del VirtualService, observamos que los parámetros del delay siguen estando presentes:

kubectl get virtualservice reviews -n bookinfo -o yaml
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"networking.istio.io/v1alpha3","kind":"VirtualService","metadata":{"annotations":{},"name":"reviews","namespace":"bookinfo"},"spec":{"hosts":["reviews"],"http":[{"route":[{"destination":{"host":"reviews","subset":"v3"}}]}]}}
  creationTimestamp: "2021-01-26T16:43:04Z"
  generation: 7
  managedFields:
  - apiVersion: networking.istio.io/v1alpha3
    fieldsType: FieldsV1
    fieldsV1:
      f:metadata:
        f:annotations:
          .: {}
          f:kubectl.kubernetes.io/last-applied-configuration: {}
      f:spec:
        .: {}
        f:hosts: {}
    manager: kubectl
    operation: Update
    time: "2021-01-26T16:43:04Z"
  - apiVersion: networking.istio.io/v1alpha3
    fieldsType: FieldsV1
    fieldsV1:
      f:spec:
        f:http: {}
    manager: OpenAPI-Generator
    operation: Update
    time: "2021-01-27T10:44:36Z"
  name: reviews
  namespace: bookinfo
  resourceVersion: "77444"
  selfLink: /apis/networking.istio.io/v1beta1/namespaces/bookinfo/virtualservices/reviews
  uid: e3375f7f-e681-4ae6-8f06-2f8d00f0bbe3
spec:
  hosts:
  - reviews
  http:
  - fault:
      delay:
        fixedDelay: 5s
        percentage:
          value: 100
    route:
    - destination:
        host: reviews
        subset: v3

Tenemos el rollback definido en nuestro experimento para forzar su ejecución independientemente del resultado. Podemos especificar la opción rollback-strategy=always, o bien con la opción rollback-strategy=deviation se ejecutará si se produce alguna desviación el comprobar la hipótesis:

chaos run experiment-network-latency.json 
--rollback-strategy=deviated

[2021-01-27 14:20:00 INFO] Validating the experiment's syntax
[2021-01-27 14:20:01 INFO] Experiment looks valid
[2021-01-27 14:20:01 INFO] Running experiment: Network latency does not impact our users
[2021-01-27 14:20:01 INFO] Steady-state strategy: default
[2021-01-27 14:20:01 INFO] Rollbacks strategy: deviated
[2021-01-27 14:20:01 INFO] Steady state hypothesis: Our service should respond under 1 second
[2021-01-27 14:20:01 INFO] Probe: sign-in-as-jason
[2021-01-27 14:20:01 INFO] Probe: fetch-productpage-for-jason-in-due-time
[2021-01-27 14:20:01 INFO] Steady state hypothesis is met!
[2021-01-27 14:20:01 INFO] Playing your experiment's method now...
[2021-01-27 14:20:01 INFO] Action: inject-fault-for-jason-only
[2021-01-27 14:20:01 INFO] Pausing after activity for 2s...
[2021-01-27 14:20:03 INFO] Steady state hypothesis: Our service should respond under 1 second
[2021-01-27 14:20:03 INFO] Probe: sign-in-as-jason
[2021-01-27 14:20:03 INFO] Probe: fetch-productpage-for-jason-in-due-time
[2021-01-27 14:20:04 CRITICAL] Steady state probe 'fetch-productpage-for-jason-in-due-time' is not in the given tolerance so failing this experiment
[2021-01-27 14:20:04 WARNING] Rollbacks will be played only because the experiment deviated
[2021-01-27 14:20:04 INFO] Let's rollback...
[2021-01-27 14:20:04 INFO] Rollback: remove-fault-for-jason-only
[2021-01-27 14:20:04 INFO] Action: remove-fault-for-jason-only
[2021-01-27 14:20:04 INFO] Experiment ended with status: deviated
[2021-01-27 14:20:04 INFO] The steady-state has deviated, a weakness may have been discovered

Si revisamos el código de la aplicación disponible en Github, veremos que hay hardcodeados timeouts para poder ver cómo se comporta Istio con estos delays.

Conclusiones

Hemos visto de forma rápida cómo empezar a ejecutar experimentos con Chaos Toolkit: desde su instalación, aprovechando por el camino la facilidad en sus extensiones, siendo capaces de ejecutar experimentos tanto a nivel de plataforma kubernetes como directamente en la malla de servicios, y aprovechando el driver de Chaos Toolkit para Istio.

Esperamos que estos ejemplos os ayuden a entender cómo funciona y qué nos aporta una herramienta como Chaos Toolkit para aumentar la resiliencia de nuestras aplicaciones.

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete

Estamos comprometidos.

Tecnología, personas e impacto positivo.