Приветствую категорически.
Спешу сообщить радостную новость о том, что после полутора лет (а не четырёх) на свет появилась стабильная версия HAProxy 1.6 с интереснейшим функционалом.
Напомню, что это сверхбыстрое решение, гарантирующее отказоустойчивость и обеспечивающее балансировку и проксирование TCP и HTTP запросов.
Маршрутизация и фильтрация запросов по многим критериям
SSL терминирование, с SNI/NPN/ALPN и OCSP stapling в комплекте
Манипуляции с HTTP заголовками и поддержка ACL
Мониторинг серверов бекенда HTTP и TCP проверками
Простота интеграции с VRRP (keepalived)
Сжатие (gzip,deflate)
Поддержка syslog, гибкий формат логов
Практически неограниченное количество серверов, ферм, сервисов
Безопасность (ни одного взлома за 13 лет)
Поддержка IPv6 и UNIX сокетов
… и множество других возможностей
Любезно прошу о всех найденных неточностях и ошибках писать в ЛС — оперативно исправлю.
В этой статье я поделюсь, чем примечателен выход версии под номером 1.6, на какие нововведения стоит обратить внимание и кратко опишу как эти новшества попробовать. Примеры в статье присутствуют для ознакомления, а их применение не освобождает от необходимости обратиться к странице постоянно обновляемой документации.
Наконец-то можно использовать кавычки в аргументах
Это действительно приятная новость. Теперь не обязательно при вставке в файл конфигурации, например, заголовков, экранировать пробелы обратным слешем.
reqirep "^Host: www.(.*)" "Host: foobar\1"
option httpchk GET / "HTTP/1.1\r\nHost: www.domain.com\r\nConnection: close"
Lua
Видимо, первоапрельская шутка разработчиков о том, что они решили переписать весь HAProxy на LUA положительно сказалась на функционале. И это, возможно, стало важнейшим изменением в 1.6, как когда-то SSL в 1.5.
Для примера взглянем на реализацию «зеркального» веб-сервера. Он вернет наши заголовки в теле ответа без изменений.
global lua-load ./webmirror.lua frontend fe_habrahabr bind :81 name frontend_name http-request lua mirror default_backend be_habrahabr backend be_habrahabr server main_nginx 127.0.0.1:82
--webmirror.lua
function mirror(txn) local buffer = "" local response = "" local mydate = txn.sc:http_date(txn.f:date()) buffer = buffer .. "You sent the following headers/r/n" buffer = buffer .. "===============================================/r/n" buffer = buffer .. txn.req:dup() buffer = buffer .. "===============================================/r/n" response = response .. "HTTP/1.0 200 OK/r/n" response = response .. "Server: haproxy-lua/mirror/r/n" response = response .. "Content-Type: text/html/r/n" response = response .. "Date: " .. mydate .. "/r/n" response = response .. "Content-Length: " .. buffer:len() .. "/r/n" response = response .. "Connection: close/r/n" response = response .. "/r/n" response = response .. buffer txn.res:send(response) txn:close() end
$ curl -v 127.0.0.1:82 HTTP/1.0 200 OK Server: haproxy-lua/mirror Content-Type: text/html Date: Fri, 12 Mar 2015 13:06:44 GMT Content-Length: 208 Connection: keep-alive You sent the following headers =============================================== GET / HTTP/1.1 User-Agent: curl/7.41.0 Host: 127.0.0.1:82 Accept: */* ===============================================
Или, например, tcp-сервер:
global lua-load hello_world.lua listen proxy bind 127.0.0.1:10001 tcp-request content use-service lua.hello_world
— hello_world.lua
core.register_service("hello_world", "tcp", function(applet) applet:send("hello world\n") end)
Передача заголовков между секциями (контекстами)
Ранее каждый контекст был изолирован. Иными словами, нельзя было заголовки запроса использовать для ответа. Но теперь можно.
defaults mode http frontend fe_habr bind :9001 declare capture request len 32 # id=0 to store Host header declare capture request len 64 # id=1 to store User-Agent header http-request capture req.hdr(Host) id 0 http-request capture req.hdr(User-Agent) id 1 default_backend be_habr backend be_habr http-response set-header Your-Host %[capture.req.hdr(0)] http-response set-header Your-User-Agent %[capture.req.hdr(1)] server nginx1 10.0.0.3:4444 check
Мультипроцессинг, peers и stick-tables
peer — другой haproxy инстанс. Например, на другой ВМ, в другом ДЦ.
stick-table — плоская база данных для хранения информации, например, о количестве запросов в секунду с одного IP-адреса, кол-ве одновременных сессий, частоте ошибок, идентификаторе сессии по cookie и т.п.
В 1.5 существовал (в 1.6 остался) такой параметр как peers. Предназначен для синхронизации stick-tables между балансировщиками. И, к сожалению, при включении мультипроцессинга в haproxy (параметр nbproc) данный функционал начинал работать некорректно из-за собственной таблицы на каждый процесс в памяти.
Решение пришло в виде параметра
bind-process
, пример наглядно покажет его использование:peers article peer itchy 127.0.0.1:1023 global pidfile /tmp/haproxy.pid nbproc 3 defaults mode http frontend f_scalessl bind-process 1,2 bind :9001 ssl crt /home/bassmann/haproxy/ssl/server.pem default_backend bk_lo backend bk_lo bind-process 1,2 server f_myapp unix@/tmp/f_myapp send-proxy-v2 frontend f_myapp bind-process 3 bind unix@/tmp/f_myapp accept-proxy default_backend b_myapp backend b_myapp bind-process 3 stick-table type ip size 10k peers article stick on src server s1 10.0.0.3:4444 check
Логи: syslog-теги и новые переменные
Отныне для удобства фильтрации логов можно применять различные syslog-теги на каждый фронтенд, бекенд и процесс. Если параметр не указан, то будет использовано слово haproxy.
frontend fe_habr_ssl log-tag SSL [...] frontend fe_habr log-tag CLEAR [...]
Новые переменные, которые можно использовать в параметре log-format:
%HM: HTTP method (ex: POST) %HP: HTTP request URI without query string (path) %HQ: HTTP request URI query string (ex: ?bar=baz) %HU: HTTP request URI (ex: /foo?bar=baz) %HV: HTTP version (ex: HTTP/1.0)
DNS-имена серверов
В версии 1.5 и ранее, если в качестве бекенда было указано DNS-имя, то HAProxy получал IP-адрес при старте и использовал при этом glibc (/etc/resolv.conf)
В 1.6 HAProxy асинхронно проверяет актуальность соответствия имени IP-адресу на лету и использует указанные явно DNS-сервера. Это избавляет от необходимости перезапускать балансировщик в случае, если сменился IP-адрес сервера в бекенде (что часто случается в окружениях Docker или Amazon Web Service).
Пример конфигурации для Docker:
resolvers docker nameserver dnsmasq 127.0.0.1:53 defaults mode http log global option httplog frontend fe_habr bind :80 default_backend be_habr backend be_habr server s1 nginx1:80 check resolvers docker resolve-prefer ipv4
Теперь, если мы перезапустим контейнер с nginx командой «docker restart nginx1» то увидим доказательство работы этого функционала в логах:
(...) haproxy[15]: b_myapp/nginx1 changed its IP from 172.16.0.4 to 172.16.0.6 by docker/dnsmasq.
Правила обработки HTTP запросов
Появились новые правила обработки HTTP-запросов.
http-request: capture, set-method, set-uri, set-map, set-var, track-scX, sc-in-gpc0, sc-inc-gpt0, silent-drop
http-response: capture, set-map, set-var, sc-inc-gpc0, sc-set-gpt0, silent-drop, redirect
Борцам с DDoS стоит обратить внимание на интересный параметр
silent-drop
. Он может заменить собой reqtarpit/reqitarpit
. Эффект заключается в том, что установленное клиентом соединение (ESTABLISHED) после применения
silent-drop
на HAProxy исчезает из списка соединений на балансировщике, освобождая ресурсы. Таким образом, можно отбивать атаки гораздо большей мощности, не тратя на это драгоценные ресурсы балансировщика. Но стоит помнить, что все файрволлы, прокси, балансировщики, через которых прошло данное соединение будут продолжать держать это соединение и могут стать узким местом («бутылочным горлышком») в защите. Переменные
Ранее использовались HTTP заголовки для хранения временных данных в HAProxy. Яркий тому пример — ограничение количества запросов в секунду в 1.5.
Теперь есть переменные.
Записываем User-agent в нижнем регистре:
http-request set-var(req.my_var) req.fhdr(user-agent),lower
Пример с контекстами, переписанный с использованием переменных
global # variables memory consumption, in bytes tune.vars.global-max-size 1048576 tune.vars.reqres-max-size 512 tune.vars.sess-max-size 2048 tune.vars.txn-max-size 256 defaults mode http frontend f_myapp bind :9001 http-request set-var(txn.host) req.hdr(Host) http-request set-var(txn.ua) req.hdr(User-Agent) default_backend b_myapp backend b_myapp http-response set-header Your-Host %[var(txn.host)] http-response set-header Your-User-Agent %[var(txn.ua)] server s1 10.0.0.3:4444 check
Почта
HAProxy научился слать письма. Например о том, что перестал отвечать бекенд.
Пример ниже, наверное, охватывает все возможности этого нововведения. Поддержки авторизации нет.
mailers mymailers mailer smtp1 192.168.0.1:587 mailer smtp2 192.168.0.2:587 backend be_habr mode tcp balance roundrobin email-alert mailers mymailers email-alert from haproxy@habrahabr.ru email-alert to admin@habrahabr.ru server srv1 192.168.0.30:80 server srv2 192.168.0.31:80
Обработка тела HTTP запроса
Теперь помимо обработки HTTP заголовков имеется возможность обработки тела запроса.
Включается в секции frontend или backend параметром
option http-buffer-request
В версии 1.5 можно было бороться с атакой типа slowloris, при которой заголовки запроса с атакущего передаются максимально медленно, на грани таймаута соединения,
Но никто не мешал максимально медленно передавать тело POST запроса. Версия 1.6 позволяет лишить злоумышленника и этой возможности.
Кстати, с применением опции
http-buffer-request
становится возможным использовать такие методы, как req.body, req.body_param, req.body_len, req.body_size и т.д.Вот пример, как заблокировать любое упоминание строки «SELECT *» в теле POST запросов:
defaults mode http frontend f_mywaf bind :9001 option http-buffer-request http-request deny if { req.body -m reg "SELECT \*" } default_backend b_myapp backend b_myapp server s1 10.0.0.3:4444 check
Преобразователи (converters)
Использовались в ACL и всячески упрощали конфигурацию. Например, маршрутизация запросов без них:
frontend ft_allapps [...] use_backend bk_app1 if { hdr(Host) -i app1.domain1.com app1.domain2.com } use_backend bk_app2 if { hdr(Host) -i app2.domain1.com app2.domain2.com } default_backend bk_default
С преобразователями:
frontend ft_allapps [...] use_backend %[req.hdr(host),lower,map(/etc/haproxy/domain2backend.map,bk_default)]
— domain2backend.map
#domainname backendname app1.domain1.com bk_app1 app1.domain2.com bk_app1 app2.domain1.com bk_app2 app2.domain2.com bk_app2
Удобно, не правда ли?
Так вот, в 1.6 их стало еще больше и я буду признателен за чей-нибудь пример в комментариях.
Определение устройства клиента
Совершенно неожиданно для меня HAProxy получил возможность работать с DeviceAtlas и 51Degrees для определения типа устройства и передачи бекенду результата.
Пример конфигурации для DeviceAtlas:
global deviceatlas-json-file <path to json file> frontend www-only-ua bind *:8881 default_backend servers #Передача только заголовка User-agent http-request set-header X-DeviceAtlas-Data %[req.fhdr(User-Agent),da-csv-conv(primaryHardwareType,osName,osVersion,browserName,browserVersion)] deviceatlas-json-file <path> frontend www-all-headers bind *:8882 default_backend servers #Передача всех заголовков для идентификации http-request set-header X-DeviceAtlas-Data %[da-csv-fetch(primaryHardwareType,osName,osVersion,browserName,browserVersion)]
Для 51Degrees:
global 51degrees-data-file '51D_REPO_PATH'/data/51Degrees-LiteV3.2.dat 51degrees-property-name-list IsTablet DeviceType IsMobile 51degrees-property-separator , 51degrees-cache-size 10000 frontend www-only-ua bind *:8082 default_backend servers #Передача только заголовка User-agent http-request set-header X-51D-DeviceTypeMobileTablet %[req.fhdr(User-Agent),51d.single(DeviceType,IsMobile,IsTablet)] frontend www-all-headers bind *:8081 default_backend servers # Передача всех заголовков для идентификации http-request set-header X-51D-DeviceTypeMobileTablet %[51d.all(DeviceType,IsMobile,IsTablet)] http-request set-header X-51D-Tablet %[51d.all(IsTablet)] # Опционально, укажет уверенность 51Degrees в результате http-request set-header X-51D-Stats %[51d.all(Method,Difference,Rank)]
Внимание! Поддержка не включена по-умолчанию. Для работы с ней необходимо:
Для DeviceAtlas:
Загрузить исходный код API с сайта DeviceAtlas
Скомпилировать HAProxy cо следующими параметрами:
$ make TARGET=<target> USE_PCRE=1 USE_DEVICEATLAS=1 DEVICEATLAS_SRC=<path to the API root folder>
Для 51Degrees:
$ git clone https://github.com/51Degrees/Device-Detection
Выбрать метод работы:
* Pattern — равномерно использует память и процессор для работы
$ make TARGET=linux26 USE_51DEGREES=1 51DEGREES_SRC='51D_REPO_PATH'/src/pattern
* Trie — высокопроизводительный алгоритм, использующий значительно больше памяти, нежели Pattern
$ make TARGET=linux26 USE_51DEGREES=1 51DEGREES_SRC='51D_REPO_PATH'/src/trie
Сохранение состояний серверов бекенда
В 1.5 при после получения команды reload или restart HAProxy присваивал всем серверам состояние UP до выполнения первой проверки. Что неприемлемо, если дорога каждая секунда аптайма сервиса. В 1.6 есть возможность указать путь до файла, где будет хранится информация о бекендах на время перезагрузки.
global stats socket /tmp/socket server-state-file /tmp/server_state backend bk load-server-state-from-file global server s1 10.0.0.3:4444 check weight 11 server s2 10.0.0.4:4444 check weight 12
Перед перезапуском сохраняем состояние бекендов:
socat /tmp/socket - <<< "show servers state" > /tmp/server_state
Задача выполнена, при старте haproxy прочитает файл и моментально примет его к сведению.
Внешние проверки
В 1.5 можно проверять состояние серверов бекенда при помощи периодического подключения к указанному порту.
В 1.6 в этих целях можно дополнительно использовать сторонние скрипты:
global external-check backend b_myapp external-check path "/usr/bin:/bin" external-check command /bin/true server s1 10.0.0.3:4444 check
TLS/SSL
Поддержка ECC и RSA на одном IP-адресе
Есть мнение, что ECC так же хорошо защищает содержимое, как и RSA, но при меньшем размере ключа, что означает меньшее время обработки запроса на сервере. К сожалению, далеко не все клиенты поддерживают ECC, а иметь совместимость хочется со всеми.
Для реализации понадобятся: ECC и RSA сертификаты для домена, HAProxy версии 1.6, и следующая конфигурация:
frontend ssl-relay mode tcp bind 0.0.0.0:443 use_backend ssl-ecc if { req.ssl_ec_ext 1 } default_backend ssl-rsa backend ssl-ecc mode tcp server ecc unix@/var/run/haproxy_ssl_ecc.sock send-proxy-v2 backend ssl-rsa mode tcp server rsa unix@/var/run/haproxy_ssl_rsa.sock send-proxy-v2 listen all-ssl bind unix@/var/run/haproxy_ssl_ecc.sock accept-proxy ssl crt /usr/local/haproxy/ecc.www.foo.com.pem user nobody bind unix@/var/run/haproxy_ssl_rsa.sock accept-proxy ssl crt /usr/local/haproxy/www.foo.com.pem user nobody mode http server backend_1 192.168.1.1:8000 check
Есть результат бенчмарка на E5-2680v3 CPU и OpenSSL 1.0.2:
256bit ECDSA:
sign verify sign/s verify/s
0.0000s 0.0001s 24453.3 9866.9
2048bit RSA:
sign verify sign/s verify/s
0.000682s 0.000028s 1466.4 35225.1
Почти 15кратный прирост при подписывании ответа.
Подделка SSL сертификатов на лету
Что позволяет использовать HAProxy в предприятиях для анализа содержимого запросов.Поддержка Certificate Transparency (RFC6962)
При загрузке .pem файлов (цепочек сертификатов с ключем) HAProxy по этому же пути попытается найти файл с тем же названием и суффиксом .sctl. При его обнаружении включается поддержка TLS Certificate Transparency. Требует версии OpenSSL 1.0.2 и выше. На данный момент расширение Certificate Transparency требует Chrome для EV сертификатов, выданных в 2015. Поддержка SNI при подключении к бекендам с SSL
backend b_myapp_ssl mode http server s1 10.0.0.3:4444 check ssl sni req.hdr(Host)
HTTP-reuse
По-умолчанию, соединение, устанавливаемое между HAProxy и сервером бекенда принадлежит сессии, которая его инициировала. Минус данного подхода в том, между запросами данное соединение простаивает. В большинстве случаев повторное использование данных соединений другими сесссиями повысит производительность работы с бекендом.
Опция
http-reuse
в 4х разных режимах предоставляет возможность использовать эти простаивающие соединения.Ошибка 408
Эта ошибка в браузерах возникала из-за таймаута pre-connect соединения, призванного ускорить серфинг по интернету.
В 1.5 лечилось строкой
errorfile 408 /dev/null
в секции defaults
.В 1.6 следует использовать option
http-ignore-probes
В заключение хочу напомнить, что все новые версии имеют полную обратную совместимость со старыми конфигурационными файлами, и обновление на новую версию не вызовет никакой головной боли. А представленные выше возможности — лишь небольшая часть той работы, что была проделана за эти полтора года разработчиками.
Спасибо, что уделили этому обзору своё внимание. Буду рад ответить на вопросы в комментариях и ЛС.
Комментарии (25)
erlyvideo
18.10.2015 11:38а напомните: он чем-то архитектурно внутри от nginx отличается?
vaniaPooh
18.10.2015 12:15Он делает активные проверки, которые в Nginx поддерживаются только в платной версии.
linjan
18.10.2015 12:19Можете, пожалуйста, привести пример платных проверок?
navion
18.10.2015 13:55Почти все фичи ngx_http_upstream_module относятся к платной версии. Ещё в бесплатной версии нет L4 прокси.
VBart
18.10.2015 17:15Это неправда, stream proxy доступен бесплатно: nginx.org/ru/docs/stream/ngx_stream_core_module.html
navion
18.10.2015 17:46Спасибо за информацию, а есть планы по его включению в основную сборку?
VBart
18.10.2015 18:38В основную сборку? Если вы про наши пакеты на nginx.org/en/linux_packages.html, то он там включен.
VBart
18.10.2015 17:26В отличие от haproxy, nginx имеет мастер-процесс и непривелигированные рабочие процессы, что позволяет ему перезагружать конфигурацию и обновляться на новую версию без потери соединений.
blind_oracle
18.10.2015 18:21+1HAProxy тоже вполне себе перезагружается без потери соединений, просто несколько иначе:
1. Создаётся новый процесс и слушающий коннекты сокет переходит к нему
2. Новые соединения уходят в новый процесс
3. Старый процесс живёт до тех пор, пока все старые соединения не завершатсяVBart
18.10.2015 18:35Зачем же тогда люди так извращаются: engineeringblog.yelp.com/2015/04/true-zero-downtime-haproxy-reloads.html?
ivlad
18.10.2015 19:22Там ведь даже написано, зачем.
Instead, it supports fast reloads where a new HAProxy instance starts up, attempts to use SO_REUSEPORT to bind to the same ports that the old HAProxy is listening to and sends a signal to the old HAProxy instance to shut down. This technique is very close to zero downtime on modern Linux kernels, but there is a brief period of time during which both processes are bound to the port. During this critical time, it is possible for traffic to get dropped due to the way that the Linux kernel (mis)handles multiple accepting processes.
VBart
18.10.2015 22:43Правильно, речь о том, что в haproxy отсутствует возможность перезагрузки без потери соединений.
brkov
19.10.2015 02:28У haproxy очень развесистая и настраеваемая архитектура в месте взаимодействия процессов системы между собой.
И с помощью правильно подобранных значений параметров:process
,bind-process
иnbproc
( www.haproxy.org/download/1.6/doc/management.txt, www.haproxy.org/download/1.6/doc/configuration.txt) можно добиться того же поведения, что используется в логике nginx.
Использование же параметра сокета SO_REUSEPORT, о которой говорится в статье от yelp, с одной стороны позволяет получить уменьшение лейтенси ответа и улучшения среднеквадратического отклонения, а с другой по понятным причинам создаёт почву для фейлов запросов. Около ~0.01% по данным haproxy:
Typically observed failure rates are around
1 failure during a reload operation every 10000 new connections per second,
which means that a heavily loaded site running at 30000 new connections per
second may see about 3 failed connection upon every reload.
В официальном блоге nginx про это тоже есть статья, описывающая полезность SO_REUSEPORT: www.nginx.com/blog/socket-sharding-nginx-release-1-9-1VBart
19.10.2015 12:25Так подскажите, как настроить haproxy, чтобы он не потерял новые входящие соединения под высокой нагрузкой?
Ещё раз повторюсь, nginx не теряет входящие соединения, вне зависимости от того, включен был SO_REUSEPORT или выключен.
blind_oracle
18.10.2015 20:50К слову, чтобы обновить nginx на новую версию его всё равно придётся перезапускать — мастер процесс то форкает детей из себя. И если его не перезапустить — версия будет старой.
VBart
18.10.2015 22:48+1В какой-то момент просто работает два мастера и два поколения рабочих процессов. После того, как вы убедились, что всё впорядке, вы можете плавно завершить старые рабочие процессы. Процедура подробно описана в документации, nginx при этом не потеряет ни одного соединения. В отличие от haproxy, он не использует SO_REUSEPORT для этой цели.
lavelas
18.10.2015 19:51Уважаемые господа!!!
Полелитесь опытом, нужно настроить балансировку СП Tomcat, который работает по проприетарному протоколу поверх tcp.
http mode не подходит, с tcp mode работает, но Проблема в сохранении реального ip клиента, что очень важно. Как быть?linjan
18.10.2015 20:03Возможно, вам подойдет haproxy transparent mode
lavelas
18.10.2015 20:31Спасибо, почитаю.
Все жду возможности использовать PROXY protocol для Tomcat, а не только Nginx
stavinsky
18.10.2015 21:29Очень и очень объемно! Спасибо.
По поводу lua прикольно. Nginx прикалывается по поводу JS, а haproxy внедряет lua.
Честно говоря js ближе.
Вопрос только как скажется на производительности добавление lua?
offline15
Выглядит многообещающим! Всегда пользовался nginx для проксирования, сейчас вот задумался…