gRPC стал одним из наиболее популярных фреймворков для межсервисного взаимодействия благодаря своей производительности, гибкости и масштабируемости. Он построен на базе протокола HTTP/2, что позволяет эффективно multiplexировать несколько запросов в пределах одного TCP-соединения. Однако, несмотря на свою репутацию надежного и быстрого инструмента, в реальных сценариях работы с сетями с низкой задержкой разработчики сталкиваются с неожиданными ограничениями и узкими местами, снижающими производительность и увеличивающими задержки на стороне клиента. Одна из таких проблем была выявлена в рамках использования gRPC в распределенной SQL базе данных YDB. Создатели системы заметили, что в конфигурациях с меньшим количеством узлов в кластере нагрузка, создаваемая клиентскими нагрузочными генераторами, значительно снижалась.
Более того, уменьшение числа серверных узлов сопровождалось увеличением простаивающих ресурсов и ростом задержек именно на стороне клиента. В итоге выяснилось, что ключевая причина — узкое место находится именно в gRPC клиенте, а конкретнее в механизме работы каналов и их взаимодействия с HTTP/2. Для понимания сущности проблемы необходимо рассмотреть устройство gRPC клиента более детально. Клиент состоит из одного или нескольких каналов, каждый из которых представляет собой логический путь для множества вызовов удаленных процедур (RPC), реализованных как потоки HTTP/2. Одно TCP-соединение может обслуживать до 100 параллельных потоков (по умолчанию лимит в gRPC).
Если количество одновременных запросов превышает этот лимит, новые вызовы начинают ставиться в очередь, что приводит к задержкам и падению пропускной способности. В официальной документации gRPC рекомендованы два основных подхода для обхода этих ограничений: создание отдельного канала для каждой активной области нагрузки и использование пула каналов с различными параметрами конфигурации для распределения запросов по нескольким TCP-соединениям. На практике оказалось, что эти два пути формируют единое комплексное решение, особенно если настроить каналы с уникальными аргументами, чтобы избежать повторного использования одного TCP-соединения. Для репликации проблемы и оценки производительности была разработана простая микробенчмарка, написанная на C++, использующая последнюю версию gRPC. В тесте реализован сервер и клиент, работающие на отдельных мощных машинах с процессорами Intel Xeon и соединением через 50 Gbps сеть с минимальной задержкой.
Эксперименты показывали, что даже с небольшим количеством параллельных запросов через один канал с одним TCP-соединением рост пропускной способности не масштабировался линейно с увеличением числа одновременно выполняемых запросов. Несмотря на минимальную задержку сети и отсутствие проблем с TCP (отсутствие потерь, отключенный Nagle, большой размер окна), наблюдалась пауза порядка 150–200 микросекунд между пакетами запросов на клиенте. Дальнейший анализ показал, что в основе проблемы лежит сам клиентский механизм gRPC, а точнее способ, которым потоки RPC отправляются, обрабатываются и ожидаются внутри одного TCP-соединения. Использование нескольких каналов с одинаковыми параметрами приводило к тому, что клиенты всё равно делили одно соединение, не разгружая узкое место. Тесты с каналами с уникальными параметрами или с включением специального аргумента GRPC_ARG_USE_LOCAL_SUBCHANNEL_POOL дали существенный прирост производительности.
Переход на многоканальную архитектуру с различающимися аргументами каналов позволил увеличить пропускную способность почти в шесть раз по сравнению с использованием одного канала. При этом задержка возросла значительно медленнее, что свидетельствовало о значительном снятии клиентского узкого места. Особенно ярко эти преимущества проявились в сетях с очень низкой задержкой — порядка десятков микросекунд. Интересно, что при подключении в сети с задержкой около 5 миллисекунд разница между одно- и многоканальной конфигурацией почти нивелировалась. Это говорит о том, что в реальных широкозоновых сетях с высокой задержкой узкое место становится менее критичным.
Но в локальных сетях и в системах с высокими требованиями к времени отклика (финансовые системы, высокочастотная торговля, распределенные базы данных) правильная настройка каналов gRPC критически важна. Помимо основной рекомендации по созданию множества каналов с уникальными параметрами, стоит учитывать и оптимизацию на стороне сервера. Использование нескольких очередей завершения (completion queues) и правильное распределение потоков обработки запросов помогают равномерно загружать CPU и улучшить устойчивость к нагрузке. Равномерное назначение потоков в пределах одной NUMA зоны увеличивает эффективность работы за счет снижения времени доступа к памяти. Практические последствия выявленной проблемы и решений могут быть весьма значительными.
Неправильная конфигурация клиентов gRPC в условиях низкой задержки способна блокировать всю систему и приводить к неэффективному использованию серверных ресурсов, что особенно критично для масштабируемых распределенных баз данных и сервисов с зафиксированным SLA по задержке. Оптимизация gRPC клиентов требует внимания к деталям настройки каналов и глубокой проверки реализации. В некоторых случаях, например при большом числе долгоживущих потоков, разумно использовать пул каналов с разными аргументами, что позволит более правильно распределить нагрузку и избежать очередей на верхнем уровне протокола. Еще один важный момент — проведение нагрузочного тестирования и измерение реальных показателей задержки и пропускной способности в условиях, максимально приближенных к боевой среде. Локальные микробенчмарки и тесты, как в примере с YDB, позволяют выявить и проанализировать узкие места на самых ранних этапах разработки и эксплуатации.
В итоге, несмотря на сложность и кажущуюся простотой архитектуры gRPC, архитектору систем и разработчику стоит помнить о существовании скрытых узких мест, влияющих на производительность. Особенно это актуально в системах с низкой задержкой сети, где влияние программных задержек на клиентской стороне становится доминирующим. Решение проблемы с помощью разделения каналов и правильной конфигурации существенно улучшает общую производительность, снижает задержки и позволяет системам более эффективно использовать свои аппаратные ресурсы. А это, в свою очередь, обеспечивает стабильную работу сервисов, удовлетворение требований конечных пользователей и снижение эксплуатационных расходов. Для сообществ разработчиков и специалистов по производительности открытым остается вопрос расширения и адаптации методов оптимизации к другим языкам и платформам, а также возможность разработать новые инструменты для автоматической диагностики и настройки gRPC клиентов.
Интересующиеся могут ознакомиться с исходным кодом микробенчмарка, опубликованным в открытом доступе, и принять участие в общем развитии платформы. Таким образом, глубокое понимание внутреннего устройства gRPC, особенностей протокола HTTP/2 и специфики сетевого взаимодействия позволяет выявлять неожиданные узкие места и эффективно их устранять, достигая оптимального баланса между пропускной способностью и задержкой. Это критически важно для построения современных, масштабируемых и производительных распределенных приложений и баз данных.