Сегодня технологии распределённых систем и микросервисов требуют высокой производительности и минимальных задержек для взаимодействия между компонентами. Одним из фундаментальных инструментов для реализации удалённых вызовов процедур (RPC) является gRPC — современный и мощный фреймворк, использующий HTTP/2 и позволяющий эффективно передавать сообщения между сервисами. Тем не менее, несмотря на широкое признание gRPC как высокопроизводительной платформы, на практике выявляются неожиданные узкие места, особенно в условиях сетей с очень низкими задержками. В этой статье мы подробно разберём проблему узкого места на клиентской стороне gRPC, которое было обнаружено инженерами YDB, и рассмотрим, как его эффективно обойти, улучшив пропускную способность и снизив задержки. gRPC и архитектурные особенности Для начала, важно понять ключевые аспекты работы gRPC клиента.
Основой gRPC является HTTP/2, который поддерживает мультиплексирование нескольких RPC-запросов в одном TCP-соединении. В gRPC клиенте создание каналов и настройка их параметров играет важную роль. Каждый канал обычно соответствует одному TCP-соединению с сервером. Внутри этого соединения возможно одновременно выполнение ограниченного количества параллельных RPC-запросов — так называемое ограничение на число потоков HTTP/2. По умолчанию лимит составляет около 100 параллельных потоков на соединение.
Система должна управлять такой нагрузкой, не создавая узких мест. Тем не менее, на практике инженеры YDB выявили, что при увеличении числа клиентов (воркеров) на стороне клиента, которые инициируют запросы через gRPC, возникает аномальное поведение. При этом, несмотря на низкую сетевую задержку и высокое качество сети, общая производительность системы не масштабируется линейно, а задержка на уровне клиента начинает резко возрастать. Это значит, что узким местом оказывается вовсе не сеть или сервер, а именно клиентская часть коммуникационного стека gRPC. Экспериментальное исследование Чтобы понять источник проблемы, разработчики YDB реализовали микро-бенчмарк, симулирующий пинг-запросы к gRPC серверу.
Клиентская часть состояла из нескольких воркеров, каждый из которых создавал собственный gRPC канал и выполнял RPC вызовы с определённым уровнем параллелизма (in-flight). Тесты запускались на мощных серверах с быстрыми процессорами и напрямую связанной сетью с пропускной способностью 50 Гбит/с, где RTT составлял порядка десятков микросекунд, что говорит о крайне низкой задержке канала. Результаты показали, что при росте количества одновременных запросов общая пропускная способность росла гораздо медленнее, чем можно было ожидать при идеальной масштабируемости. При этом медианные и пиковые значения задержек начали значительно увеличиваться, что указывало на накопление внутренней клиентской латентности. Более того, при проверке инструментами системного мониторинга выяснилось, что вне зависимости от количества параллельных потоков используется всего одно TCP-соединение между клиентом и сервером.
Это означает, что все запросы и ответы фактически проходят через один канал, который становится узким местом. Анализ сетевых трассировок подтвердил, что на уровне TCP не было никаких проблем с потерями пакетов, задержками или переключениями окон. Все параметры протокола TCP, такие как отключение алгоритма Nagle (TCP_NODELAY) и размер окон, были оптимальны. Таким образом, причина лежала не на уровне сети, а внутри логики обработки gRPC сообщений клиентом. Проблемы мультиплексирования и очередей внутри gRPC Суть проблемы кроется в механизме мультиплексирования HTTP/2, через который проходят все RPC вызовы одного TCP-соединения.
Ограничение на количество одновременных потоков вынуждает дополнительные запросы попадать в очередь на стороне клиента, что приводит к так называемой очередьющей блокировке вызовов. Этот эффект проявляется в интенсификации ожиданий на стороне клиента, создавая задержки, которые растут с увеличением числа параллельных задач. Разработчики gRPC рекомендуют несколько решений для обхода данной проблемы. Одним из них является создание отдельных каналов для различных нагруженных участков приложения. Другой метод — использование пула каналов с разными параметрами, чтобы каждый канал отвечал за часть трафика, и при этом создавались отдельные TCP-соединения.
В документации gRPC подчеркивается, что каналы, созданные с одинаковыми аргументами, будут использовать один и тот же подканал и одним TCP соединением. Причём это порождает дополнительную конкуренцию за ресурсы внутри одного канала. Реализация оптимального решения В эксперименте YDB оба метода были опробованы, при этом оказалось, что эффективное улучшение достигается, когда каждый клиентский воркер создаёт собственный канал с уникальными настройками, что гарантирует создание независимого TCP-соединения для каждого потока нагрузки. Также помогала активация специального параметра GRPC_ARG_USE_LOCAL_SUBCHANNEL_POOL, который заставляет gRPC использовать локальный пул подканалов, и тем самым снижает конкуренцию внутри единственного соединения. После внесения изменений в конфигурацию, пропускная способность приложения выросла почти в шесть раз для обычных RPC и более чем в четыре раза для streaming RPC.
Одновременно наблюдался очень медленный рост задержек при увеличении уровня параллелизма — что соответствует положительному росту производительности без дополнительных узких мест на стороне клиента. Значение проблемы и области применения Низкая задержка и высокая пропускная способность крайне важны в распределённых базах данных, системах реального времени, финансовых рынках и других критически важных приложениях. Многие современные микросервисные архитектуры опираются на gRPC для обеспечения связи между компонентами, и выявление таких особенностей работы клиента gRPC помогает лучше проектировать масштабируемые системы. Особенно важно учитывать, что в условиях высоколатентной сети (например, с задержками порядка нескольких миллисекунд) эффект от узкого места клиента gRPC существенно снижается. В таких условиях сетевые задержки доминируют в общей сумме латентности, и внутренняя оптимизация клиента становится менее критичной.
Однако в современных дата-центрах, кластерных средах и облачных приложениях с высокоскоростной инфраструктурой и низкими задержками это может стать ключевым фактором производительности. Практические рекомендации Для инженеров и разработчиков, использующих gRPC в системах с высокой нагрузкой и низкой сетевой задержкой, рекомендуется внимательно управлять количеством и настройками каналов. Рекомендуется создавать отдельные каналы с уникальными аргументами для каждого потока запросов высокого приоритета или высокой интенсивности. Это позволит распределить нагрузку по нескольким TCP-соединениям, снизить конкуренцию внутри каналов и повысить эффективность мультиплексирования. Следует также обратить внимание на параметры настройки gRPC, такие как GRPC_ARG_USE_LOCAL_SUBCHANNEL_POOL, а также мониторить внутренние метрики задержек и пропускной способности.
Своевременное выявление и устранение узких мест на клиентской стороне поможет максимизировать выгоду от высокоскоростных сетей. Перспективы развития Исследование и устранение узких мест в gRPC клиентах — открытая тема, в которой возможно дальнейшее совершенствование как самого протокола, так и реализаций фреймворка. В будущем могут появиться усовершенствования, позволяющие более эффективно балансировать нагрузку внутри одного соединения, расширять лимиты параллельных потоков или использовать новые подходы к мультиплексированию и планированию вызовов. Сообщество разработчиков, в том числе команда YDB, активно работает над такими улучшениями и призывает к сотрудничеству всех заинтересованных в повышении производительности gRPC. Пользователи могут внести вклад в открытые репозитории и делиться своими наработками для достижения оптимальной работы в различных условиях.