Sealos сократила время синхронизации Kubernetes Ingress с минут до секунд для более чем 20 000 доменов путём оптимизации Higress, Istio и Envoy. Узнайте, как в компании устранили проблему значительного снижения производительности шлюзов.

Кризис, который всё изменил

Представьте: вы управляете одной из самых амбициозных облачных платформ в мире, которая обслуживает почти 200 000 пользователей и 40 000 инстансов. Пользователи создают приложения, нажимают «развернуть» и... ждут… ждут… ждут… Процесс, который должен занимать секунды, растягивается на мучительные минуты (а иногда и на целых полчаса), прежде чем их домены станут доступны.

С такой реальностью мы столкнулись в Sealos Cloud. Наш Nginx Ingress достиг предела своих возможностей и просто не справлялся со взрывным ростом. Переход на Higress поначалу обнадёжил, но когда число конфигураций Ingress в одном кластере Kubernetes перевалило за 20 тысяч, возник новый кошмар: наш шлюз буквально задыхался под тяжестью собственного успеха.

Жестокая реальность в цифрах:

  • 20 000+ конфигураций Ingress для управления поддоменами и кастомными доменами.

  • Задержки активации новых доменов — более 10 минут в часы пик.

  • Стремительное падение производительности: каждая новая тысяча доменов только усугубляла ситуацию.

  • Рост раздражения пользователей, ведь их воркфлоу разработки практически останавливались.

Как только кластер перешагивал отметку в 10 тысяч Ingress-записей, каждая перенастройка шлюза провоцировала лавину вычислительной нагрузки. Во время скачков трафика активация новых доменов занимала более получаса — целая вечность по облачным меркам, где скорость — это всё.

Охотимся на убийцу производительности

Вооружившись инструментами для профилирования и твердой решимостью, мы приступили к расследованию, которое погрузило нас в самые недра современной архитектуры сервис-мешей (service mesh). Предстояло изучить четыре ключевых уровня:

  1. Higress — шлюз-оркестратор, на котором и проявились симптомы.

  2. Istio — слой управления, таящий в себе скрытую неэффективность.

  3. Envoy — слой данных, который «задыхался» от сложности.

  4. Protobuf — протокол сериализации, создававший неожиданные «бутылочные горлышки».

Анатомия кризиса

Чтобы понять проблему, нужно было в деталях разобрать, как Higress обрабатывает изменения в Ingress. Архитектура обнажает два критически важных компонента:

  • слой управления («мозг»): когда пользователи меняют Ingress, встроенный в Higress, Istio запрашивает полную конфигурацию кластера, вносит правки и передаёт обновлённую конфигурацию шлюза. Этот слой оркестрации казался очевидным виновником;

  • слой данных («мышцы»): Envoy получает конфигурацию и выполняет фактическую маршрутизацию трафика. Он умеет «умно» обновлять только то, что изменилось. Как оказалось, у этого «ума» была разрушительная цена, которую нам только предстояло узнать.

В чём главная проблема? Для каждого домена необходимо встраивать полную конфигурация TLS в цепочку фильтров (filterchain). Даже если у тысяч доменов абсолютно одинаковые настройки TLS, система не может их переиспользовать. Когда количество доменов резко подскочило, файлы конфигурации разрослись до невероятных размеров, что и вызвало лавинообразное падение производительности.

Вооружившись встроенным в Go инструментом pprof и флейм-графами, мы приступили к расследованию.

Акт I: укрощение управляющего слоя

Расследование началось с управляющего слоя Istio. Флейм-графы сразу помогли выявить парочку главных «злодеев», пожирающих производительность.

Катастрофа с GetGatewayByName

Глубоко в кеше lds Istio скрывалась функция с невинным названием, но с сокрушительным эффектом. GetGatewayByName выполняла сравнения путём перебора всех цепочек фильтров (filterchains) со сложностью O(n²). Фактически, каждая цепочка сверялась с каждой другой, закручивая вычисления в смертельную спираль:

Пример неэффективного кода GetGatewayByName
Пример неэффективного кода GetGatewayByName
Пример неэффективного кода GetGatewayByName
Пример неэффективного кода GetGatewayByName

Решение оказалось на удивление простым и изящным: заменить перебор «в лоб» на заранее подготовленные хеш-таблицы. Квадратичный кошмар превратился в поиск за константное время:

Оптимизированный код GetGatewayByName
Оптимизированный код GetGatewayByName
// Было: адский перебор за O(n²).
for _, chain := range filterChains {
    for _, existing := range existingChains {
        if chain.Name == existing.Name { /* поиск совпадений */ }
    }
}
 
// Стало: спасение — поиск по хеш-карте за O(1).
gatewayMap := make(map[string]*FilterChain)
// Предварительная обработка для мгновенного поиска.

Подвох с сериализацией в Protobuf

Процесс обновления конфигурации в Istio выявил ещё одного «убийцу» производительности. Система конвертировала данные конфигурации в protobuf.Message, выполняла слияния, а затем конвертировала всё обратно. Это приводило к постоянному выделению памяти и выполнению затратных операций со строками:

Фрагмент кода, где сериализация и десериализация Protobuf в Istio тормозят работу Higress
Фрагмент кода, где сериализация и десериализация Protobuf в Istio тормозят работу Higress

Вместо того чтобы переписывать всю систему слияний в Istio (это было бы непрактично), мы реализовали стратегическое кеширование. Прорыв состоял в том, чтобы каждый объект сериализовался и десериализовался лишь единожды, вне зависимости от количества операций слияния.

Результаты не заставили себя ждать: производительность контроллера подскочила более чем на 50 %, а флейм-графы показали, что прежние «бутылочные горлышки» практически испарились.

Флейм-граф управляющего слоя Istio после оптимизации
Флейм-граф управляющего слоя Istio после оптимизации

Готовую реализацию с оптимизированным кодом можно посмотреть в нашем репозитории.

Акт II: где на самом деле скрывался монстр

Несмотря на успехи с управляющим слоем, общая производительность упрямо не хотела расти. Настоящий монстр скрывался в слое данных Envoy. Губительным «бутылочным горлышком» оказалась однопоточная обработка.

Envoy — это написанный на C++ прокси для сервис-мешей с открытым исходным кодом (в нашем случае — версия, оптимизированная под Higress). Чтобы изучить его производительность, мы с помощью утилит perf и FlameGraph.pl построили развёрнутые флейм-графы, которые отражали процесс запуска слоя данных:

Флейм-граф запуска Envoy
Флейм-граф запуска Envoy

Кошмар сериализации Filterchain

Флейм-графы Envoy показали шокирующую правду: почти всё время уходило на сериализацию в Protobuf. Чтобы разобраться, почему так происходит, мы копнули глубже в механизм обнаружения изменений Envoy:

Флейм-граф изменений в Envoy Ingress
Флейм-граф изменений в Envoy Ingress

Вот изящная проблема: как среди 10 000 записей Envoy находит изменившиеся цепочки фильтров (filterchains), когда:

  • у каждой цепочки нет своего уникального ключа;

  • для того, чтобы найти отличия, нужно сравнивать всё содержимое целиком;

  • связка контроллер-шлюз не гарантирует никакого порядка.

Изначально реализация Envoy была такой: вся конфигурация цепочки фильтров сериализуется и используется как ключ в хеш-таблице (hashmap) для поиска. Для хеширования выбрали быстрый xxHash, но даже он начинает тормозить при обработке гигантских сериализованных конфигов.

Envoy хеширует сериализованные данные цепочки фильтров с помощью xxHash
Envoy хеширует сериализованные данные цепочки фильтров с помощью xxHash

Хоррор-шоу с хеш-функциями

Всё оказалось гораздо хуже, чем мы думали. Из-за использования absl::flat_hash_map в Envoy хеш-функции вызывались постоянно, причем не только при поиске, но и при операциях присваивания и обновления. Когда у вас 20 с лишним тысяч цепочек фильтров, эти безобидные на вид операции превращаются в настоящий армагеддон производительности.

absl::flat_hash_map постоянно вызывает хеш-функции
absl::flat_hash_map постоянно вызывает хеш-функции

Акт III: решающий прорыв

Решение этой задачи потребовало хирургической точности сразу на нескольких уровнях системы.

Революционное рекурсивное хеширование

Полностью отказались от сериализации в пользу рекурсивного хеширования. При таком подходе хеш-значение типа вычисляется на основе хешей его вложенных полей. Это позволило избавиться от ресурсоёмких циклов форматирования строк и выделения памяти.

Стратегическое кеширование хешей

Внедрили повсеместное кеширование хешей. Пришлось даже поправить protobuf.Message, чтобы кеш можно было копировать. Это позволило избежать лишних вычислений хешей на разных уровнях системы.

Наш собственный класс CachedMessageUtil

Ещё одно ключевое нововведение — класс CachedMessageUtil, созданный на базе Protobuf. Он позволил свести к минимуму изменения в коде Envoy, но при этом получить максимальный прирост производительности. Этот подход не только дал впечатляющие результаты, но и упростил нам жизнь на будущее — поддерживать и обновлять систему стало легче.

Класс CachedMessageUtil реализует кеширование хеш-значений Protobuf
Класс CachedMessageUtil реализует кеширование хеш-значений Protobuf
Модифицированная хеш-функция в сравнении с исходной
Модифицированная хеш-функция в сравнении с исходной

Трансформация оказалась поразительной. Флейм-графы после оптимизации показали, что выявленные узкие места теперь потребляют лишь малую долю общего вычислительного времени.

Flame-граф после оптимизации слоя данных Envoy
Flame-граф после оптимизации слоя данных Envoy

Победа: как мы ускорили производительность

Лабораторные результаты

Тестирование с 7000 активных Ingress'ов выявило ошеломляющие улучшения:

Метрика

До оптимизации

После оптимизации

Улучшение

Время отклика Ingress

47 секунд

2,3 секунды

В 20 раз!

Время синхронизации Higress Ingress до оптимизации
Время синхронизации Higress Ingress до оптимизации
Время синхронизации Higress Ingress после оптимизации
Время синхронизации Higress Ingress после оптимизации

Результаты в production

Результаты в реальной рабочей среде оказались даже лучше, чем ожидалось:

  • активация домена — ускорилась с 10+ минут до менее чем 5 секунд;

  • пиковый трафик — больше никаких 30-минутных задержек при резких скачках трафика;

  • масштабируемость — производительность почти не зависит от количества доменов;

  • эффективность использования ресурсов — резко снизилась вычислительная нагрузка.

Текущая статистика из production показывает, какими масштабами мы оперируем:

Число Ingress’ов в проде Sealos
Число Ingress’ов в проде Sealos
Статистика по ресурсам в проде Sealos
Статистика по ресурсам в проде Sealos

Сравнение производительности «до» и «после» говорит само за себя:

Время отклика Ingress в проде Sealos — до
Время отклика Ingress в проде Sealos — до
Время отклика Ingress в проде Sealos — после
Время отклика Ingress в проде Sealos — после

Наш кластер в Ханчжоу, активация доменов в котором раньше затягивалась на несколько минут, теперь стабильно отвечает менее чем за 5 секунд. Улучшения отлично масштабируются — то, что раньше переставало работать при больших нагрузках, теперь сохраняет пиковую производительность.

Триумф инженерной мысли

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

Ключевые инновации

  • Управляющий слой Istio: поиск шлюзов на основе хеш-таблиц и умное кэширование Protobuf.

  • Слой данных Envoy: рекурсивные алгоритмы хеширования и комплексное кэширование хешей.

  • Системный уровень: не затрагивающие API оптимизации, которые гарантируют легкое развёртывание.

Общее влияние

  • Пользовательский опыт: от раздражающих задержек до мгновенного результата.

  • Эксплуатационное совершенство: предсказуемая производительность в любом масштабе.

  • Оптимизация ресурсов: значительное сокращение бесполезной вычислительной нагрузки.

  • Уверенность инженеров: стабильная работа шлюза даже в условиях экстремальных нагрузок.

Планы на будущее: новая глава

Эта успешная оптимизация — только первый шаг в нашей гонке за производительностью. Сегодня Sealos уверенно обслуживает 200 000 пользователей на 40 000 инстансов, а наш шлюз без малейших проблем справляется с более чем 20 000 конфигураций Ingress.

Накоплен бесценный опыт в таких областях, как:

  • продвинутые методы оптимизации Protobuf;

  • высокопроизводительные алгоритмы хеширования, включая xxHash и др.;

  • продуманные стратегии кеширования;

  • тонкая настройка производительности глобальных сервис-мешей на базе Istio и Envoy.

Полученные знания применимы далеко за пределами нашего конкретного сценария. Эти техники глубокой оптимизации будут полезны любой компании, которая озабочена повышением производительности Kubernetes Ingress при больших нагрузках. Поскольку внедрение нативных облачных технологий ускоряется, а мультитенантность в одном кластере становится нормой, эти паттерны производительности будут приобретать всё большее значение.

Вся мощь глубокой оптимизации

Эта история иллюстрирует фундаментальную истину о современных распределённых системах: поверхностные оптимизации часто упускают из виду настоящих «убийц» производительности, скрывающихся в сложных взаимодействиях внутри системы. Глубоко погрузившись в архитектуру Higress, Istio и Envoy, мы обнаружили, что самые серьёзные проблемы возникали на стыке хорошо спроектированных компонентов.

Наш путь от десятиминутных задержек до пятисекундных откликов заключался не просто в исправлении кода, а в понимании сложных взаимоотношений между слоями управления и слоями данных, сериализацией и кешированием, алгоритмами и архитектурой. Результат? Шлюз, который отлично масштабируется и стабильно работает, какими бы высокими ни становились требования наших пользователей.

P. S.

Читайте также в нашем блоге:

Комментарии (0)