Современные распределённые системы и облачные сервисы всё чаще используют gRPC для организации эффективного межслужебного взаимодействия. Этот протокол, построенный поверх HTTP/2, предлагает мощный и удобный инструментарий для выполнения удалённых вызовов процедур (RPC), обеспечивая высокую производительность и удобство масштабирования. Однако несмотря на признанную эффективность, в условиях работы в сетях с минимальными задержками была обнаружена неожиданная проблема, которая существенно влияет на производительность клиента gRPC. Внимательное изучение и решение данной проблемы является важным этапом для специалистов, занимающихся разработкой высоконагруженных систем и распределённых баз данных, таких как YDB, что позволяет улучшить пропускную способность и уменьшить время отклика сервиса. В данной статье рассматривается природа и причины узкого места, а также предлагаются практические методы его обхода, с акцентом на особенности внутренней архитектуры gRPC и поведение в условиях реальной нагрузки.
Начнём с небольшого обзора того, что представляет собой gRPC и как он устроен. gRPC построен на базе HTTP/2, что обеспечивает мультиплексирование нескольких потоков данных по одному TCP-соединению. Такой подход позволяет одновременно запускать разнообразные вызовы на одной параллельной шине, снижая накладные расходы на установку соединений. Клиентская часть gRPC обычно использует несколько каналов (channels), где каждый канал соответствует одному или нескольким TCP-соединениям с сервером. Важным параметром реализации является ограничение на количество одновременных параллельных потоков (RPCs), которые допускаются в одном HTTP/2-соединении — по умолчанию их максимум около 100.
Если количество активных вызовов превышает это значение, новые запросы ставятся в очередь и ждут освобождения ресурсов. Именно из-за этого ограничения сервисы с высокой нагрузкой и длительными потоковыми вызовами рекомендуют создавать пул каналов или использовать отдельные каналы для разных областей нагрузки. В YDB, распределённой SQL базе данных с поддержкой строгой согласованности и транзакций ACID, gRPC используется для взаимодействия клиентов с кластером. При тестировании производительности выяснилось, что с уменьшением количества нод кластера нагрузка с клиента падает, а латентность, наоборот, растёт, несмотря на очевидные свободные ресурсы сервера. Анализ логов, сетевого трафика и работы клиента показал, что основное узкое место находится именно со стороны клиента gRPC.
Интересно, что проблема проявляется даже при большой пропускной способности сети (50 Гбит/с) и минимальных задержках пакетов (десятки микросекунд). Для глубокого изучения был разработан простой микро-бенчмарк ping-сервер и клиент на C++, использующие актуальную версию gRPC (v1.72.0). Тесты были проведены на двух мощных bare-metal серверах с процессорами Intel Xeon Gold, подключённых скоростной сетью.
Клиент создаёт несколько параллельных рабочих потоков, каждый из которых держит в полёте некоторое число запросов к серверу по своему gRPC-каналу. Измерения показали, что несмотря на ожидаемый линейный рост пропускной способности при увеличении числа параллельных запросов и каналов, на практике наблюдалась существенная деградация — увеличение латентности в среднем в 3–4 раза и слабый рост суммарной пропускной способности. Дальнейший анализ сетевых пакетов и загруженности TCP-соединения показал, что все запросы клиента сосредоточены в одном TCP-соединении. Это заставило задуматься, почему gRPC не распределяет вызовы по нескольким соединениям, несмотря на наличие отдельных каналов. Выяснилось, что по умолчанию каналы, созданные без различающихся параметров, реиспользуют один и тот же подканал и TCP-соединение, что приводит к очередям и большим задержкам.
Задержка в 150-200 микросекунд между пакетами вызовов в одном потоке стала главным фактором ограничения производительности клиента, тогда как серверная часть работала без узких мест. Решение заключалось в правильной настройке клиента. Во-первых, каждый рабочий поток должен иметь отдельный gRPC-канал с уникальными параметрами, чтобы гарантировать создание собственного TCP-соединения. Изменение аргументов канала (например, добавление уникального идентификатора) помогает gRPC избежать повторного использования внешнего соединения. Во-вторых, использование внутреннего параметра GRPC_ARG_USE_LOCAL_SUBCHANNEL_POOL позволяет управлять пулом подканалов на уровне клиента для оптимального распределения нагрузки.
После внедрения этих изменений в настройках клиента показатели производительности сильно улучшились. Пропускная способность выросла почти в 6 раз для стандартных RPC и в 4.5 раза для стриминговых вызовов, а латентность выросла гораздо медленнее даже при увеличении количества одновременных запросов. Это вновь подчеркнуло важность внутренней архитектуры gRPC и её влияния на масштабируемость при работе с низкой сетевой задержкой. Также была проведена проверка поведения в сети с высоким уровнем задержки, около 5 миллисекунд.
В такой среде эффекты повторного использования соединений и очередей становятся менее критичными, поскольку сама сеть добавляет значительную задержку. По результатам показано, что в сетях с высокой латентностью преимущества мультиканального подхода менее заметны, так как сетевые задержки доминируют над внутренними задержками протокола. Выводы из проведённого исследования имеют важное практическое значение для разработчиков и архитекторов распределённых систем и высокопроизводительных приложений. Несмотря на все достоинства gRPC, в средах с низкой сетевой задержкой дефолтные настройки клиента могут стать сутью узкого места в производительности системы. Для достижения максимальной пропускной способности и минимальных задержек необходимо применить подходы, разделяющие трафик по нескольким каналам с уникальными настройками, что приводит к реальному созданию множества TCP-соединений и снижению конкуренции внутри gRPC.
Помимо этого, стоит рекомендовать тщательную настройку соответствующих параметров операционной системы и библиотеки gRPC, включая использование подобъёмов (completion queues) на сервере, а также правильное привязывание потоков к CPU для минимизации накладных расходов на переключения и оптимизации работы с NUMA. Всё это в комплексе обеспечит стабильную и предсказуемую работу серверов и клиентов даже при экстремально высоких нагрузках. Тем не менее, разработчики gRPC и сообщества продолжают работу над совершенствованием производительности, поэтому могут быть доступны дополнительные оптимизации в будущих релизах. Также возможна интеграция дополнительных пулов и адаптивных механизмов управления соединениями. Пользователи, заинтересованные в повышении эффективности своих систем, могут внести предложения или улучшения в открытые репозитории gRPC и бенчмарков, что станет вкладом в развитие всей индустрии.
Таким образом, ключ к разрешению неожиданного узкого места клиента gRPC в условиях низкой сетевой задержки — создание и управление множеством независимых каналов с уникальными настройками, что обеспечивает параллелизм, минимизирует очереди и значительно повышает общую производительность и отзывчивость системы. Эти знания и наработки особенно актуальны для современных масштабируемых баз данных и микросервисных архитектур, где эффективность взаимодействия компонентов напрямую влияет на качество и быстродействие сервисов.