Хабрахабр

Пробы и ошибки при выборе HTTP Reverse Proxy

Всем привет!

О своем опыте рассказывает undying, DevOps Team Lead в Ostrovok.ru. Сегодня мы хотим рассказать о том, как команда сервиса бронирования отелей Ostrovok.ru решала проблему роста микросервиса, задачей которого является обмен информацией с нашими поставщиками.

Сначала микросервис был мал и выполнял следующие функции:

  • принять запрос от локального сервиса;
  • сделать запрос партнеру;
  • нормализовать ответ;
  • вернуть результат вопрошающему сервису.

Однако время шло, сервис рос вместе с количеством партнеров и запросов к ним.

Разные поставщики выдвигают свои правила работы: кто-то ограничивает максимальное количество соединений, кто-то ограничивает клиентов белыми списками. По мере роста сервиса стали всплывать разного рода проблемы.

В итоге нам предстояло решить следующие задачи:

  • желательно иметь несколько фиксированных внешних IP адресов, чтобы можно было предоставлять их партнерам для добавления их в белые списки,
  • иметь единый пул соединений ко всем поставщикам, чтобы при масштабировании нашего микросервиса количество соединений оставалось минимальным,
  • терминировать SSL и держать keepalive в одном месте, тем самым снижая нагрузку для самих партнеров.

Долго думать не стали и сразу задались вопросом, что выбрать: Nginx или Haproxy.
Сперва маятник качнулся в сторону Nginx, так как большую часть проблем, связанных с HTTP/HTTPS, я решал с его помощью и всегда оставался доволен результатом.

Из map брался адрес и делался proxy_pass на этот адрес. Схема была простой: делался запрос в наш новый Proxy Server на Nginx с доменом вида <partner_tag>.domain.local, в Nginx был map, где <partner_tag> соответствовал адресу партнера.

Вот пример map, которым мы парсим домен и выбираем апстрим из списка:

### берем префикс из имени домена: <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;
}

А вот как выглядит “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; }
}

Все классно, все работает. Можно на этом закончить статью, если бы не один нюанс.

0 без keepalive и закрывается сразу после завершения ответа. При использовании proxy_pass прямиком на нужный адрес запрос идет, как правило, по протоколу HTTP/1. 1, без апстрима ничего не изменится (proxy_http_version). Даже если мы выставим proxy_http_version 1.

Первая мысль – завести всех поставщиков в апстримы, где в качестве server будет нужный нам адрес поставщика, а в map держать "tag" "upstream_name". Что делать?

Добавляем еще один map для парсинга схемы:

### берем префикс из имени домена: <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;
}

И создаем 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 cache, если записи в нем истекли, и заменять адреса для апстримов в том случае, если они изменились. У Haproxy есть возможность указать dns resolvers и настроить 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} <<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})

Теперь все, что нам нужно, это добавить новый хост в nginx_map, запустить генератор и получить готовый haproxy конфиг.

Данная статья относится скорее к вводной и была посвящена проблеме выбора решения и его интеграции в текущее окружение. На сегодня, пожалуй, все.

В следующей статье я расскажу подробнее о том, какие подводные камни нам встречались при использовании Haproxy, какие метрики оказалось полезно мониторить и что точно стоит оптимизировать в системе, чтобы выжать максимум производительности из серверов.

Всем спасибо за внимание, до встречи!

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть