VK Cloud

Балансировка нагрузки и масштабирование долгоживущих соединений в Kubernetes

Обновлено 15 июня 2026 г.
20 мая 2020 г.
_blog_head_179.png

Когда в Kubernetes говорят о балансировке, обычно имеют в виду привычный сценарий: сервис получает трафик и распределяет его по репликам приложения. Для коротких HTTP-запросов эта схема работает хорошо и почти не требует вмешательства. Проблемы начинаются там, где соединение живёт долго: в gRPC, HTTP/2, WebSocket, AMQP и других протоколах, которые не открывают новое TCP-соединение на каждый запрос.

В 2026 году сама логика этой проблемы не изменилась, но инструменты вокруг неё стали заметно взрослее. У kube-proxy появился стабильный бэкенд на nftables, Cilium окончательно закрепился как рабочий путь к замене kube-proxy через eBPF, Istio перевёл Ambient Mode в зрелый режим, а Gateway API стал нормальной опорой для L7-маршрутизации, в том числе для gRPC. При этом базовое ограничение осталось прежним: Kubernetes по-прежнему не умеет равномерно распределять запросы внутри уже установленного долгоживущего соединения. Поэтому вопрос уже не в том, есть ли проблема, а в том, где именно её решать — на клиенте, в mesh, в Gateway или на сетевом уровне.

Как в Kubernetes вообще распределяется трафик

В основе здесь две абстракции: Deployment и Service. Deployment описывает, сколько экземпляров приложения должно работать, а Service даёт стабильную точку входа к этим подам. У самих подов IP-адреса меняются при пересоздании, скейле и переезде по нодам, поэтому клиенты ходят не напрямую в поды, а в сервис.

Service выглядит как виртуальный IP-адрес, за которым стоит набор готовых принимать трафик подов. Когда приложение обращается к сервису, запрос уходит не в отдельный процесс-балансировщик, а в сетевые правила на ноде. Kubernetes хранит список бэкендов и передаёт его компонентам сетевого слоя, которые уже решают, куда именно отправить соединение.

На схеме это всё выглядит так:

Раньше для этого использовался объект Endpoints, где весь список адресов сервиса лежал в одном ресурсе. На больших кластерах это быстро становилось узким местом: любое изменение приводило к пересылке крупных объектов и лишней нагрузке на control plane. Поэтому рабочим стандартом стали EndpointSlices — список эндпоинтов разбивается на части, и система обновляет только затронутые фрагменты. Для крупных сервисов это уже не оптимизация, а нормальный способ жить без лишнего шума в API и etcd.

Как работает балансировка в Service

Сам по себе Service ничего не балансирует. У него нет процесса, который слушает выделенный IP и решает, куда отправить запрос. Этот IP существует как виртуальная сущность в Kubernetes, а реальное перенаправление делает kube-proxy на каждой ноде.

В классическом варианте kube-proxy получает список сервисов и их эндпоинтов и строит правила сетевой подсистемы Linux. Дальше, когда пакет идёт к IP сервиса, система подменяет адрес назначения на IP одного из подов. Для клиента всё выглядит так, будто он подключился к одному стабильному адресу, хотя на самом деле соединение уже на узле было переписано на конкретный бэкенд.

Исторически самым массовым режимом kube-proxy оставался iptables. Он давно понятен, широко поддерживается и до сих пор используется по умолчанию во многих окружениях. Но у него есть заметный минус: чем больше сервисов и эндпоинтов в кластере, тем хуже он чувствует себя на больших объёмах правил и на частых обновлениях.

В 2025 году в Kubernetes 1.33 бэкенд kube-proxy на nftables стал стабильным. Для кластера это важное изменение: nftables работает с более современными структурами данных, лучше переносит большие таблицы правил и позволяет делать более точечные обновления. На небольших кластерах это не всегда драматично заметно, но на крупных инсталляциях с большим числом сервисов и постоянным движением подов разница уже вполне практическая. Поэтому в 2026 году nftables — это не эксперимент, а нормальный кандидат на роль основного бэкенда для новых кластеров, если позволяет окружение.

Есть и третий путь — вообще убрать kube-proxy из цепочки и отдать реализацию сервисов Cilium через eBPF. Этот вариант уже не выглядит экзотикой. Если кластер и так живёт на Cilium, переход к kube-proxy replacement даёт более короткий сетевой путь, меньше зависимостей от iptables-стека и больше пространства для тонкой оптимизации. Но это уже не просто «включить новый режим», а архитектурное решение, которое надо принимать осознанно.

Почему долгоживущие соединения ломают привычную схему

Для коротких HTTP-запросов всё устроено просто: клиент открывает TCP-соединение, запрос проходит через правила балансировки, выбирается бэкенд, соединение закрывается. На следующем запросе выбор повторяется, и трафик более-менее размазывается по репликам.

С долгоживущими соединениями эта схема перестаёт работать. HTTP keep-alive, HTTP/2, gRPC, WebSocket и AMQP стараются держать соединение открытым как можно дольше. Выбор бэкенда происходит только один раз — в момент установки TCP-соединения. После этого все запросы внутри этого соединения идут в тот же под. Kubernetes здесь уже ничего не перераспределяет, потому что для сетевого стека это не новые подключения, а продолжение уже существующего сеанса.

Отсюда возникает типичный эффект: Deployment масштабировали с трёх реплик до десяти, но старые клиенты по-прежнему держат соединения с первыми тремя подами. Семь новых подов формально готовы принимать трафик, но реальной нагрузки почти не получают. На графиках это выглядит как перекошенное распределение: часть реплик перегружена, часть простаивает, а автоскейлинг не даёт ожидаемого эффекта.

Это касается не только gRPC или HTTP/2. Та же проблема возникает у приложений, которые держат постоянные подключения к базе данных, брокерам сообщений или другим сервисам внутри кластера. Если за Service стоит несколько бэкендов, а клиент подолгу живёт на одном TCP-сеансе, балансировка на уровне Service перестаёт быть балансировкой запросов и превращается просто в выбор точки подключения.

Вот как это все выглядит на схеме:

Что изменилось в 2024–2025 годах

kube-proxy стал лучше жить на больших кластерах благодаря nftables. Это не решает проблему gRPC и HTTP/2 как таковую, но снимает часть сетевых и эксплуатационных ограничений у самих сервисов, особенно если в кластере много эндпоинтов и высокая частота обновлений.

eBPF-подход окончательно вышел из категории «для энтузиастов». Cilium научился не только заменять kube-proxy, но и делать socket-level балансировку, DSR и другие вещи, которые уменьшают накладные расходы на сетевом пути. Для платформ с большим количеством соединений это уже не лабораторный сценарий, а вполне рабочий продовый инструмент.

service mesh перестал быть почти обязательной историей про sidecar в каждом поде. У Istio Ambient Mode появилась зрелая архитектура без sidecar-ов, где базовый уровень безопасности и L4-обработки вынесен в ztunnel, а полноценный L7 включается только там, где он действительно нужен. У Linkerd продолжилось развитие лёгкой схемы с per-request балансировкой, а Gateway API заметно продвинулся как стандартный способ описывать маршрутизацию для HTTP и gRPC.

Почему L4-балансировка не решает проблему до конца

Service и kube-proxy работают на L4. Они умеют выбрать бэкенд в момент установки соединения, но не умеют принимать решение отдельно по каждому запросу внутри этого соединения. Для обычного HTTP/1.1 без keep-alive этого почти достаточно, потому что запросов много и соединения короткие. Для gRPC или HTTP/2 — уже нет.

Важно понимать, что задачи здесь две, и они разные:

  • выбрать, к какому поду подключиться;
  • равномерно распределять именно запросы, а не TCP-сеансы.

Kubernetes хорошо закрывает первую задачу и почти не помогает со второй, если соединение живёт долго.

Из-за этого на уровне Service всё может выглядеть исправно: readiness-пробы зелёные, эндпоинты актуальны, kube-proxy синхронизирован, скейлинг отработал. Но внутри приложения картина совсем другая: большая часть клиентских сессий давно привязана к старым бэкендам, и новые реплики просто не получают свою долю трафика.

По этой причине для долгоживущих протоколов приходится подниматься выше L4. Либо сам клиент должен понимать, что за Service стоит набор эндпоинтов, и уметь работать с этим набором. Либо нужен промежуточный L7-слой, который видит отдельные запросы и может распределять их независимо от того, как долго живёт конкретное TCP-соединение.

Клиентская балансировка по-прежнему остаётся рабочим вариантом

Самый прямой способ обойти ограничение Service — не пытаться заставить kube-proxy делать то, чего он не умеет, а перенести балансировку в клиент. Для этого обычно используют headless Service, где вместо одного виртуального IP клиент получает список IP-адресов всех подов.

