Kubernetes: Istio Traffic Management

Buenas, hoy hablaremos de la gestión de tráfico con Istio sobre Kubernetes. En anteriores posts, ya hablamos de como desplegar Istio. Y ahora toca poner en práctica lo que se puede hacer.

Para ello, vamos a utilizar imágenes de docker muy simples que son un Nginx con un HTML que muestra o versión 1 o versión 2 dependiendo el tag que se aplique en el Deployment. También usaremos nip.io para crear los DNSs y siege para hacer pruebas de carga y ver los resultados.

Los ficheros que vamos a utilizar, están en este repo:
https://github.com/ichasco/Istio-traffic-management

Let’s Go!

Traffic Split

El Traffic Split o comúnmente denominado Canary, es dividir el tráfico entre diferentes versiones de una aplicación, de una forma controlada. Esto nos vale en el caso de desplegar una versión nueva, para poder comprobar sobre un pequeño porcentaje, que no haya errores. Y una vez comprobado, hacer que todo el tráfico vaya a la nueva versión.

En Istio, el traffic split, se hace por pesos (weights). Hay que definir en el VirtualService el balanceo de tráfico que queremos y aplicarlo por subsets. Una vez hecho esto, se definen los subsets en el DestinationRule con las labels del Deployment y ya estaría.

https://github.com/ichasco/Istio-traffic-management/tree/master/01-traffic-split

A continuación lo explico con un ejemplo:

Deployment

En el Deployment, vamos a desplegar PODs con diferentes versiones de contenedor. Lo importante en este caso, es definir el spec.template.metadata.version con la versión correcta.

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: traffic-split-v1
  namespace: istio-poc
  labels:
    app: traffic-split
spec:
  replicas: 1
  selector:
    matchLabels:
      app: traffic-split
  template:
    metadata:
      labels:
        app: traffic-split
        version: v1
    spec:
      containers:
      - name: traffic-split
        image: ichasco/test-app:v1
        imagePullPolicy: Always

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: traffic-split-v2
  namespace: istio-poc
  labels:
    app: traffic-split
spec:
  replicas: 1
  selector:
    matchLabels:
      app: traffic-split
  template:
    metadata:
      labels:
        app: traffic-split
        version: v2
    spec:
      containers:
      - name: traffic-split
        image: ichasco/test-app:v2
        imagePullPolicy: Always

Service

El service no tiene nada especial. Lo único que se hace es definir el puerto y a que aplicación aplica.

---
kind: Service
apiVersion: v1
metadata:
  name: traffic-split
  namespace: istio-poc
  labels:
    app: traffic-split
spec:
  selector:
    app: traffic-split
  type: ClusterIP
  ports:
  - name: http
    port: 80

VirtualService

En el VirtualService, tenemos que definir tanto el host, como los pesos que tendrán las aplicaciones.

---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: traffic-split
  namespace: istio-poc
spec:
  hosts:
  - traffic-split.104.155.93.105.nip.io
  gateways:
  - istio-poc
  http:
  - name: traffic-split
    match:
    - uri:
        prefix: /
    route:
    - destination:
        host: traffic-split
        subset: v1
      weight: 80
    - destination:
        host: traffic-split
        subset: v2
      weight: 20

DestinationRule

Aquí tenemos que definir los subsets que hemos aplicado en el VirtualService.

---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: traffic-split
spec:
  host: traffic-split
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v2
    labels:
      version: v2

Prueba

Para probar esto, vamos a lanzar un test de carga con la herramienta siege y veremos los resultados en Kiali.

siege -c 5 -r 1000 http://traffic-split.35.240.106.245.nip.io/

Como se puede observar, se está dividiendo el tráfico en un 80/20 sobre las versiones de la aplicación.

Traffic Header Rules

En Istio, cuando se le quiere dar algo de lógica al tráfico, poner condiciones, establecer pesos… se utilizan los VirtualServices. En este ejemplo, vamos a hacer que el tráfico que vaya desde un user-agent (Firefox) vaya contra la versión 1 y lo que vaya con otro (curl), vaya a la versión 2. Esto se puede extrapolar a por ejemplo, discriminación de sistemas operativos; Linux/Windows, Android/iOS… O también si alguna de las headers de la petición trae algún parámetro en concreto; localización, usuario…

https://github.com/ichasco/Istio-traffic-management/tree/master/02-traffic-rule

Para hacer esto, hay que cambiar el VirtualService

  http:
    - match:
      - headers:
          user-agent:
            regex: '.*Firefox.*'
      route:
      - destination:
          host: traffic-rule
          subset: v1
    - match:
      - headers:
          user-agent:
            regex: '.*curl.*'
      route:
      - destination:
          host: traffic-rule
          subset: v2

Ahora para probar que funciona, vamos a lanzar un curl con los diferentes user-agents

curl -A Firefox traffic-rule.104.155.93.105.nip.io
Version 1
curl traffic-rule.104.155.93.105.nip.io
Version 2

