HAProxy исполнилось 1.6

HAProxy Logo
Приветствую категорически.
Спешу сообщить радостную новость о том, что после полутора лет (а не четырёх) на свет появилась стабильная версия 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

В заключение хочу напомнить, что все новые версии имеют полную обратную совместимость со старыми конфигурационными файлами, и обновление на новую версию не вызовет никакой головной боли. А представленные выше возможности — лишь небольшая часть той работы, что была проделана за эти полтора года разработчиками.

Спасибо, что уделили этому обзору своё внимание. Буду рад ответить на вопросы в комментариях и ЛС.

© Habrahabr.ru