QoE

Implementación canaria con NGINX

agosto 5, 2015

Acaba de implementar la primera versión de su servicio web, a sus usuarios les gusta y no pueden dejar de usarlo, y ahora contempla implementar una versión mejorada del servicio de manera que:

  • No debe haber ninguna interrupción del servicio mientras implementa y configura la nueva versión, incluso si esto implica detener y reiniciar varios procesos que componen su servicio.
  • Puede ejecutar ambas versiones en paralelo durante un tiempo, cada versión en una parte controlada del tráfico, para validar que la nueva versión del servicio se comporte como se esperaba. Esto se conoce como canary-deployment, o despliegue azul-verde, y también es una instancia de prueba A/B.
  • El mecanismo de control de tráfico debe ser configurable sin interrupción del servicio. Por ejemplo, si la nueva versión parece funcionar mal, el tráfico se puede dirigir rápidamente para volver a la versión anterior.
  • Los mecanismos de control de tráfico deben permitir tanto el control basado en porcentajes (p. ej., 1% del tráfico público debe dirigirse a la nueva versión) como el control dependiente del cliente, por ejemplo, basado en la dirección IP o encabezados HTTP como User-agent. y Recomendador (por ejemplo, empleados o probadores conocidos, deben ser dirigidos a la nueva versión), o una combinación de los mismos.
  • Los mecanismos de control de tráfico deben garantizar la "permanencia": todas las solicitudes HTTP de un cliente deben terminar siendo atendidas por la misma versión, para una configuración de control de tráfico determinada.
  • Las versiones separadas están usando la misma base de datos. Esta es una tarea difícil, porque requiere que diseñes la aplicación web con compatibilidad hacia adelante y hacia atrás. Esto no es tan difícil como parece y es esencial para lograr una implementación y reversión incrementales. Hablaremos sobre cómo lograr esto en una publicación futura.

Canary deployment es una técnica muy útil y existen múltiples mecanismos que se pueden usar para implementarla. En esta publicación, describo cómo hacerlo si está usando NGINX como su servidor web frontal, con los ejemplos específicos para un Django basado en uWSGI servicio web, aunque se puede utilizar una configuración similar para otros tipos de servicios web. En una publicación futura, describiré una forma un poco más compleja de lograr lo mismo usando HAProxy.
Aprendí estas técnicas mientras desarrollaba el servicio web Touchstone para Conviva Inc.  Conviva es líder en análisis y optimización de calidad de video, y Touchstone es un servicio web patentado que los clientes de Conviva usan para probar y ajustar su integración de las bibliotecas de Conviva en los reproductores de video. Dado que Conviva tiene clientes en muchas zonas horarias y hay una serie de herramientas de prueba continua automatizadas que utilizan el servicio web Touchstone, no hay un momento conveniente para interrumpir el servicio para implementar actualizaciones.

Describamos ahora un ejemplo de configuración de control de tráfico que queremos lograr (como se muestra en la siguiente figura):

  • Queremos implementar tres instancias separadas del servicio web, cada una con su propio conjunto de archivos de activos estáticos y su propio servidor uWSGI que ejecuta versiones posiblemente diferentes de la aplicación Django. Llamamos a estas versiones "alfa" (versión menos probada por los primeros usuarios), "beta" (se cree que está lista para disponibilidad general) y "ga" (versión reforzada de disponibilidad general). Tenga en cuenta que todas estas instancias de la aplicación Django usan la misma base de datos, y todo el soporte de compatibilidad hacia adelante y hacia atrás para los datos persistentes está integrado en la propia aplicación.
  • Identificamos a los clientes que provienen de los grupos de "empleados" y "probadores", en función de sus direcciones IP públicas. Queremos enviar 100% de tráfico de "empleados" y 30% de tráfico del grupo "probador" a la instancia "alfa". La instancia "beta" obtendrá el resto del tráfico "probador" y también 1% del tráfico público. Finalmente, 99% del tráfico público debe ir a la instancia "ga".

A Chart Of Nginx, A Web Server Configuration Language

