Prueba y error al elegir HTTP Reverse Proxy

Hola a todos!

Hoy queremos hablar sobre cómo el equipo del servicio de reservas de hoteles Ostrovok.ru resolvió el problema del crecimiento del microservicio, cuya tarea es intercambiar información con nuestros proveedores. Sobre su experiencia le dice que no muere , Líder del equipo DevOps en Ostrovok.ru.


Al principio, el microservicio era pequeño y realizaba las siguientes funciones:

  • aceptar una solicitud de un servicio local;
  • hacer una solicitud a un socio;
  • normalizar la respuesta;
  • devolver el resultado al servicio de consulta.

Sin embargo, con el tiempo, el servicio creció junto con el número de socios y las solicitudes que les hicieron.

A medida que el servicio creció, comenzaron a surgir varios tipos de problemas. Diferentes proveedores presentan sus propias reglas: alguien limita el número máximo de conexiones, alguien restringe a los clientes a listas blancas.

Como resultado, tuvimos que resolver los siguientes problemas:

  • es deseable tener varias direcciones IP externas fijas para que pueda proporcionarlas a los socios para agregarlas a las listas blancas,
  • tener un único grupo de conexiones con todos los proveedores para que al escalar nuestro microservicio el número de conexiones sea mínimo,
  • finalice SSL y manténgase keepalive en un solo lugar, reduciendo así la carga en los propios socios.

No pensaron durante mucho tiempo e inmediatamente se preguntaron qué elegir: Nginx o Haproxy.
Al principio, el péndulo giró hacia Nginx, ya que resolví la mayoría de los problemas asociados con HTTP / HTTPS con su ayuda y siempre estuve satisfecho con el resultado.

El esquema era simple: se realizó una solicitud a nuestro nuevo Servidor Proxy en Nginx con un dominio de la forma <partner_tag>.domain.local , en Nginx había un map , donde <partner_tag> correspondía a la dirección del socio. Se tomó una dirección del map y se realizó proxy_pass a esta dirección.

Aquí hay un map ejemplo con el que analizamos el dominio y seleccionamos el flujo ascendente de la lista:

 ###     : <tag>.domain.local map $http_host $upstream_prefix { default 0; "~^([^\.]+)\." $1; } ###      map $upstream_prefix $upstream_address { include snippet.d/upstreams_map; default http://127.0.0.1:8080; } ###   upstream_host    upstream_address map $upstream_address $upstream_host { default 0; "~^https?://([^:]+)" $1; } 

Y así es como se ve " snippet.d/upstreams_map ":
 “one” “http://one.domain.net”; “two” “https://two.domain.org”; 

Aquí tenemos el server{} :

 server { listen 80; location / { proxy_http_version 1.1; proxy_pass $upstream_address$request_uri; proxy_set_header Host $upstream_host; proxy_set_header X-Forwarded-For ""; proxy_set_header X-Forwarded-Port ""; proxy_set_header X-Forwarded-Proto ""; } } # service for error handling and logging server { listen 127.0.0.1:8080; location / { return 400; } location /ngx_status/ { stub_status; } } 

Todo es genial, todo funciona. Es posible finalizar este artículo, si no es por un matiz.

Cuando se usa proxy_pass, la solicitud va directamente a la dirección deseada, como regla, usando el protocolo HTTP / 1.0 sin keepalive y se cierra inmediatamente después de que se completa la respuesta. Incluso si proxy_http_version 1.1 , nada cambiará sin upstream ( proxy_http_version ).

Que hacer El primer pensamiento es lograr que todos los proveedores ingresen en el flujo ascendente, donde el servidor será la dirección del proveedor que necesitamos, y en el map mantenga "tag" "upstream_name" .

Agregue otro map para analizar el esquema:

 ###     : <tag>.domain.local map $http_host $upstream_prefix { default 0; "~^([^\.]+)\." $1; } ###      map $upstream_prefix $upstream_address { include snippet.d/upstreams_map; default http://127.0.0.1:8080; } ###   upstream_host    upstream_address map $upstream_address $upstream_host { default 0; "~^https?://([^:]+)" $1; } ###   ,       https,    ,    -  http map $upstream_address $upstream_scheme { default "http://"; "~(https?://)" $1; } 

Y cree upstreams con nombres de etiquetas:
  upstream one { keepalive 64; server one.domain.com; } upstream two { keepalive 64; server two.domain.net; } 

El servidor en sí está ligeramente modificado para tener en cuenta el esquema y usar el nombre de la cadena en lugar de la dirección:

 server { listen 80; location / { proxy_http_version 1.1; proxy_pass $upstream_scheme$upstream_prefix$request_uri; proxy_set_header Host $upstream_host; proxy_set_header X-Forwarded-For ""; proxy_set_header X-Forwarded-Port ""; proxy_set_header X-Forwarded-Proto ""; } } # service for error handling and logging server { listen 127.0.0.1:8080; location / { return 400; } location /ngx_status/ { stub_status; } } 

