Привет, Хабр! Меня зовут Данила Трушин, я руководитель направления в СберТехе. Мы с командой развиваем Platform V Synapse Service Mesh — продукт, который обеспечивает надёжную безопасную интеграцию и оркестрацию микросервисов в облаке.
При промышленной эксплуатации иногда можно столкнуться с ситуациями, которые зачастую непросто или невозможно получить на тестовых или испытательных стендах. В ряде случаев такие ситуации касаются производительности и устойчивости компонентов к нагрузке.
Сегодня хочу поговорить о том, как мы решили проблему троттлинга процессора на граничном прокси в случае, когда наблюдаемая нагрузка приложения была ниже заявленной и проверенной на стендах нагрузочного тестирования.
Ситуация: увеличение троттлинга в инсталляции
Клиент обратился к нам с вопросом: в давно работающей инсталляции при выполнении стандартных задач наблюдается троттлинг процессора, и время обработки запросов на граничных прокси увеличилось в три и более раза.
Что такое троттлинг в системах оркестрации контейнеров Kubernetes
Когда контейнер достигает лимита потребления процессорного времени, оркестрация процессов в нем принудительно замедляется. Этот процесс называется троттлинг (англ. throttling). Он помогает избежать конкурентной гонки за ресурсы между процессами в разных контейнерах.
Граничный прокси (edge proxy) — это инсталляция прокси, которая представляет собой точку входа (ingress) или выхода (egress) трафика в/из namespace.
Основные задачи граничного прокси — аутентификация и авторизация трафика, маршрутизация и балансировка трафика между подами в namespace, защита приложений от чрезмерной и/или вредной нагрузки (например, атак типа DDOS), несанкционированного доступа, санации и валидации трафика.
Ниже — типичная схема namespace, схематично показывающая расположение граничных прокси относительно подов приложений:
Интересно, что в ситуации клиента именно граничный прокси стал бутылочным горлом для системы. Часть запросов не обрабатывалась в установленный таймаут в две секунды на выполнение запроса, и система-потребитель фиксировала ошибку превышения таймаута. Нужно было разобраться, почему так происходит, и исправить ситуацию.
Как решали вопрос
Давайте посмотрим, что мы наблюдали. Нижеприведённые графики получены с тестового стенда. Они не отражают все возможные варианты оптимизации или нагрузки, но достаточно полно иллюстрируют ситуацию. Первый график иллюстрирует постоянный троттлинг процессора, который мы наблюдаем при относительно небольшой нагрузке в 300 запросов в секунду:
Детальное исследование показало наиболее загруженные процессы в контейнере. Ими оказались отдельные рабочие потоки (worker threads) нашего прокси. Уменьшение числа рабочих потоков позволило при той же нагрузке снизить объем троттлинга в шесть раз:
Для сравнения — графики до и после:
Причиной «забивания» рабочих потоков стали неуспешные подключения со стороны одной из систем-потребителей. Здесь нужно пояснить, что несколько рабочих потоков параллельно обрабатывают данные с помощью граничного прокси, построенного на базе Envoy Proxy. Основной поток распределяет обработку отдельных запросов. Каждый из потоков устанавливает соединение с источником запроса (downstream) и сервером (upstream), выполняет в зависимости от настроек аутентификацию и авторизацию запроса, применяет различные фильтры обработки запроса и т. д.
В случае клиента неуспешными были соединения, с которыми серверу не удалось установить доверенное TLS‑соединение из‑за неудавшейся проверки клиентского сертификата. Причины могут быть разными — например, разрыв соединения, неуспешная аутентификация запроса, ошибки сетевых настроек и прочее, вплоть до случаев DDOS-атак.
Множественные попытки соединений, тем более с TLS, сами по себе дорогие с точки зрения CPU. Но такие попытки быстро отбиваются прокси. Это приводит к тому, что рабочий поток, на который попало такое соединение, быстрее выполнит обработку единичного запроса, чем параллельный поток, который отдает данные дальше на сервер: возникает гонка за ресурсы.
Как следствие, первый рабочий поток быстрее использует доступный лимит CPU, выделенный контейнеру, не оставив доступных тактов CPU второму потоку для обработки полезной нагрузки.
Получается, что множественные попытки некорректно сконфигурированного клиента установить соединение с прокси вызывали паразитную нагрузку. После того, как мы ее убрали, ситуация нормализовалась, а производительность повысилась в два раза:
Рекомендации
Мы проанализировали ситуацию и выработали рекомендации, которые позволят избежать подобных ситуаций:
-
При конфигурировании граничных прокси, через которые проходит большое число запросов, следует учитывать параметр concurrency, определяющий количество рабочих потоков прокси. При большом числе процессоров на ноде рекомендуется уменьшить concurrency, например, до двух, для равномерного распределения CPU limit по рабочим потокам. Целесообразно провести нагрузочное тестирование для определения оптимальных для вашей инсталляции параметров.
При значении по умолчанию (concurrency 0) прокси выставляет число потоков в значение, равное числу процессорных юнитов пода (см. nproc). Это может приводить к быстрому исчерпанию лимита CPU на «быстрых» ответах. При этом увеличится время обработки запросов к другим сервисам.
«Быстрыми» в этом случае могут быть ответы самого прокси о недоступности сервисов, ошибке аутентификации/авторизации, которые могут появляться в случае, например, DDOS-атак или ошибок в конфигурировании смежных систем.
При использовании общих для нескольких систем граничных прокси нагрузочное тестирование должно проводиться с подачей нагрузки по всем каналам / со всеми видами проксируемого трафика. Это позволит определить взаимное влияние сервисов друг на друга, а также предельные параметры нагрузки.
Тесты на надежность/устойчивость системы должны проводиться в том числе и с негативными сценариями, то есть при подаче некорректных соединений от одного или нескольких потребителей. Например, мы подложили в один из экземпляров эмулятора нагрузки некорректный клиентский сертификат.
Поделюсь дополнительными рекомендациями, которые будут полезны при разработке архитектуры системы:
Учитывайте критичность сервисов, включаемых в проект. Граничные прокси — часть интеграционных сценариев высокого уровня (Business или Mission сritical). Их рекомендуется выделять в отдельные инсталляции (deployments), чтобы исключить взаимное влияние параллельных интеграций.
Учитывайте время жизни сетевых соединений/сессий. Потребление памяти на граничных прокси может расти, если в интеграционных сценариях есть запросы, которые выполняются длительное время, или приложения, которые требуют поддержания длительной сессии. При большом числе одновременно открытых сессий рост потребления памяти может приводить к исчерпанию лимита и перезапуску пода граничного прокси с ошибкой OOMKilled.
-
Учитывайте требования безопасности, предъявляемые к вашей системе:
разделение потоков, содержащих критичные данные, и прочих;
использование отдельных инсталляций граничного прокси для сценариев с дополнительными валидаторами трафика или с подключением внешних сервисов авторизации;
использование отдельных прокси с монтированием критичных секретов (x509 ключей, в частности).
Разделяйте фронтенд‑ и бэкенд‑трафик, пользовательский трафик и межсервисные взаимодействия. В принципе фронтенд и бэкенд лучше разнести по разным namespace из соображений безопасности.
-
При сопровождении следите за потреблением памяти контейнерами прокси. Если замечаете повышенное потребление, обратите внимание на следующие аспекты:
частые TCP‑соединения в access‑логах с небольшим объемом передаваемых данных: понаблюдайте за метриками с суффиксами cx_total, cx_active, cx_destroyed, rq_total и другими аналогичными по смыслу;
количество и частоту ошибок соединений, фиксируемых в логах граничного прокси.
HunterXXI
а может ли как то повлиять на ситуацию замена прокси компонента, на пример на nginx? или логика обработки запроса всегда одинаковая?