NGINX es un servidor web de alto rendimiento y un servidor proxy inverso y tiene un flexible idioma de configuración para controlar cómo maneja las solicitudes entrantes. Ya sabe que un buen diseño de implementación para un servicio web debe usar un servidor web real para los activos estáticos y para la terminación de HTTPS, con una configuración de proxy inverso para su aplicación real (llamada aplicación "ascendente" en este contexto). NGINX es una muy buena opción como servidor web y, como mostramos aquí, resulta que puede hacer mucho más por su implementación que servir archivos estáticos y terminar las conexiones HTTPS.
Aprovecharemos las siguientes características del lenguaje de configuración NGINX:

  • Las configuraciones de NGINX pueden usar variables, que se establecen para cada solicitud en función del contenido de la solicitud HTTP entrante. Luego, las variables se pueden usar para calcular otras variables y, en última instancia, para controlar cómo se maneja cada solicitud, como a qué aplicación ascendente se envía o desde qué directorio se sirven los archivos estáticos.
  • La directiva de configuración geo establece el valor de una variable en función de la dirección IP de la que proviene una solicitud:
    # La variable "$ip_range" se establece en "empleados" o "testers",
    # o “público”, basado en la dirección IP del cliente
    geo $ip_rango {
    69.12.128.0/17 empleados; # Direcciones IP de nuestra oficina
    128.32.0.0/16 probadores; # Dirección IP de nuestros probadores
    público predeterminado; # Todos los demás
    }

     

  • la directiva clientes_divididos establece el valor de una variable en función de una división porcentual aleatoria con rigidez personalizada. En el siguiente ejemplo, el valor de la variable $remote_addr (la dirección IP del cliente) se concatena con la cadena "AAA" y el resultado se convierte en un número de 32 bits. El valor de la variable definida se establece en función de en qué parte del rango de 32 bits cae el valor hash:
    # La variable "$split" está configurada en diferentes rangos porcentuales
    # pegajoso por remote_addr (dirección IP del cliente)
    split_clients "${remote_addr}AAA" $split {
    1% fracción1; # 1% de direcciones remotas asignadas a "fracción1"
    30% fracción2; # 30% de direcciones remotas asignadas a "fracción2"
    * otro; # resto a "otro"
    }

    Este esquema garantiza que dos solicitudes con la misma dirección IP darán como resultado el mismo valor para la variable "$split". Este esquema se puede adaptar utilizando otras variables en lugar de, o además de "$remote_addr", en la directiva split_client.
    Tenga en cuenta que la notación “${remote_addr}AAA” realiza una interpolación de cadenas, calculando una cadena basada en el valor de la variable $remote_addr concatenada con “AAA”.

  • los mapa La directiva se puede usar para calcular el valor de una variable condicionalmente en función de otras variables. En el siguiente ejemplo, la variable "$instance" se establece en función de una concatenación de las variables $ip_range y $split calculadas anteriormente, utilizando coincidencias de expresiones regulares.
    # La variable "$instance" se establece en función de $ip_range y $split.
    mapa "${ip_range}_${split}" $instance {
    "~^empleados_.*" alfa; # todo, desde "empleados" hasta "alfa"
    "~^testers_fraction2$" alfa; # 30% de "probadores" a "alfa"
    "~^probadores_.*" beta; # el resto de "testers" a "beta"
    "~^public_fraction1$" beta; # 1% del público a "beta"
    predeterminado ga; # todo lo demás a "ga"
    }

     

    El prefijo "~" le dice a "mapa" que use la coincidencia de expresiones regulares, y las diferentes cláusulas de "mapa" se evalúan en orden hasta que una coincida.

Todo lo que tenemos que hacer ahora es usar el valor de la variable $instance para decidir a qué instancia de la aplicación web enviar solicitudes de proxy, como se muestra a continuación:

Controladores ascendentes # uUSGI, sockets UNIX separados para cada instancia
upstream app_alpha_django {
servidor unix:///opt/app_alpha/uwsgi.sock;
}
upstream app_beta_django {
servidor unix:///opt/app_beta/uwsgi.sock;
}
upstream app_ga_django {
servidor unix:///opt/app_ga/uwsgi.sock;
}
servidor {
escuchar 80;
ubicación /estático {
# Cada instancia tiene sus propios archivos estáticos
root /opt/app_${instancia}/static;
}

# Definir los parámetros comunes para todas las solicitudes de uwsgi
ubicación / {
uwsgi_param QUERY_STRING $query_string;
uwsgi_param REQUEST_METHOD $request_method;
… más parámetros estándar de uwsgi …
# Cada instancia tiene su propio servidor upstream
uwsgi_pass app_${instancia}_django;
}
}

 

¡Eso es todo! Bueno, casi. También utilizo un script para generar la configuración anterior en función de una especificación de las diferentes instancias, los rangos de IP y los porcentajes. Por ejemplo, el script puede generar rápidamente una configuración que obligue a todo el tráfico a una sola instancia. Finalmente, la secuencia de comandos prueba la configuración de NGINX, y solo entonces le dice a NGINX que cargue la nueva configuración (algo que NGINX puede hacer sin descartar solicitudes):

# Asegúrese de decirle a nginx que pruebe la configuración antes de recargar
sudo /usr/sbin/nginx -t
sudo kill -HUP `cat /var/run/nginx.pid`

Espero que esta publicación lo ayude a aprovechar NGINX al máximo y tal vez lo motive a explorar el manual para más golosinas.