Genial La solución funciona, agregue la directiva keepalive a cada proxy_http_version 1.1 ascendente, establezca proxy_http_version 1.1 , ahora tenemos un grupo de conexiones y todo funciona como debería.

Esta vez, definitivamente puedes terminar el artículo e ir a tomar té. O no?

Después de todo, mientras bebemos té, uno de los proveedores puede cambiar la dirección IP o el grupo de direcciones bajo el mismo dominio (hola, Amazon), por lo que uno de los proveedores puede caerse a la altura de nuestra fiesta de té.

Bueno, que hacer? Nginx tiene un matiz interesante: durante la recarga, puede sobriar los servidores dentro del upstream a nuevas direcciones y ponerles tráfico. En general, también una solución. Agregue cron reload nginx cada 5 minutos y continúe tomando té.

Pero aún así me pareció una decisión más o menos, así que comencé a mirar con recelo hacia Haproxy.

Haproxy tiene la capacidad de especificar dns resolvers y configurar dns cache . Por lo tanto, Haproxy actualizará el dns cache si las entradas han expirado, y reemplazará las direcciones de flujo ascendente si han cambiado.

Genial Ahora depende de la configuración.

Aquí hay un breve ejemplo de configuración para Haproxy:

 frontend http bind *:80 http-request del-header X-Forwarded-For http-request del-header X-Forwarded-Port http-request del-header X-Forwarded-Proto capture request header Host len 32 capture request header Referer len 128 capture request header User-Agent len 128 acl host_present hdr(host) -m len gt 0 use_backend %[req.hdr(host),lower,field(1,'.')] if host_present default_backend default resolvers dns hold valid 1s timeout retry 100ms nameserver dns1 1.1.1.1:53 backend one http-request set-header Host one.domain.com server one--one.domain.com one.domain.com:80 resolvers dns check backend two http-request set-header Host two.domain.net server two--two.domain.net two.domain.net:443 resolvers dns check ssl verify none check-sni two.domain.net sni str(two.domain.net) 

Todo parece funcionar como debería esta vez. Lo único que no me gusta de Haproxy es la complejidad de la descripción de la configuración. Necesita construir una gran cantidad de texto para agregar uno que funcione en sentido ascendente. Pero la pereza es el motor del progreso: si no quieres escribir lo mismo, escribe un generador.

Ya tenía un mapa de Nginx con el formato "tag" "upstream" , así que decidí tomarlo como base, analizar y generar un back-end de haproxy basado en estos valores.

 #! /usr/bin/env bash haproxy_backend_map_file=./root/etc/haproxy/snippet.d/name_domain_map haproxy_backends_file=./root/etc/haproxy/99_backends.cfg nginx_map_file=./nginx_map while getopts 'n:b:m:' OPT;do case ${OPT} in n) nginx_map_file=${OPTARG} ;; b) haproxy_backends_file=${OPTARG} ;; m) haproxy_backend_map_file=${OPTARG} ;; *) echo 'Usage: ${0} -n [nginx_map_file] -b [haproxy_backends_file] -m [haproxy_backend_map_file]' exit esac done function write_backend(){ local tag=$1 local domain=$2 local port=$3 local server_options="resolvers dns check" [ -n "${4}" ] && local ssl_options="ssl verify none check-sni ${domain} sni str(${domain})" [ -n "${4}" ] && server_options+=" ${ssl_options}" cat >> ${haproxy_backends_file} <<EOF backend ${tag} http-request set-header Host ${domain} server ${tag}--${domain} ${domain}:${port} ${server_options} EOF } :> ${haproxy_backends_file} :> ${haproxy_backend_map_file} while read tag addr;do tag=${tag//\"/} [ -z "${tag:0}" ] && continue [ "${tag:0:1}" == "#" ] && continue IFS=":" read scheme domain port <<<${addr//;} unset IFS domain=${domain//\/} case ${scheme} in http) port=${port:-80} write_backend ${tag} ${domain} ${port} ;; https) port=${port:-443} write_backend ${tag} ${domain} ${port} 1 esac done < <(sort -V ${nginx_map_file}) 

Ahora todo lo que necesitamos es agregar un nuevo host en nginx_map, iniciar el generador y obtener la configuración de haproxy lista.

Eso es probablemente todo por hoy. Este artículo se refiere más al introductorio y se dedicó al problema de elegir una solución y su integración en el entorno actual.

En el próximo artículo, hablaré con más detalle acerca de los escollos que encontramos al usar Haproxy, qué métricas resultaron útiles para monitorear y qué se debe optimizar exactamente en el sistema para obtener el máximo rendimiento de los servidores.

Gracias a todos por su atención, nos vemos!

Source: https://habr.com/ru/post/436992/


All Articles