Дальше приложение должно: получить список эндпоинтов, открыть соединения к нескольким или ко всем бэкендам, выбрать алгоритм распределения и периодически обновлять пул соединений, если состав подов изменился. Для некоторых стеков это давно нормальная практика. У gRPC есть собственные механизмы discovery и балансировки, а у приложений на Java, Go и других языках есть библиотеки, которые умеют работать с несколькими эндпоинтами без особой экзотики.

В живом проде часто используют и более простой приём: сервер сам ограничивает срок жизни соединения. Для gRPC это обычно схема с MaxConnectionAge и GOAWAY, когда сервер через заданный интервал мягко просит клиента пересоздать канал. После переподключения клиент снова проходит discovery и уже с нормальной вероятностью попадает на новые реплики. Это не идеальная балансировка, но во многих практических сценариях её хватает, чтобы убрать перекос после скейла.

Есть и более продвинутый вариант — proxyless gRPC с xDS, когда клиент получает от control plane не просто список бэкендов, а полноценные политики маршрутизации. Такой подход даёт больше контроля, но и повышает требования к инфраструктуре, версиям библиотек и общей сложности платформы. Для отдельных сервисов он оправдан, но делать его универсальным решением для всего кластера обычно нет смысла.

Service mesh как инфраструктурный ответ

Если переписывать клиентскую логику в каждом сервисе не хочется или не получается, задачу обычно переносят в инфраструктуру. Для этого и нужен service mesh: между приложениями появляется прокси-слой, который видит запросы на уровне L7 и может балансировать именно их, а не соединения целиком.

Долгое время у такого подхода был очевидный минус: sidecar в каждом поде стоил дорого по памяти, CPU и операционной сложности. На небольшом кластере это терпимо, на сотнях и тысячах подов — уже заметный overhead. Поэтому многие команды либо откладывали mesh, либо ограничивали его только узкими зонами.

Ситуация изменилась, когда Istio довёл Ambient Mode до зрелого состояния. Базовый слой теперь можно держать без sidecar-ов, а L7-функции включать выборочно через waypoint-прокси там, где они действительно нужны. Это сильно снижает стоимость входа в mesh и убирает прежнюю необходимость тащить Envoy в каждый под по умолчанию.

Linkerd идёт похожим путём с упором на более лёгкую эксплуатацию и per-request балансировку. Для команд, которым не нужен весь объём возможностей Istio, это остаётся сильным вариантом: меньше сложность, быстрее внедрение, понятная модель работы. Если же кластер уже использует Cilium как CNI, логично смотреть и в сторону Cilium Service Mesh, где часть сетевых функций остаётся в eBPF, а L7-поведение подключается по мере необходимости.

Здесь важен практический вывод: service mesh в 2026 году — уже не «тяжёлая надстройка ради моды», а вполне рациональный способ решить старую проблему долгоживущих соединений на инфраструктурном уровне, особенно если в кластере много команд, много языков и нет желания тащить клиентскую балансировку в каждое приложение отдельно.

Gateway API постепенно вытесняет старый Ingress

Для внешнего трафика и части внутренних маршрутов разговор теперь всё чаще идёт не про Ingress, а про Gateway API. Старый Ingress никуда не делся, но его модель слишком тесная для современных сценариев: слишком много завязано на аннотации, слишком мало стандартизированных возможностей и слишком слабое разделение ответственности между владельцами платформы и владельцами приложений.

Gateway API за последние релизы заметно укрепился. Для gRPC появился полноценный GRPCRoute, а вместе с развитием GAMMA один и тот же подход стал применим не только к north-south, но и к части east-west маршрутизации. Это не значит, что Gateway API автоматически заменяет service mesh, но для многих сценариев он стал естественным внешним слоем, который лучше сочетается с современными практиками, чем старый набор ingress-контроллеров и специфичных аннотаций.

Если говорить практично, в 2026 году новый сервис с внешним HTTP- или gRPC-трафиком уже разумно проектировать с прицелом на Gateway API. Это даёт более понятную схему маршрутизации, лучше ложится на managed-среды и проще переносится между реализациями. Для внутренней балансировки долгоживущих соединений одного Gateway API недостаточно, но как часть общей архитектуры он уже смотрится намного современнее старого Ingress.

Где здесь место eBPF и socket-level балансировке

Подход Cilium интересен тем, что пытается сократить сетевой путь ещё до того, как трафик дошёл до привычной схемы с iptables или nftables. При socket-level балансировке выбор бэкенда происходит в момент системного вызова connect(), ещё до полноценной обработки пакетов в сетевом стеке. За счёт этого уменьшается зависимость от NAT и conntrack, а сам путь запроса получается короче.

