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"

    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

      0
      Выглядит многообещающим! Всегда пользовался nginx для проксирования, сейчас вот задумался…
        0
        а напомните: он чем-то архитектурно внутри от nginx отличается?
          0
          Он делает активные проверки, которые в Nginx поддерживаются только в платной версии.
          0
          В отличие от haproxy, nginx имеет мастер-процесс и непривелигированные рабочие процессы, что позволяет ему перезагружать конфигурацию и обновляться на новую версию без потери соединений.
            +1
            HAProxy тоже вполне себе перезагружается без потери соединений, просто несколько иначе:
            1. Создаётся новый процесс и слушающий коннекты сокет переходит к нему
            2. Новые соединения уходят в новый процесс
            3. Старый процесс живёт до тех пор, пока все старые соединения не завершатся
              0
              Зачем же тогда люди так извращаются: engineeringblog.yelp.com/2015/04/true-zero-downtime-haproxy-reloads.html?
                0
                Там ведь даже написано, зачем.
                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.
                  0
                  Правильно, речь о том, что в haproxy отсутствует возможность перезагрузки без потери соединений.
                    0
                    У 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
                      0
                      Так подскажите, как настроить haproxy, чтобы он не потерял новые входящие соединения под высокой нагрузкой?

                      Ещё раз повторюсь, nginx не теряет входящие соединения, вне зависимости от того, включен был SO_REUSEPORT или выключен.
              0
              К слову, чтобы обновить nginx на новую версию его всё равно придётся перезапускать — мастер процесс то форкает детей из себя. И если его не перезапустить — версия будет старой.
                +1
                В какой-то момент просто работает два мастера и два поколения рабочих процессов. После того, как вы убедились, что всё впорядке, вы можете плавно завершить старые рабочие процессы. Процедура подробно описана в документации, nginx при этом не потеряет ни одного соединения. В отличие от haproxy, он не использует SO_REUSEPORT для этой цели.
                0
                без потери входящих или без разрыва открытых?
                  +1
                  Ни одного входящего соединения не будет потеряно. Открытые будут продолжать обслуживаться до тех пор, пока их нельзя будет безболезненно закрыть.
              0
              Уважаемые господа!!!
              Полелитесь опытом, нужно настроить балансировку СП Tomcat, который работает по проприетарному протоколу поверх tcp.

              http mode не подходит, с tcp mode работает, но Проблема в сохранении реального ip клиента, что очень важно. Как быть?
                0
                Возможно, вам подойдет haproxy transparent mode
                  0
                  Спасибо, почитаю.

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

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое