Деплой и инфра
Балансировка L4 vs L7: что каждый уровень видит и чего не видит
Команда пилит монолит: /api/* должен идти в новый сервис, всё остальное — в старый. У них уже крутится AWS NLB «ради производительности», поэтому они добавляют правило маршрутизации по пути — и обнаруживают, что такого правила нет. NLB не видит путь. Он форвардит сырой TCP; URL появляется только после парсинга HTTP, а L4-балансировщик HTTP никогда не парсит. Миграция встаёт, пока кто-то не поставит ALB. Урок стоил дня: выбранный уровень решает, какая маршрутизация вообще возможна.
L4 форвардит байты; L7 читает запросы
Балансировщик уровня 4 работает на транспортном уровне. Он видит TCP/UDP-соединение — source IP, destination IP, порты — и форвардит пакеты на бэкенд, ни разу не декодируя содержимое. По сути это быстрый, соединение-знающий NAT. Так как payload он не парсит, он протокол-агностичен (балансирует любой TCP- или UDP-сервис, а не только HTTP) и крайне дёшев на пакет — AWS NLB держит миллионы запросов в секунду с накладными в микросекунды на пакет и выдаёт статические IP.
Балансировщик уровня 7 работает на прикладном уровне. Он терминирует клиентское соединение, читает весь HTTP-запрос — метод, хост, путь, заголовки, cookie — и затем открывает собственное соединение к выбранному бэкенду. Это «терминировать и переоткрыть» — весь источник его мощи и его цены. Раз балансировщик держит распарсенный запрос, он может маршрутизировать на /api/* против /, по заголовку Host:, по cookie или по кастомному заголовку для канареек. Цена: он обязан буферизировать и парсить каждый запрос, а если в игре TLS — расшифровывать и шифровать заново. Реальный CPU на запрос, и он говорит только на тех протоколах, которые понимает (HTTP/1.1, HTTP/2, gRPC).
Решение не в том, «что лучше», а в том, «что мне нужно видеть». Если нужно лишь раскидать сырой TCP по одинаковым бэкендам — L4 быстрее и проще. В момент, когда маршрутизация зависит от чего-то внутри запроса, нужен L7 — и никакой тюнинг L4 не наколдует URL, который тот не декодировал.
| Возможность | L4 (транспортный) | L7 (прикладной) |
|---|---|---|
| Маршрутизирует по | IP + порт | пути, хосту, заголовку, cookie |
| Читает payload? | Нет — форвардит байты | Да — парсит HTTP |
| Протоколы | любой TCP/UDP | HTTP/1.1, HTTP/2, gRPC |
| Терминация TLS | passthrough (обычно) | терминирует + может шифровать заново |
| HTTP-aware retry | Нет | Да (идемпотентные запросы) |
| Цена на запрос | очень низкая | парсинг + буфер + крипто |
| Пример в AWS | NLB | ALB |
Где терминируется TLS — меняет всё
Терминация TLS — самая чистая призма на раздел L4/L7. L7-балансировщик завершает зашифрованную сессию на краю: он держит сертификат, расшифровывает запрос, читает открытый HTTP, принимает решение о маршрутизации и форвардит — либо открытым текстом внутри доверенной сети, либо заново зашифрованным к бэкенду. Именно эта расшифровка и позволяет маршрутизировать по пути или заголовку. Она же централизует управление сертификатами и снимает крипто с серверов приложения.
L4-балансировщик обычно не может терминировать TLS, потому что терминировать — значит расшифровывать, а расшифровывать — значит стать HTTP-aware. Поэтому он делает TLS passthrough: зашифрованные байты идут прямо на бэкенд, который держит сертификат и терминирует. Это держит балансировщик слепым по дизайну — отлично для end-to-end шифрования и для не-HTTP протоколов, бесполезно, если хотелось маршрутизации по пути. Это и есть конкретная причина, почему «просто возьми быстрый» бьёт в обратку: быстрый буквально не может прочитать то, по чему ты хочешь маршрутизировать.
Почему это работает
«L4 всегда быстрее» — полуправда и обман. На пакет — да, нет парсинга, нет крипто. Но L7-балансировщик, терминирующий TLS один раз на краю, может быть дешевле в сумме, чем прокидывание шифрования на каждый бэкенд, потому что каждый сервер приложения больше не платит за handshake и расшифровку. Правильный вопрос — где работу лучше сделать, а не какой уровень выигрывает микробенчмарк.
Алгоритмы, health-чеки и вынос мёртвых бэкендов
Оба уровня всё равно должны выбрать бэкенд. Round-robin раскидывает соединения поровну и нормален, когда запросы стоят примерно одинаково. Least-connections отправляет следующий запрос на бэкенд с наименьшим числом активных соединений — куда лучше, когда длительности запросов сильно разнятся, потому что один медленный эндпоинт не будет получать всё больше нагрузки. Хеширование (по IP клиента или consistent hashing) прикрепляет данный ключ к данному бэкенду — так L4-балансировщик имитирует stickiness без cookie.
Health-чеки держат пул честным. Активные чеки опрашивают каждый бэкенд по интервалу (скажем, раз в несколько секунд) и выносят его из ротации после N подряд неудач; пассивные помечают бэкенд мёртвым после того, как он вернул ошибки реальному трафику. Open-source nginx опирается на пассивные чеки; ALB/NLB и nginx Plus делают активный опрос. То, что не доходит до интуиции: упавший бэкенд выносят только после того, как health-чек это заметит — поэтому слишком медленный интервал даёт окно, когда живой трафик продолжает бить в мёртвую коробку и 502 утекают пользователям.
Нужно маршрутизировать /api/* в новый сервис, / в старый, терминировать TLS на краю и катить канарейку по заголовку запроса. Выбери балансировщик.
Sticky-сессии и connection draining: два способа сломать деплой
Sticky-сессии (session affinity) прикрепляют клиента к одному бэкенду — через L4 IP-хеш или L7-cookie — чтобы in-memory состояние сессии оставалось на месте. Это работает и тихо вредит: нагрузка перестаёт распределяться ровно (один «кит»-клиент молотит одну коробку), и этот бэкенд нельзя чисто слить, потому что он владеет сессиями, которых нет ни у кого. Рефлекс сеньора — делать бэкенды stateless (сессии в Redis или JWT), чтобы любой бэкенд обслуживал любой запрос; тянуться к stickiness только когда состояние реально нельзя вынести.
Убийца деплоя — отсутствие connection draining. Когда ты убираешь бэкенд во время выката, на нём всё ещё обслуживаются in-flight запросы. Если балансировщик вырывает его мгновенно, эти запросы умирают — пользователи видят сброс на середине оформления заказа. Connection draining (AWS зовёт это deregistration delay) велит балансировщику перестать слать на бэкенд новые соединения, но дать существующим доработать в течение grace-окна. В AWS дефолт — 300 секунд, настраиваемо от 0 до 3600. Поставишь короче самого долгого запроса — всё равно режешь живые запросы; забудешь включить — каждый деплой это маленький простой. Сочетай с health-чеками: слить, подождать, потом гасить.
Твоему L4 (NLB) балансировщику нужно слать /admin/* в отдельный пул бэкендов. Как настроить правило по пути?
Во время rolling-деплоя пользователи жалуются на сброс соединения на середине запроса каждый раз, когда убирают старый инстанс. Какой фикс?
Расставь шаги, чтобы мягко убрать бэкенд во время деплоя:
- 1 Пометить бэкенд на дерегистрацию, чтобы балансировщик перестал слать ему новые соединения
- 2 Дать connection draining отработать: существующие in-flight запросы продолжают обслуживаться
- 3 Выждать окно слива (например, AWS deregistration delay, дефолт 300с)
- 4 Как соединения доработают или окно истечёт — погасить инстанс
- 5 Health-чеки подтверждают, что оставшийся пул обслуживает трафик
- 01Коллега хочет маршрутизацию по пути, но команда уже крутит L4 (NLB) балансировщик «потому что быстрее». Объясни точно, почему маршрутизация по пути там невозможна и что изменится при переходе на L7.
- 02Пройди по тому, что делает connection draining во время деплоя и что ломается без него. Включи дефолт AWS.
Выбранный уровень решает, по чему балансировщик может маршрутизировать, потому что решает, что он может видеть. L4-балансировщик работает на транспортном уровне — IP, порт, сырой TCP/UDP — и форвардит байты без парсинга, что делает его быстрым, протокол-агностичным и способным на TLS passthrough, но структурно слепым к URL, заголовкам и cookie. L7-балансировщик терминирует соединение, парсит HTTP и маршрутизирует по пути, хосту, заголовку или cookie — это и даёт маршрутизацию по пути, терминацию TLS на краю, канарейки по заголовку и HTTP-aware retry — оплаченные CPU на запрос. Поверх выбора уровня лежат операционные детали, решающие надёжность: round-robin, least-connections и хеширование балансируют по-разному при неровной нагрузке; активные и пассивные health-чеки выносят мёртвые бэкенды, но лишь так быстро, как их интервал; sticky-сессии держат состояние на месте ценой ровного распределения и чистого слива; а connection draining (AWS deregistration delay, дефолт 300 секунд) даёт in-flight запросам доработать до удаления бэкенда. Два продакшен-провала падают прямо отсюда: потребовать маршрутизацию по пути на L4-балансировщике, что невозможно, и убрать бэкенд без draining, что убивает живые запросы на каждом деплое.