HAProxy исполнилось 1.6
Приветствую категорически.
Спешу сообщить радостную новость о том, что после полутора лет (а не четырёх) на свет появилась стабильная версия 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"
Видимо, первоапрельская шутка разработчиков о том, что они решили переписать весь 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
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-теги на каждый фронтенд, бекенд и процесс. Если параметр не указан, то будет использовано слово 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)
В версии 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-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 заголовков имеется возможность обработки тела запроса.
Включается в секции 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
Использовались в 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
Поддержка 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 стал проще
backend b_myapp_ssl mode http server s1 10.0.0.3:4444 check ssl sni req.hdr(Host)
По-умолчанию, соединение, устанавливаемое между HAProxy и сервером бекенда принадлежит сессии, которая его инициировала. Минус данного подхода в том, между запросами данное соединение простаивает. В большинстве случаев повторное использование данных соединений другими сесссиями повысит производительность работы с бекендом.
Опция
http-reuse
в 4х разных режимах предоставляет возможность использовать эти простаивающие соединения.
Эта ошибка в браузерах возникала из-за таймаута pre-connect соединения, призванного ускорить серфинг по интернету.
В 1.5 лечилось строкой errorfile 408 /dev/null
в секции defaults
.
В 1.6 следует использовать option http-ignore-probes
В заключение хочу напомнить, что все новые версии имеют полную обратную совместимость со старыми конфигурационными файлами, и обновление на новую версию не вызовет никакой головной боли. А представленные выше возможности — лишь небольшая часть той работы, что была проделана за эти полтора года разработчиками.
Спасибо, что уделили этому обзору своё внимание. Буду рад ответить на вопросы в комментариях и ЛС.