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"

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)


  1. offline15
    18.10.2015 11:07

    Выглядит многообещающим! Всегда пользовался nginx для проксирования, сейчас вот задумался…


  1. erlyvideo
    18.10.2015 11:38

    а напомните: он чем-то архитектурно внутри от nginx отличается?


    1. vaniaPooh
      18.10.2015 12:15

      Он делает активные проверки, которые в Nginx поддерживаются только в платной версии.


      1. linjan
        18.10.2015 12:19

        Можете, пожалуйста, привести пример платных проверок?



        1. navion
          18.10.2015 13:55

          Почти все фичи ngx_http_upstream_module относятся к платной версии. Ещё в бесплатной версии нет L4 прокси.


          1. VBart
            18.10.2015 17:15

            Это неправда, stream proxy доступен бесплатно: nginx.org/ru/docs/stream/ngx_stream_core_module.html


            1. navion
              18.10.2015 17:46

              Спасибо за информацию, а есть планы по его включению в основную сборку?


              1. VBart
                18.10.2015 18:38

                В основную сборку? Если вы про наши пакеты на nginx.org/en/linux_packages.html, то он там включен.


    1. VBart
      18.10.2015 17:26

      В отличие от haproxy, nginx имеет мастер-процесс и непривелигированные рабочие процессы, что позволяет ему перезагружать конфигурацию и обновляться на новую версию без потери соединений.


      1. blind_oracle
        18.10.2015 18:21
        +1

        HAProxy тоже вполне себе перезагружается без потери соединений, просто несколько иначе:
        1. Создаётся новый процесс и слушающий коннекты сокет переходит к нему
        2. Новые соединения уходят в новый процесс
        3. Старый процесс живёт до тех пор, пока все старые соединения не завершатся


        1. VBart
          18.10.2015 18:35

          Зачем же тогда люди так извращаются: engineeringblog.yelp.com/2015/04/true-zero-downtime-haproxy-reloads.html?


          1. 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.


            1. VBart
              18.10.2015 22:43

              Правильно, речь о том, что в haproxy отсутствует возможность перезагрузки без потери соединений.


              1. 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-1


                1. VBart
                  19.10.2015 12:25

                  Так подскажите, как настроить haproxy, чтобы он не потерял новые входящие соединения под высокой нагрузкой?

                  Ещё раз повторюсь, nginx не теряет входящие соединения, вне зависимости от того, включен был SO_REUSEPORT или выключен.


      1. blind_oracle
        18.10.2015 20:50

        К слову, чтобы обновить nginx на новую версию его всё равно придётся перезапускать — мастер процесс то форкает детей из себя. И если его не перезапустить — версия будет старой.


        1. VBart
          18.10.2015 22:48
          +1

          В какой-то момент просто работает два мастера и два поколения рабочих процессов. После того, как вы убедились, что всё впорядке, вы можете плавно завершить старые рабочие процессы. Процедура подробно описана в документации, nginx при этом не потеряет ни одного соединения. В отличие от haproxy, он не использует SO_REUSEPORT для этой цели.


      1. erlyvideo
        19.10.2015 00:42

        без потери входящих или без разрыва открытых?


        1. VBart
          19.10.2015 12:30
          +1

          Ни одного входящего соединения не будет потеряно. Открытые будут продолжать обслуживаться до тех пор, пока их нельзя будет безболезненно закрыть.


  1. lavelas
    18.10.2015 19:51

    Уважаемые господа!!!
    Полелитесь опытом, нужно настроить балансировку СП Tomcat, который работает по проприетарному протоколу поверх tcp.

    http mode не подходит, с tcp mode работает, но Проблема в сохранении реального ip клиента, что очень важно. Как быть?


    1. linjan
      18.10.2015 20:03

      Возможно, вам подойдет haproxy transparent mode


      1. lavelas
        18.10.2015 20:31

        Спасибо, почитаю.

        Все жду возможности использовать PROXY protocol для Tomcat, а не только Nginx


  1. stavinsky
    18.10.2015 21:29

    Очень и очень объемно! Спасибо.
    По поводу lua прикольно. Nginx прикалывается по поводу JS, а haproxy внедряет lua.
    Честно говоря js ближе.
    Вопрос только как скажется на производительности добавление lua?


    1. xandr0s
      19.10.2015 14:42

      Nginx по Lua уже наприкалывался, наскучило )