gRPC давно завоевал популярность среди разработчиков благодаря своей производительности и надёжности как платформа для межсервисного взаимодействия. Используемый во множестве масштабируемых распределённых систем, gRPC построен поверх протокола HTTP/2 и позволяет эффективно управлять большим числом параллельных запросов с минимальной задержкой. Однако, несмотря на эти преимущества, в определённых условиях, особенно при работе в сетях с очень низкой задержкой, был обнаружен неожиданный узкий профиль на стороне клиента gRPC, способный существенно ограничивать производительность и увеличивать время отклика приложений. В данной статье мы подробно рассмотрим суть проблемы, её причины и предложим проверенные решения, способные значительно улучшить скорость и стабильность работы gRPC клиентов в высокоскоростных средах. Основой для анализа послужил опыт команды разработчиков YDB — открытой распределённой SQL базы данных, которая применяет gRPC для общения клиентов с кластерами серверов.
В ходе нагрузочного тестирования и бенчмарков выяснилось любопытное явление: сокращение количества узлов кластера приводило не только к простой неэффективности, но и к росту клиентской латентности. Казалось бы, чем меньше компонентов, тем проще и быстрее должно быть выполнение запросов. Однако реальность оказалась обратной — при уменьшении количества серверов клиент сталкивался с заметно ухудшающейся пропускной способностью, при этом ресурсы самих серверов простаивали, а задержки на клиенте нарастали по экспоненте. Этот парадокс заставил глубже изучать архитектурные особенности gRPC в условиях высокой нагрузки и минимальных сетевых задержек. Для начала стоит напомнить ключевые принципы работы gRPC.
Клиент gRPC оперирует несколькими каналами (channels), каждый из которых представляет собой долговременное соединение с сервером. При этом все вызовы удалённых процедур (RPC) по умолчанию транспортируются внутри одного TCP-соединения с использованием HTTP/2, что позволяет мультиплексировать множество потоков. Аналитики YDB отметили, что каналы, созданные с одинаковыми параметрами конфигурации, по факту используют общий TCP-сокет, что не всегда очевидно. При высокой параллельной нагрузке на клиента это ведёт к ограничению пропускной способности из-за лимита на число одновременно активных потоков HTTP/2, который по умолчанию составляет около 100. Когда этот лимит исчерпывается, следующие запросы на стороне клиента ставятся в очередь и ждут освобождения используемых потоков, что вызывает возникновение задержек.
В официальной документации gRPC рекомендуется два варианта обхода подобного узкого места: создавать отдельные каналы для участков приложения с высокой нагрузкой либо использовать пул каналов, каждый из которых имеет отличающиеся параметры, чтобы избежать объединения их через один TCP-сокет. Опыт YDB показал, что эти рекомендации на деле не разделены жёстко, а вместе представляют комплексный подход к решению проблемы. Проверка гипотезы осуществлялась с помощью собственного микро-бенчмарка — простого пинга на базе gRPC, написанного на C++. Этот тест состоял из клиентской и серверной части, запускаемых на разных физических машинах, соединённых 50 Гбит/с сетью с минимальными задержками в несколько десятков микросекунд. Такая среда идеальна для выявления внутренних узких мест клиентов, поскольку сетевые задержки минимальны и не искажают результаты.
Результаты показали явное отставание от теоретической линейной масштабируемости: при увеличении числа одновременных запросов производительность росла, но гораздо медленнее ожидаемой и с ростом количества параллельных запросов медианные и пиковые задержки значительно увеличивались. Анализ сетевого трафика с помощью tcpdump и Wireshark позволил исключить сетевые проблемы: не было обнаружено задержек из-за перегрузки каналов или механизмов TCP, таких как отложенные подтверждения или влияние агрегации Nagle. Главным источником дополнительной задержки оказался клиент gRPC, точнее внутренняя его логика, связанная с тем, что весь трафик от параллельных воркеров клиента мультиплексировался по одному TCP-соединению. После обработки сервером очередного пакетного ответа по всем потокам наблюдалась пауза порядка 150-200 мкс, прежде чем клиент инициировал следующий раунд запросов. Несмотря на минимализм теста и отсутствие нагрузки на саму бизнес-логику, этот промежуток оказывался доминирующей причиной роста латентности.
Эксперименты с созданием отдельных каналов для каждого клиента подтверждали, что если каналы создаются с одинаковыми параметрами, то по-прежнему используется общий TCP-сокет, и это не улучшает ситуацию. Зато при добавлении пользовательских аргументов к каждому каналу, заставляющих gRPC откладывать обмен по отдельным TCP-соединениям, производительность значительно выросла. Аналогично включение параметра GRPC_ARG_USE_LOCAL_SUBCHANNEL_POOL, отвечающего за изоляцию каналов, эффективно устраняло проблему задействованности единственного соединения и замедления. Переход на многопоточный клиент с наборами независимых каналов по сути решает узкое место, позволяя параллельно обслуживать сотни потоков без блокировок и очередей на уровне соединений HTTP/2. Оптимизация приводит к росту пропускной способности примерно в 6 раз по сравнению с одно соединением и значительно снижает задержки при увеличении числа параллельных запросов.
Интересно, что в сетях с задержкой порядка нескольких миллисекунд подобные улучшения имеют меньшее значение, поскольку сетевая инерция доминирует на уровне клиент-серверного раунда. Значительный выигрыш достигается именно в условиях сверхбыстрых соединений с минимальным RTT. Резюмируя, выявленное узкое место gRPC клиента связано с особенностями мультиплексирования HTTP/2 при использовании единственного TCP-соединения, ограничением на количество параллельных потоков и внутренней организацией каналов в gRPC. Для его устранения эффективным решением является создание наборов каналов с уникальными параметрами, что позволяет задействовать несколько TCP-соединений и уменьшить конкуренцию за системные ресурсы. Это открытие очень важно для всех, кто строит производительные распределённые приложения с использованием gRPC в условиях высокоскоростных инфраструктур.