Traffic Mirroring

Esta técnica, es bastante efectiva si no estamos seguros de que la nueva versión que queremos desplegar, vaya a funcionar y no queremos ninguna pérdida de servicios. Esto lo que hace, es hacer un mirror de todo el tráfico.

https://github.com/ichasco/Istio-traffic-management/tree/master/03-traffic-mirror

Vamos a empezar mandando todo el tráfico a la versión 1 para ver que todo va bien. Y haremos que el mismo tráfico que va la V1, vaya a la V2 y revisaremos los logs.

Para esto, solo hay que cambiar en el VirtualService lo siguiente:

    route:
    - destination:
        host: traffic-mirror
        subset: v1
      weight: 100

Y el resultado sería

Como se puede ver, todo el tráfico está yendo contra la v1. Ahora vamos a hacer que se replique contra el v2.

Para hacer esto, hay que volver a cambiar el VirtualService y dejarlo de la siguiente manera:

route:
    - destination:
        host: traffic-mirror
        subset: v1
      weight: 100
    mirror:
        host: traffic-mirror
        subset: v2
    mirror_percent: 100

Como se puede ver, el tráfico sigue yendo contra la v1, pero la v2 se ha puesto como mirror. Y de este modo, podemos comprobar todos los logs de esta versión y ver si hay algún error.

Circuit Breaking

Ahora vamos a hablar del Circuit Breaking. Con esto, podemos limitar y dar conmutar los errores. Podemos limitar nuestra aplicación a que solo se tenga X request y limitar o deshabilitar keepalive contra el backend. ¿En qué caso se puede aplicar esto? Pues si por ejemplo, tenemos un e-comerce seguramente prefiramos desechar peticiones, a que nos tiren el site. Hay otras opciones mas viables que esta, como utilizar caché.
También se puede hacer una lógica de que si en un intervalo, da mas de X errores, esa aplicación se considere como Unhealthy y el tráfico se conmute a otra.

https://github.com/ichasco/Istio-traffic-management/tree/master/04-circuit-breaker

Para empezar vamos a lanzar un test de carga contra la web sin aplicar ninguna restricción.

Transactions:                   5000 hits
Availability:                 100.00 %
Elapsed time:                  65.18 secs
Data transferred:               0.56 MB
Response time:                  0.06 secs
Transaction rate:              76.71 trans/sec
Throughput:                     0.01 MB/sec
Concurrency:                    4.98
Successful transactions:        5000
Failed transactions:               0
Longest transaction:            0.42
Shortest transaction:           0.05

Como se puede observar, todas las peticiones han sido satisfactorias. Ahora vamos a modificar el DestinationRule, y darle la siguiente lógica:

  trafficPolicy:
    connectionPool:
      http:
        http1MaxPendingRequests: 1
        maxRequestsPerConnection: 1
    outlierDetection:
      consecutiveErrors: 1
      interval: 1s
      baseEjectionTime: 3m
      maxEjectionPercent: 100

Con esto controlamos lo siguiente:

  • http1MaxPendingRequests: Máximo número de peticiones HTTP pendientes a un destino.
  • maxRequestsPerConnection: Máximo número de peticiones por conexión hacia un backend.
  • consecutiveErrors: Errores consecutivos antes de descartar el host.
  • interval: Intervalo de verificación de los host descartados.
  • baseEjectionTime: Tiempo que el host será retirado del pool antes de volver a ser evaluado.
  • maxEjectionPercent: Porcentaje máximo de hosts que se pueden descartar. Si es 100, todo los hosts que estén lanzando errores, serán descartados.

Se puede observar en kiali, que ha empezado a descartar tráfico y que en el diagrama, ha aparecido un rayo (símbolo del circuit breaker). Si no tenemos bien configurado el outlierDetection, posiblemente nos acabe descartando todos los hosts y se quede el stream unhealthy.

En cuanto a los datos de siege, podemos ver que hay peticiones que han sido descartadas.

Transactions:                   4393 hits
Availability:                  87.86 %
Elapsed time:                  62.34 secs
Data transferred:               0.51 MB
Response time:                  0.07 secs
Transaction rate:              70.47 trans/sec
Throughput:                     0.01 MB/sec
Concurrency:                    4.98
Successful transactions:        4393
Failed transactions:             607
Longest transaction:            0.22
Shortest transaction:           0.05

Troubleshooting

Si se activa el mTLS, hay que configurar en el DestinationRule para que lo soporte.

spec:
  host: traffic-split
  trafficPolicy:
    tls:
      mode: ISTIO_MUTUAL

Como se puede ver, es bastante fácil jugar con la red. Además de esto, se pueden controlar varios factores mas como; Fault Injection, Request Timeout, Circuit Breaking… Abstrayendonos de la aplicación y configurándolo directamente en Istio.


Also published on Medium.

Leave a Reply

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *