Современный периметр часто требует от одного балансировщика одновременно обслуживать классический веб‑трафик (HTTP/1.1 и HTTP/2) и gRPC‑стримы, при этом внедрять трассировку, лёгкую бизнес‑логику и санитаризацию заголовков. Всё это реально в одном HAProxy с правильной конфигурацией TLS+ALPN, маршрутизации по признакам gRPC и Lua‑фильтрами. Ниже — практический разбор, на который можно опираться в проде.
Версии, предпосылки и базовые принципы
Для стабильной работы HTTP/2 на фронтенде и бэкенде, а также gRPC и Lua, ориентируйтесь на HAProxy 2.6+ (желательно 2.8 LTS или новее). В сборке должен быть включён Lua (опция USE_LUA), а для TLS с ALPN — OpenSSL 1.1.1+ или 3.x. В типовой установке пакетного HAProxy это уже есть.
Если балансировщик ещё не развёрнут, удобнее запускать его на изолированном сервере — подойдёт наш VDS, где можно гибко настроить ядро, sysctl и обновления ядра/библиотек без влияния на соседей.
Ключевые идеи конфигурации:
- Один публичный порт 443, завершение TLS на HAProxy, ALPN:
h2,http/1.1. - Маршрутизация gRPC по заголовкам:
content-type: application/grpcиte: trailers. - Отдельные бэкенды: для обычного веба (HTTP/1.1/HTTP/2) и для gRPC (стримы, длинные таймауты, TCP keepalive).
- Lua‑фильтры для X‑Request‑ID/санитаризации, но без попыток модификации бинарного тела gRPC.
- Расширенное логирование: фиксируем ALPN, тип трафика и корреляционный ID.
TLS и ALPN: единый 443 для всего
ALPN решает, какой протокол на клиентской стороне будет выбран: HTTP/2 или HTTP/1.1. gRPC строится поверх HTTP/2, поэтому клиент с gRPC почти всегда договаривается на h2. Нам важно корректно настроить bind с alpn, кривыми ECDHE и шифрами. Не забудьте о валидном сертификате: оформить и обновлять удобно через наши SSL-сертификаты.
global
log stdout format raw local0
master-worker
nbthread 4
stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
lua-load /etc/haproxy/lua/reqid.lua
tune.bufsize 16384
tune.maxaccept 64
tune.h2.max-concurrent-streams 100
defaults
log global
mode http
option httplog
option http-keep-alive
option http-use-htx
timeout connect 5s
timeout client 1m
timeout server 5m
timeout http-request 10s
http-reuse safe
# Унифицированный формат логов с ALPN и корреляцией
log-format "%ci:%cp [%t] %ft %b %ST alpn=%[ssl_fc_alpn] ct=%[req.hdr(content-type)] id=%[var(txn.req_id)] %r"
frontend fe_https
bind :443 ssl crt /etc/haproxy/certs alpn h2,http/1.1 ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256 ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384 ssl-min-ver TLSv1.2
# Генерация корреляционного ID и сквозная передача
http-request lua.add_req_id
http-response set-header X-Request-ID %[var(txn.req_id)]
# Диагностика gRPC
capture request header content-type len 64
capture request header te len 16
# ACL, определяющая gRPC-трафик
acl is_grpc req.hdr(content-type) -m sub application/grpc
acl is_trailers req.hdr(te) -i trailers
use_backend be_grpc if is_grpc is_trailers
default_backend be_web
Маршрутизация gRPC: почему content-type и te
gRPC использует HTTP/2 и идиоматично помечает запрос заголовком content-type: application/grpc, а также te: trailers. Этого набора обычно достаточно, чтобы с высокой точностью отличать gRPC от любого другого трафика HTTP/2. Проверка только ALPN не гарантирует gRPC, так как обычные сайты тоже работают по h2.
Такой фронтенд корректно «разруливает» ALPN на клиентской стороне и передаёт запросы дальше логикой ACL. Для статических сайтов и REST API пойдём в be_web, а gRPC — в be_grpc.

Бэкенды: HTTP/1.1, HTTP/2 и gRPC одновременно
Если ваши веб‑приложения «за» балансировщиком умеют HTTP/2, имеет смысл говорить с ними по h2 и на серверной стороне (меньше overhead на коннекты и заголовки). Для gRPC это обязательно, так как протокол поверх HTTP/2. Если бэкенд не умеет TLS внутри сегмента — используйте proto h2 без ssl (h2c).
backend be_web
balance roundrobin
# Общие сайты и API; HAProxy -> backend по HTTP/2 поверх TLS
server app1 10.0.0.11:8443 ssl alpn h2,http/1.1 check proto h2
server app2 10.0.0.12:8443 ssl alpn h2,http/1.1 check proto h2
backend be_grpc
mode http
balance roundrobin
# Для долгих стримов gRPC таймауты шире и включаем keepalive на TCP
timeout server 10m
option clitcpka
option srvtcpka
# gRPC бэкенды по HTTP/2; если внутри без TLS, уберите "ssl" и оставьте proto h2
server svc1 10.0.1.21:50051 ssl alpn h2 check proto h2
server svc2 10.0.1.22:50051 ssl alpn h2 check proto h2
# Пример h2c (без TLS):
# server svc3 10.0.1.23:50051 proto h2 check
Ключевой момент — proto h2 на серверной линии. Без него HAProxy будет говорить с бэкендом по HTTP/1.1, что сломает gRPC и лишит преимуществ HTTP/2 для веб‑сервисов. http-reuse safe в defaults разрешает безопасное переиспользование соединений к бэкенду и экономит establish‑overhead.
Health‑checks для HTTP/2 и gRPC
gRPC‑health‑check по протобуфу не так просто собрать силами HAProxy, поэтому есть три рабочих подхода:
- HTTP‑endpoint на сервисе (рекомендуется, когда возможно):
backend be_web
option httpchk GET /healthz
http-check expect status 200
server app1 10.0.0.11:8443 ssl alpn h2 check proto h2
- TCP‑check для gRPC‑портов, если нет HTTP‑health:
backend be_grpc
option tcp-check
tcp-check connect
server svc1 10.0.1.21:50051 ssl alpn h2 check proto h2
- Отдельный агент/проба, публикующая статус (agent‑check). Это гибко, но сложнее в поддержке.
Если сервисы поддерживают HTTP/2 и простой GET /healthz, используйте вариант №1: он точнее. Для чистого gRPC без HTTP‑эндпоинтов чаще всего хватает TCP‑check, а детальные проверки выполняются внешним мониторингом.
Lua‑фильтры: трассировка, санитаризация, A/B
Lua в HAProxy удобна для неблокирующей логики на границе: присвоение X‑Request‑ID, нормализация заголовков, лёгкая маршрутизация на основе заголовков/куков, защита от мусорных значений. Важно помнить: тело gRPC — бинарное и фреймится HTTP/2, поэтому его лучше не пытаться переписывать на ходу.
-- /etc/haproxy/lua/reqid.lua
core.register_action("add_req_id", { "http-req" }, function(txn)
local id = string.format("%08x-%06x", core.now(), math.random(0, 0xffffff))
txn:set_var("txn.req_id", id)
-- Перепишем X-Request-ID, если пришёл от клиента
txn.http:req_del_hdr("X-Request-ID")
txn.http:req_add_header("X-Request-ID", id)
end)
В конфигурации мы уже подключили Lua через lua-load и добавили http-request lua.add_req_id. В ответе используем http-response set-header, чтобы сквозной ID дошёл до клиента. Дальше тот же ID подтягивайте в логи приложения — так получится end‑to‑end трассировка.
Практические идеи для Lua
- Санитаризация User‑Agent, Forwarded/X‑Forwarded‑For, ограничение длины отдельных заголовков.
- Маркировка запросов по куке/заголовку в var(txn.*) и маршрутизация в canary‑бэкенд.
- Пополнение метрик через лог‑формат или stats socket (например, счётчик по ключам).
Не модифицируйте тело gRPC в Lua: это небезопасно и приводит к поломке фрейминга HTTP/2. Работайте только с заголовками и переменными транзакции.
Тюнинг HTTP/2: окна, буферы, коннекты
По умолчанию HAProxy аккуратно настроен, но при больших стримах, нагрузке или высоких задержках полезно пересмотреть параметры. Несколько ориентиров:
tune.h2.max-concurrent-streams: ограничьте количество одновременных потоков на соединение, если бэкенды тяжёлые (например, 100–200).tune.bufsize: 16–32 КБ обычно достаточно; рост повышает расход RAM на сессию.http-reuse: режимsafe— хороший компромисс.alwaysвозможен для чистых API без авторизации на уровне заголовков, но используйте с осторожностью.timeout serverиoption *tcpka: для gRPC‑стримов выделяйте минуты, включайте TCP keepalive и следите за системными sysctl для TCP‑KA.
Логи и отладка ALPN/gRPC
Собственный log-format с ssl_fc_alpn и заголовком content-type — простой способ увидеть реальное распределение трафика и ошибки клиентов. Для оперативной диагностики:
- Проверка ALPN от клиента:
openssl s_client -alpn h2 -connect YOUR_HOST:443 - HTTP/2 нагрузка:
h2load -n 10000 -c 100 https://YOUR_HOST/ - gRPC вызов:
grpcurl -insecure -d '{}' YOUR_HOST:443 your.Service/Method - Статистика сокета:
echo "show info" | socat stdio /run/haproxy/admin.sock
Если в логах видите HTTP 502/503 на gRPC при долгих стримах — первым делом увеличьте timeout server и включите option clitcpka/option srvtcpka. Также проверьте, что бэкенд реально говорит по proto h2.

