Пробы и ошибки при выборе HTTP Reverse Proxy
Всем привет!
Сегодня мы хотим рассказать о том, как команда сервиса бронирования отелей Ostrovok.ru решала проблему роста микросервиса, задачей которого является обмен информацией с нашими поставщиками. О своем опыте рассказывает undying, DevOps Team Lead в Ostrovok.ru.
Сначала микросервис был мал и выполнял следующие функции:
— принять запрос от локального сервиса;
— сделать запрос партнеру;
— нормализовать ответ;
— вернуть результат вопрошающему сервису.
Однако время шло, сервис рос вместе с количеством партнеров и запросов к ним.
По мере роста сервиса стали всплывать разного рода проблемы. Разные поставщики выдвигают свои правила работы: кто-то ограничивает максимальное количество соединений, кто-то ограничивает клиентов белыми списками.
В итоге нам предстояло решить следующие задачи:
- желательно иметь несколько фиксированных внешних IP адресов, чтобы можно было предоставлять их партнерам для добавления их в белые списки,
- иметь единый пул соединений ко всем поставщикам, чтобы при масштабировании нашего микросервиса количество соединений оставалось минимальным,
- терминировать SSL и держать
keepalive
в одном месте, тем самым снижая нагрузку для самих партнеров.
Долго думать не стали и сразу задались вопросом, что выбрать: Nginx или Haproxy.
Сперва маятник качнулся в сторону Nginx, так как большую часть проблем, связанных с HTTP/HTTPS, я решал с его помощью и всегда оставался доволен результатом.
Схема была простой: делался запрос в наш новый Proxy Server на Nginx с доменом вида
, в Nginx был map
, где
соответствовал адресу партнера. Из map
брался адрес и делался proxy_pass
на этот адрес.
Вот пример map
, которым мы парсим домен и выбираем апстрим из списка:
### берем префикс из имени домена: .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;
}
А вот как выглядит »snippet.d/upstreams_map
»:
"one” "http://one.domain.net”;
"two” "https://two.domain.org”;
Тут у нас сам 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;
}
}
Все классно, все работает. Можно на этом закончить статью, если бы не один нюанс.
При использовании proxy_pass прямиком на нужный адрес запрос идет, как правило, по протоколу HTTP/1.0 без keepalive
и закрывается сразу после завершения ответа. Даже если мы выставим proxy_http_version 1.1
, без апстрима ничего не изменится (proxy_http_version).
Что делать? Первая мысль — завести всех поставщиков в апстримы, где в качестве server будет нужный нам адрес поставщика, а в map
держать "tag" "upstream_name"
.
Добавляем еще один map
для парсинга схемы:
### берем префикс из имени домена: .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;
}
И создаем upstreams
с именами тегов:
upstream one {
keepalive 64;
server one.domain.com;
}
upstream two {
keepalive 64;
server two.domain.net;
}
Сам сервер немного видоизменяем, чтобы учитывать схему и вместо адреса использовать имя апстрима:
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;
}
}
Отлично. Решение работает, добавляем в каждый апстрим директиву keepalive
, выставляем proxy_http_version 1.1
, — теперь у нас есть пул соединений, и все работает как надо.
На этот раз точно можно заканчивать статью и идти пить чай. Или нет?
Ведь пока мы пьем чай, у кого-то из поставщиков может под тем же доменом измениться IP адрес или группа адресов (привет, Амазон), тем самым один из поставщиков может отвалиться в самый разгар нашего чаепития.
Ну что же, как быть? Есть у Nginx интересный нюанс: во время reload он может отрезолвить сервера внутри upstream
в новые адреса и пустить трафик на них. В целом, тоже решение. Закидываем в cron reload nginx
раз в 5 минут и продолжаем пить чай.
Но все же это показалось мне так себе решением, поэтому я стал косо посматривать в сторону Haproxy.
У Haproxy есть возможность указать dns resolvers
и настроить dns cache
. Тем самым Haproxy будет сам обновлять dns cache
, если записи в нем истекли, и заменять адреса для апстримов в том случае, если они изменились.
Отлично! Теперь осталось дело за настройками.
Вот краткий пример конфигурации для 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)
Кажется, что на этот раз все работает как нужно. Вот только чем мне не нравится Haproxy, так это сложностью описания конфигураций. Нужно настрочить довольно много текста, чтобы добавить один работающий апстрим. Но лень — двигатель прогресса: если не хочется писать одно и то же, напиши генератор.
У меня уже был map из Nginx с форматом "tag" "upstream"
, поэтому я решил взять его за основу, парсить и генерировать на основании этих значений haproxy backend.
#! /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} < ${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})
Теперь все, что нам нужно, это добавить новый хост в nginx_map, запустить генератор и получить готовый haproxy конфиг.
На сегодня, пожалуй, все. Данная статья относится скорее к вводной и была посвящена проблеме выбора решения и его интеграции в текущее окружение.
В следующей статье я расскажу подробнее о том, какие подводные камни нам встречались при использовании Haproxy, какие метрики оказалось полезно мониторить и что точно стоит оптимизировать в системе, чтобы выжать максимум производительности из серверов.
Всем спасибо за внимание, до встречи!