gRPC — современный и популярный фреймворк для удалённого вызова процедур, который активно используется для межсервисной коммуникации в распределённых системах и крупных корпоративных проектах. Благодаря своей надстройке над протоколом HTTP/2, gRPC обеспечивает эффективную передачу сообщений с высокой производительностью, что идеально подходит для масштабируемых архитектур и высоконагруженных приложений. Однако в условиях сетей с низкой задержкой и высокой пропускной способностью порой могут возникать непредвиденные проблемы, связанные с производительностью самой клиентской части gRPC. Появляется узкое место, которое ограничивает возможности всей системы и снижает эффективность обращения к серверу. Исследования и тесты, проведённые на примере открытой распределённой СУБД YDB, выявили любопытный феномен — чем меньше узлов в кластере сервера, тем тяжелее нагрузочным тестам (бенчмаркам) добиться ожидаемой производительности, а также наблюдается неадекватно высокий рост задержек на клиенте.
Такая ситуация была очевидным признаком того, что источником проблемы является не сеть или сервер, а именно клиентская часть gRPC. Протокол gRPC построен на HTTP/2, который допускает мультиплексирование нескольких потоков на одном TCP-соединении. Каждое gRPC-подключение (канал) работает поверх TCP, предоставляя единый канал связи между клиентом и сервером. В официальных рекомендациях к gRPC упоминается, что максимальное количество одновременных потоков на одно подключение обычно ограничивается значением около 100, и при достижении этого лимита новые запросы начинают ставиться в очередь и ожидают завершения активных вызовов. В ситуации с маленьким количеством каналов и многопоточными нагрузками возникает эффект внутренней очереди, которая существенно сужает пропускную способность при высоких нагрузках.
На практике это означает, что если все запросы направляются через один канал, то при большом числе одновременных вызовов клиент начинает испытывать задержки, так как «конвейер» TCP/HTTP/2 оказывается заполненным и не способен эффективно обрабатывать дополнительный трафик. В YDB для воспроизведения и анализа проблемы был создан специализированный микробенчмарк — grpc_ping, состоящий из сервера и клиента, написанных на C++ с использованием последней версии gRPC. Тесты проводились на специализированных серверах с двухпроцессорными Intel Xeon Gold 6338, подключённые через 50 Гбит сети с низкой задержкой порядка нескольких десятков микросекунд. Тестирование позволило выявить характерные профили задержек и пропускной способности при различном числе параллельных запросов (in-flight). Результаты показали, что при увеличении числа одновременных запросов общая производительность растёт не линейно, а с меньшей скоростью, при этом задержки на клиенте начинают расти значительно быстрее, чем физические задержки сети.
Главной загадкой стала ситуация, когда при фактическом количестве TCP соединений ровно одного, все параллельные запросы отправляются по одной и той же цепочке, мешая друг другу, и создавая очередь на клиенте. Неоднократные попытки использовать пуллинг каналов с одинаковыми параметрами приводили к тому, что все каналы повторно использовали одно и то же TCP соединение, не давая прироста производительности. Выход из этой ситуации найден был в создании отдельных gRPC каналов с уникальными параметрами или включении специального флага GRPC_ARG_USE_LOCAL_SUBCHANNEL_POOL, который заставляет gRPC создавать отдельные субканалы вместо общего пула. Такая реализация позволила добиться значительного улучшения — в 4.5-6 раз выросла пропускная способность, а задержки перестали расти.
Особенно важно отметить, что при увеличении нагрузки рост задержек стал почти минимальным, что говорит о существенном снижении узкого места на клиенте. При испытаниях в сетях с более высокой задержкой (около 5 миллисекунд) разница между вариантом с одним TCP соединением и множественными соединениями не является критичной — прирост при использовании мультиканалов небольшой. Это указывает на то, что описанное узкое место особенно заметно именно в высокоскоростных сетях с очень низкой задержкой, где накладные задержки на самом клиенте становятся главным ограничением. Для разработчиков и системных архитекторов, использующих gRPC в высокопроизводительных системах, ключевым выводом является то, что применение нескольких gRPC каналов с уникальными параметрами или активация локальных пулов субканалов существенно улучшает баланс между максимальной пропускной способностью и минимальной задержкой. Это особенно актуально для нагрузочных тестов, микросервисов с большими пичами параллелизма и систем, работающих в условиях очень быстрой и стабильной сетевой инфраструктуры.
Помимо решения описанной проблемы, результаты поднимают важный вопрос оптимизации работы gRPC клиентов и указывают на необходимость более детальной настройки каналов и их параметров с учётом реальных условий эксплуатации. Возможна также дальнейшая оптимизация внутреннего механизма gRPC, направленная на снижение контеншина и ожиданий внутри клиентских очередей. Поиском новых путей улучшения производительности активно занимаются разработчики YDB и сообщества вокруг gRPC, а открытый характер микробенчмарка приглашает всех желающих принять участие в развитии и совершенствовании технологий. В итоге, опыт YDB показывает: совершенствование производительности распределённых систем требует понимания не только серверных, но и клиентских аспектов. В проектах с использованием gRPC важно внимательно следить за параметрами каналов подключения, чтобы избежать неожиданных узких мест и максимально эффективно использовать возможности современных высокоскоростных сетей.
Такой подход способствует созданию более отзывчивых, масштабируемых и надёжных систем, способных выдерживать высокий нагрузочный поток и обеспечивать требуемый уровень качества обслуживания.