gRPC стал стандартом де-факто для коммуникации между сервисами благодаря своей производительности и удобству. Однако в современных распределённых системах, работающих в сетях с очень низкой задержкой, разработчики и инженеры зачастую сталкиваются с неожиданными ограничениями клиентской части gRPC, которые снижают общую эффективность. Интересно, что несмотря на хорошую производительность протокола HTTP/2, лежащего в основе gRPC, и высокоскоростные сети, пропускная способность может быть значительно меньше, чем ожидалось, а задержка – значительно выше. В данной статье рассматривается выявленное узкое место на стороне клиента gRPC и предлагаются проверенные способы его преодоления для достижения высокого вcпроизводства и низкой латентности. В основе gRPC лежит HTTP/2, позволяющий multiplex-ить несколько потоков в рамках единого TCP-соединения.
При этом каждый gRPC-канал использует один или несколько HTTP/2-соединений. В официальной документации gRPC указано, что существуют ограничения на количество одновременно активных потоков на одном HTTP/2-соединении — обычно около 100. Когда это ограничение достигается, новые RPC просто ставятся в очередь на клиентской стороне, что приводит к задержкам и снижению производительности. Для повышения производительности описывается два основных подхода. Первый — создание отдельных каналов для каждой области высокого нагрузки, что позволяет избежать узкого места в одном соединении.
Второй — организация пула каналов, каждый с уникальными параметрами, чтобы gRPC не переиспользовал один TCP-сокет, а распределял нагрузку между несколькими соединениями. Но на практике оказалось, что эти методы неразрывно связаны и являются частями одного целого решения. Компания YDB, разработчик открытой распределённой SQL-базы данных с поддержкой строгой согласованности и транзакций ACID, столкнулась с проблемой при нагрузочном тестировании кластера через gRPC. Чем меньше узлов в кластере, тем сложнее было загрузить систему, при этом ресурсы на сервере оставались неиспользованными, а клиентская задержка росла. Для исследования причины была реализована простая микро-бенчмарка с gRPC на C++, где каждый клиент работал с собственным каналом и выполнял вызовы через синхронный API.
Тестирование проводилось на выделенных серверах с Intel Xeon Gold 6338 и сетью 50 Гбит/с с очень низкой задержкой около 40 мкс. В сценарии ожидалось, что пропускная способность должна расти линейно с увеличением числа одновременных запросов (in-flight), однако данные резко отклонялись от идеального роста, показывая ухудшение до 3-4-кратного прироста при увеличении нагрузки в 10-20 раз, а задержка росла почти пропорционально количеству клиентов. Дальнейший анализ tcpdump и сетевого трафика не обнаружил проблем в сетевом стеке: не было задержек, проблем с окнами TCP или пакетными потерями. Все указывало на то, что узкое место находится в самом gRPC клиенте. Подробное изучение выявило, что все запросы по умолчанию шли по одному и тому же TCP-соединению, где HTTP/2 мультиплексировал запросы, и это являлось основным фактором их задержки.
Клиентская часть в моменты отсутствия данных на сокете простаивала 150-200 микросекунд, что сказывалось на общей производительности. Эксперименты с различными подходами к организации каналов gRPC показали, что создание отдельных каналов с идентичными параметрами не устраняет проблему, так как gRPC по-прежнему агрегирует каналы на одном TCP-соединении. Лишь задание уникальных параметров (channel args) или включение опции GRPC_ARG_USE_LOCAL_SUBCHANNEL_POOL приводит к созданию нескольких TCP-соединений и переключению нагрузки между ними. При таком подходе наблюдался значительный рост пропускной способности — до 6 раз для обычных RPC и около 4.5 раза для стриминговых запросов, при этом задержка увеличивалась очень медленно с ростом количества одновременных запросов.
Для полноты экспериментов авторы также продемонстрировали, что в сетях с более высокой латентностью порядка 5 миллисекунд обычно нет ощутимых проблем с узким местом на клиенте gRPC. В таких условиях преимущество многоканального подхода снижается и становится заметно лишь при большой нагрузке с высоким числом in-flight запросов. Выводы, сделанные на базе результатов, позволяют глубже понять, что узкое место gRPC клиента в высокопроизводительных системах связано с поведением HTTP/2 и особенностями управления соединениями и многопоточностью на клиенте. Простое масштабирование количества потоков или запросов в одном соединении не приводит к ожидаемому улучшению, вместо этого вносится дополнительная задержка из-за внутреннего контекста и синхронизации в gRPC. Для практиков, работающих с gRPC в условиях низкой сетевой задержки и высоких требований к пропускной способности, важно осознавать, что настройка и организация каналов — это не просто вопрос оптимизации, а ключевой фактор работы всей системы.
Использование нескольких каналов с уникальными конфигурациями, либо явная активация локального пула подсоединений, позволяет снизить внутреннюю конкуренцию и увеличить фактическую производительность коммуникаций. Стоит также отметить, что оптимальная многоканальная конфигурация должна учитывать особенности железа и сетевой среды. Например, закрепление потоков и каналов на ядра процессора с помощью инструментов вроде taskset и соблюдение принципов работы с NUMA могут дополнительно повысить стабильность и минимизировать задержки. Не менее важно тестировать систему с реалистичной нагрузкой и анализировать метрики задержки на разных перцентилях, чтобы выявлять истоки проблем. Несмотря на выявленное решение, разработчики сообщества gRPC и пользователей продолжают искать пути дальнейшего улучшения.
Возможно, будущие версии библиотеки позволят более гибко управлять соединениями и потоками. Кроме того, использование альтернативных подходов или протоколов может быть оправдано в критически чувствительных средах. В целом, история с gRPC клиентским узким местом служит напоминанием, что даже проверенные технологии могут проявлять неожиданные ограничения в специфичных условиях, и только тщательное измерение, анализ и эксперименты помогут достичь оптимальной производительности. Для инженеров, работающих с распределёнными базами данных, микросервисами и системами реального времени, понимание подобных тонкостей становится ключевым элементом успеха. Если вы занимаетесь разработкой высоконагруженных систем и используете gRPC, стоит обратить внимание на организацию клиентских каналов и мониторинг метрик производительности.
Такой подход не только улучшит текущую систему, но и поможет подготовиться к будущему росту нагрузки без неожиданных потерь в скорости и увеличения задержек. Кроме того, сотрудничество с сообществом и внесение своих наблюдений и улучшений в библиотеки высокого уровня — отличный способ внести вклад и сделать gRPC ещё лучше для всех. Таким образом, правильное управление каналами и использование мультиканальных стратегий является эффективным способом устранить узкое место на стороне клиента gRPC в сетях с низкой задержкой, что обеспечивает более плавную и быструю работу распределённых сервисов и баз данных.