Это полезно, когда в кластере много соединений и сетевой overhead уже чувствуется на уровне CPU и latency. Особенно заметен эффект там, где соединения долгие и их очень много: например, у gRPC-стримов или WebSocket-нагрузки. В таких сценариях eBPF помогает не тем, что внезапно начинает балансировать запросы внутри соединения, а тем, что снижает стоимость самого сетевого транспорта.

Здесь, правда, важно не приписывать технологии лишнего. Socket-level балансировка не решает главную проблему gRPC и HTTP/2: бэкенд по-прежнему выбирается один раз на соединение. Поэтому если нужна именно per-request балансировка, всё равно придётся подниматься на уровень клиента, Gateway или mesh. Зато как способ уменьшить нагрузку на сетевой стек и conntrack у платформ с большим количеством долгих соединений это уже вполне зрелый инструмент.

Что будет, если ничего не делать

Иногда проблема долго не проявляется и создаёт ощущение, что всё и так работает. Если клиентов много, а бэкендов мало, даже без специальной логики трафик хоть как-то размазывается по репликам. Не идеально, но в ряде систем этого хватает, чтобы долго жить без явной боли.

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

На больших кластерах к этому добавляются и чисто сетевые издержки. Старый iptables-режим начинает тяжелее переживать рост числа сервисов и эндпоинтов, синхронизация правил становится дороже, а latency на установку соединений может неприятно подрастать. Если сверху ещё висит масса долгих соединений, можно быстро уткнуться и в ограничения conntrack, и в перекос трафика, и в неочевидную деградацию во время rolling update.

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

Что выбрать в Cloud Containers VK Cloud в 2026 году

Если вы поднимаете новый кластер в 2026 году, базовую сетевую схему разумно строить на Service, EndpointSlices и kube-proxy, ориентируясь уже не на 1.33, а на поддерживаемые ветки Kubernetes 1.34 и выше. Для большинства HTTP-сервисов этого достаточно: такая конфигурация закрывает типовые сценарии без лишнего усложнения и даёт понятную точку роста по мере расширения кластера.

Если нагрузка завязана на gRPC, HTTP/2, WebSocket и другие долгоживущие соединения, первым кандидатом на внедрение остаётся клиентская балансировка. Headless Service, discovery endpoint’ов и контролируемое переподключение через ограничение срока жизни соединения во многих случаях дают нужный результат без service mesh и без отдельного L7-слоя. Это особенно удобно там, где стек приложений однородный и такую логику можно держать в коде без лишних согласований между командами.

Когда сервисов в кластере становится много, языки и фреймворки различаются, а требования к безопасности, маршрутизации и observability выходят за пределы простой L4-доставки, имеет смысл смотреть в сторону service mesh. Для зрелого и гибкого сценария подойдёт Istio с Ambient Mode. Если нужен более лёгкий вариант, остаётся Linkerd. В кластерах на Cilium логично рассматривать и kube-proxy replacement, и его mesh-возможности, чтобы не собирать сетевой слой из разрозненных компонентов.

Для внешнего трафика и современной L7-маршрутизации ориентироваться стоит на Gateway API, а не строить новую схему вокруг старого Ingress. Это особенно заметно в gRPC-сценариях, где нужна более выразительная модель маршрутов и единый подход к управлению трафиком.

Cloud Containers в VK Cloud позволяет перенести такую архитектуру в управляемый кластер без пересборки всей модели манифестов. Поэтому выбор здесь обычно сводится не к совместимости платформы с нужным паттерном, а к тому, на каком уровне вы хотите решать проблему долгоживущих соединений: в клиенте, в service mesh или в отдельном L7-слое.

Оставьте заявку, чтобы получить консультацию

Наши специалисты свяжутся с вами в ближайшее время и ответят на все вопросы.

section-subscribe_2x.png

            Узнавайте о выходе новых статей в блоге первыми!

            Будем держать в курсе новостей и облачных трендов

            section-subscribe_2x.png
              section-subscribe_2x.png
              Теги: Разработка, kubernetes, devops, Balancer, iptables, microservices, Service Mesh
              Ссылка скопирована
              Поделиться

              Почитать по теме

              _blog_head_158.png
              24 июля

              External Secrets Operator в Managed Kubernetes: безопасное управление секретами без ручной работы

              _blog_head_100.png
              24 июля

              S3-CSI в Managed Kubernetes: масштабирование приложений без потери общих файлов

              40+ готовых сервисов