Частые проблемы и решения
- 502 с gRPC при долгих вызовах: короткие таймауты на бэкенде, нет TCP keepalive. Лечится увеличением таймаутов и включением keepalive.
- gRPC‑клиент сообщает об Unavailable или RST_STREAM: обычно закрытие канала при переключении сервера во время health‑check/деплоя. Проверьте
balance, стратегию drain и время снятия из пулы. - Обычные сайты падают на HTTP/2 из‑за специфики бэкенда: временно отключите
proto h2для проблемного сервера (оставьте HTTP/1.1), либо обновите серверное ПО. - Маршрутизация «промахивается» мимо gRPC: проверьте заголовки
content-typeиte. Некоторые клиенты забываютte: trailers— добавьте fallback‑ACL только поcontent-type, если уверены. - gRPC‑Web — это не тот же протокол; ему нужен транскодер (например, Envoy). Подробности см. в материале gRPC‑Web и Envoy: как подружить браузер с gRPC.
Дополнительно: если нужен rate‑limit на уровне L7, используйте stick‑tables HAProxy. Мы разбирали подход в статье ограничение запросов через stick‑tables.
Распределение нагрузок и границы ответственности
Надёжный вариант — держать один фронтенд на 443 для всех клиентов, а разделение типов трафика выполнять по заголовкам/ALPN. Под капотом — два и больше бэкендов с изолированными таймаутами, проверками, стратегиями балансировки. Это позволяет независимо масштабировать веб и gRPC‑микросервисы, не мешая профилям нагрузки друг друга.
Чек‑лист внедрения
- Обновите HAProxy до версии с поддержкой HTTP/2 на фронте и бэке, Lua включён.
- Включите TLS+ALPN на фронтенде:
h2,http/1.1, современные шифры и минимальную версию TLS. - Сделайте ACL для gRPC по
content-type/teи разведите бэкенды. - На бэкендах gRPC и HTTP/2 поставьте
proto h2, задайте таймауты под стримы и включите TCP keepalive. - Добавьте Lua‑фильтры для X‑Request‑ID и санитаризации заголовков.
- Включите расширенный
log-format, проверьте метрики и мониторинг сокета. - Проведите нагрузочный прогон h2/gRPC и калибруйте
tune.h2.*,timeout *.
С такой конфигурацией один балансировщик уверенно тянет HTTP/2‑сайты и gRPC‑сервисы, остаётся прозрачным для трассировки и не накладывает лишних ограничений на разработку. Главное — дисциплина в таймаутах и аккуратность с Lua: делайте то, что безопасно на уровне заголовков, и не трогайте бинарное содержимое gRPC.


