Hola de nuevo
La última vez, hablamos sobre elegir una herramienta en
Ostrovok.ru para resolver el problema de enviar un gran número de solicitudes a servicios externos, sin poner a nadie al mismo tiempo. El artículo terminó con una selección de
Haproxy . Hoy compartiré los matices que tuve que enfrentar al usar esta solución.

Configuración de haproxy
La primera dificultad fue que la opción
maxconn
es diferente según el contexto:
Por costumbre, solo ajusté la primera opción (
performance tuning
). Esto es lo que dice la documentación sobre esta opción:
Establece el número máximo de conexiones simultáneas por proceso en <número>. Se
es equivalente al argumento de la línea de comandos "-n". Los representantes dejarán de aceptar
conexiones cuando se alcanza este límite.
Parecería que lo que se necesita. Sin embargo, cuando me di cuenta de que las nuevas conexiones al proxy no funcionaban de inmediato, comencé a leer la documentación con más cuidado, y allí ya encontré el segundo parámetro (
bind options
):
Limita los sockets a este número de conexiones concurrentes. Extraño
las conexiones permanecerán en la cartera de pedidos del sistema hasta que se conecte
liberado Si no se especifica, el límite será el mismo que el maxconn de la interfaz.
Entonces,
frontends maxconn
, luego busque
frontends maxconn
:
Fijar el número máximo de conexiones concurrentes en una interfaz
...
De forma predeterminada, este valor se establece en 2000.
Genial, lo que necesitas. Agregar a la configuración:
global daemon maxconn 524288 ... defaults mode http maxconn 524288
La siguiente mordaza fue que Haproxy es de un solo subproceso. Estoy muy acostumbrado al modelo en Nginx, por lo que este matiz siempre me ha deprimido. Pero no se desespere:
Willy Tarreau (desarrollador de Haproxy ) entendió lo que estaba haciendo, por lo que agregó la opción:
nbproc
.
Sin embargo, directamente en la documentación dice:
UTILIZANDO MÚLTIPLES PROCESOS
ES MÁS DIFÍCIL DE DEPURAR Y ESTÁ REALMENTE DESCURRIDO.
Esta opción realmente puede causar dolor de cabeza en caso de que necesite:
- limite el número de solicitudes / conexiones a los servidores (ya que no tendrá un solo proceso con un contador, sino muchos procesos, y cada uno tiene su propio contador);
- Recopile estadísticas del socket de administración de Haproxy
- activar / desactivar backends a través del zócalo de control;
- ... tal vez algo más. ¯ \ _ (ツ) _ / ¯
Sin embargo, los dioses nos dieron procesadores multinúcleo, por lo que me gustaría usarlos al máximo. En mi caso, había cuatro núcleos en dos núcleos físicos. Para Haproxy, seleccioné el primer núcleo, y se veía así:
nbproc 4 cpu-map 1 0 cpu-map 2 1 cpu-map 3 2 cpu-map 4 3
Usando
cpu-map, vinculamos los procesos de Haproxy a un núcleo específico. El programador del sistema operativo ya no necesita pensar dónde planificar Haproxy, manteniendo así el
content switch
frío y el caché de la CPU caliente.
Hay muchos tampones, pero no en nuestro caso.
- tune.bufsize : en nuestro caso no fue necesario ejecutarlo, pero si tiene errores con el código
400 (Bad Request)
, este es probablemente su caso. - tune.http.cookielen : si distribuye cookies grandes a los usuarios, entonces, para evitar daños durante la transmisión a través de la red, también puede tener sentido aumentar este búfer.
- tune.http.maxhdr es otra posible fuente de 400 códigos de respuesta si tiene demasiados encabezados.
Ahora considere las cosas de nivel inferior
tune.rcvbuf.client /
tune.rcvbuf.server ,
tune.sndbuf.client /
tune.sndbuf.server : la documentación dice lo siguiente:
Normalmente, nunca debe establecerse, y el tamaño predeterminado (0) permite que el kernel ajuste automáticamente este valor dependiendo de la cantidad de memoria disponible.
Pero para mí, lo obvio es mejor que lo implícito, así que forcé los valores de estas opciones para estar seguros del mañana.
Y otro parámetro que no está relacionado con los buffers, pero que es bastante importante es
tune.maxaccept .
Establece el número máximo de conexiones consecutivas que un proceso puede aceptar en un
fila antes de cambiar a otro trabajo. En modo de proceso único, números más altos
dar un mejor rendimiento a altas velocidades de conexión. Sin embargo en multiproceso
modos, mantener un poco de equidad entre los procesos generalmente es mejor
aumentar el rendimiento
En nuestro caso, se generan muchas solicitudes de proxy, por lo que elevé este valor para aceptar más solicitudes a la vez. Sin embargo, como dice la documentación, vale la pena probar que en modo de subprocesos múltiples la carga se distribuye de la manera más uniforme posible entre los procesos.
Todos los parámetros juntos:
tune.bufsize 16384 tune.http.cookielen 63 tune.http.maxhdr 101 tune.maxaccept 256 tune.rcvbuf.client 33554432 tune.rcvbuf.server 33554432 tune.sndbuf.client 33554432 tune.sndbuf.server 33554432
Lo que nunca sucede son los tiempos de espera. ¿Qué haríamos sin ellos?
- timeout connect : tiempo para establecer una conexión con el backend. Si la conexión con el backend no es muy buena, entonces es mejor deshabilitarla antes de que se agote el tiempo hasta que la red vuelva a la normalidad.
- cliente de tiempo de espera: tiempo de espera para la transmisión de los primeros bytes de datos. Ayuda a desconectar a quienes hacen solicitudes "en reserva".
Kulstory sobre el cliente HTTP en GoGo tiene un cliente HTTP normal que tiene la capacidad de mantener un grupo de conexiones a los servidores. Entonces sucedió una historia interesante, en la que participaron el tiempo de espera y el grupo de conexiones descritos anteriormente en el cliente HTTP. Una vez que un desarrollador se quejó de que periódicamente tiene 408 errores de un proxy. Examinamos el código del cliente y vimos la siguiente lógica allí:
- Estamos tratando de tomar una conexión gratuita establecida del grupo;
- si no funciona, comience la instalación de una nueva conexión en goroutine;
- revise la piscina nuevamente;
- si hay libre en la piscina, la tomamos y colocamos la nueva en la piscina, si no, use la nueva.
¿Ya entendiste qué es la sal?
Si el cliente ha establecido una nueva conexión, pero no la ha utilizado, luego de cinco segundos el servidor la cierra y el caso ha terminado. El cliente detecta esto solo cuando ya obtiene la conexión del grupo e intenta usarla. Vale la pena tener esto en cuenta.
- servidor de tiempo de espera : el tiempo máximo para esperar una respuesta del servidor.
- timeout client-fin / timeout server-fin : aquí nos protegemos de las conexiones semicerradas para no acumularlas en la tabla del sistema operativo.
- timeout http-request es uno de los tiempos de espera más adecuados. Le permite cortar clientes lentos que no pueden realizar una solicitud HTTP en el tiempo asignado para ellos.
- tiempo de espera http-keep-alive : específicamente en nuestro caso, si una conexión de
keep-alive
bloquea sin solicitudes durante más de 50 segundos, lo más probable es que algo haya salido mal y la conexión se pueda cerrar, liberando así memoria para algo nuevo luz
Todos los tiempos de espera juntos:
defaults mode http maxconn 524288 timeout connect 5s timeout client 10s timeout server 120s timeout client-fin 1s timeout server-fin 1s timeout http-request 10s timeout http-keep-alive 50s
Registro ¿Por qué es tan difícil?
Como escribí anteriormente, la mayoría de las veces en mis decisiones uso Nginx, por lo tanto, me siento mimado por su sintaxis y la simplicidad de modificar los formatos de registro. Me gustó especialmente la característica asesina: formatee los registros en forma de json y luego analícelos con cualquier biblioteca estándar.
¿Qué tenemos en Haproxy? Existe tal oportunidad, solo usted puede escribir exclusivamente en syslog, y la sintaxis de configuración está un poco más ajustada.
Te daré una configuración de ejemplo con comentarios:
El dolor particular es causado por tales momentos:
- nombres de variables cortos, y especialmente sus combinaciones como% HU o% fp
- el formato no se puede dividir en varias líneas, por lo que debe escribir el footfoot en una línea. difícil de agregar / eliminar elementos nuevos / innecesarios
- para que algunas variables funcionen, deben declararse explícitamente a través del encabezado de solicitud de captura
Como resultado, para obtener algo interesante, tienes que tener tal paño:
log-format '{"status":"%ST","bytes_read":"%B","bytes_uploaded":"%U","hostname":"%H","method":"%HM","request_uri":"%HU","handshake_time":"%Th","request_idle_time":"%Ti","request_time":"%TR","response_time":"%Tr","timestamp":"%Ts","client_ip":"%ci","client_port":"%cp","frontend_port":"%fp","http_request":"%r","ssl_ciphers":"%sslc","ssl_version":"%sslv","date_time":"%t","http_host":"%[capture.req.hdr(0)]","http_referer":"%[capture.req.hdr(1)]","http_user_agent":"%[capture.req.hdr(2)]"}'
Bueno, parecería, pequeñas cosas, pero agradables
Describí el formato del registro anterior, pero no es tan simple. Para depositar algunos elementos en él, como:
- http_host
- http_referer,
- http_user_agent,
primero necesita capturar estos datos de la solicitud (
captura ) y ponerlos en una matriz de valores capturados.
Aquí hay un ejemplo:
capture request header Host len 32 capture request header Referer len 128 capture request header User-Agent len 128
Como resultado, ahora podemos acceder a los elementos que necesitamos de esta manera:
%[capture.req.hdr(N)]
, donde N es el número de secuencia de la definición del grupo de captura.
En el ejemplo anterior, el encabezado del Host estará en el número 0 y el User-Agent estará en el número 2.
Haproxy tiene una peculiaridad: resuelve las direcciones DNS de los backends al inicio y, si no puede resolver ninguna de las direcciones, cae la muerte de los valientes.
En nuestro caso, esto no es muy conveniente, ya que hay muchos backends, no los gestionamos, y es mejor obtener 503 de Haproxy que todo el servidor proxy se negará a iniciar debido a un solo proveedor. La siguiente opción nos ayuda con esto:
init-addr .
Una línea tomada directamente de la documentación nos permite ver todos los métodos disponibles para resolver una dirección y, en el caso de un archivo, simplemente posponer este asunto para más adelante y avanzar:
default-server init-addr last,libc,none
Y finalmente, mi favorito: selección de backend.
La sintaxis para la configuración de selección de back-end de Haproxy es familiar para todos:
use_backend <backend1_name> if <condition1> use_backend <backend2_name> if <condition2> default-backend <backend3>
Pero, en palabras, de alguna manera no es muy. Ya he descrito todos los backends de forma automatizada (ver el
artículo anterior ), también sería posible generar
use_backend
aquí, el mal negocio no es complicado, pero no quería hacerlo. Como resultado, se encontró otra forma:
capture request header Host len 32 capture request header Referer len 128 capture request header User-Agent len 128 # host_present Host acl host_present hdr(host) -m len gt 0 # , use_backend %[req.hdr(host),lower,field(1,'.')] if host_present # , default_backend default backend default mode http server no_server 127.0.0.1:65535
Por lo tanto, estandarizamos los nombres de backends y urls por los cuales puede acceder a ellos.
Bueno, ahora compilando de los ejemplos anteriores en un archivo:
Versión completa de configuración global daemon maxconn 524288 nbproc 4 cpu-map 1 0 cpu-map 2 1 cpu-map 3 2 cpu-map 4 3 tune.bufsize 16384 tune.comp.maxlevel 1 tune.http.cookielen 63 tune.http.maxhdr 101 tune.maxaccept 256 tune.rcvbuf.client 33554432 tune.rcvbuf.server 33554432 tune.sndbuf.client 33554432 tune.sndbuf.server 33554432 stats socket /run/haproxy.sock mode 600 level admin log /dev/stdout local0 debug defaults mode http maxconn 524288 timeout connect 5s timeout client 10s timeout server 120s timeout client-fin 1s timeout server-fin 1s timeout http-request 10s timeout http-keep-alive 50s default-server init-addr last,libc,none log 127.0.0.1:2514 len 8192 local1 notice emerg log 127.0.0.1:2514 len 8192 local7 info log-format '{"status":"%ST","bytes_read":"%B","bytes_uploaded":"%U","hostname":"%H","method":"%HM","request_uri":"%HU","handshake_time":"%Th","request_idle_time":"%Ti","request_time":"%TR","response_time":"%Tr","timestamp":"%Ts","client_ip":"%ci","client_port":"%cp","frontend_port":"%fp","http_request":"%r","ssl_ciphers":"%sslc","ssl_version":"%sslv","date_time":"%t","http_host":"%[capture.req.hdr(0)]","http_referer":"%[capture.req.hdr(1)]","http_user_agent":"%[capture.req.hdr(2)]"}' 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 backend default mode http server no_server 127.0.0.1:65535 resolvers dns hold valid 1s timeout retry 100ms nameserver dns1 127.0.0.1:53
Gracias a quienes leyeron hasta el final. Sin embargo, esto no es todo.
La próxima vez veremos cosas de nivel inferior relacionadas con la optimización del sistema en sí, en el que trabaja Haproxy, para que él y su sistema operativo se sientan cómodos juntos, y haya suficiente hierro para todos.
Hasta pronto!