Современные распределённые системы всё чаще используют gRPC — высокопроизводительный фреймворк удалённого вызова процедур поверх HTTP/2, широко признанный за надёжность и эффективность межсервисного взаимодействия. Однако при тестировании производительности YDB, открытой распределённой SQL базы данных, разработанной для обеспечения высокой доступности и строгой согласованности, была выявлена неожиданная проблема. Несмотря на близость серверов и высокоскоростное соединение, латентность на стороне клиента возрастала с уменьшением размера кластера, в то время как вычислительные ресурсы оставались недозагруженными. Анализ выявил клиента gRPC как узкое место системы, оказывающее существенное влияние на общую производительность и отклик. Такое поведение граничит с парадоксом: чем меньше серверных узлов, тем выше задержка и ниже эффективность, что напрямую противоречит распространённым ожиданиям.
Чтобы понять корни проблемы, важно выяснить особенности внутреннего устройства gRPC. В основе существуют каналы (channels), через которые множественные RPC-вызовы (стримы) мультиплексируются на уровне одного HTTP/2 соединения. Ключевой аспект — ограничение на число одновременных потоков в одном TCP соединении, обычно установленное на уровне 100. Стоит отметить, что если создавать каналы с одинаковыми параметрами, гRPC часто объединяет их в одну TCP сессию, что приводит к конкуренции потоков и очередям на отправку вызовов из-за установленного лимита. На практике в YDB используется подход с отдельным каналом на каждый клиентский рабочий поток, что должно было бы оптимизировать нагрузку.
Тем не менее наблюдалось значительное ограничение пропускной способности. Для диагностики была разработана микро-бенчмарка на C++, использующая асинхронный API gRPC для сервера и синхронный для клиентов. Запуск проводился на мощных серверах с двумя процессорами Intel Xeon Gold, связанными сетью 50 Гбит/с, с минимальным RTT порядка 40 микросекунд, что обеспечивало почти идеальные условия для низкой задержки. Результаты микро-бенчмарка показали тревожную картину: при увеличении количества параллельных запросов (in-flight) прирост пропускной способности оказывается далеким от линейного, а задержка растёт практически линейно с числом рабочих потоков. При этом имел место факт использования только одного TCP соединения, что создавало очереди запросов и фактически ограничивало производительность.
Анализ сетевого трафика через Wireshark исключил сетевые задержки, проблемы с TCP-конгестией или настройками вроде Nagle и delayed ACKs, а сервер демонстрировал быструю и стабильную реакцию. Основная причина оказалась лежать внутри клиентской реализации gRPC: мультиплексирование многочисленных RPC вызовов в одном соединении приводит к внутреннему батчингу и ожиданию, вследствие чего происходит «простой» на клиентской стороне между отправкой и получением данных, увеличивая latency и ухудшая throughput. Опережающее ожидания решение проблемы заключалось в создании для каждого клиентского рабочего потока отдельного gRPC канала с индивидуальными параметрами, что приводило к установлению уникального TCP соединения для каждого канала. Это позволяло избежать ограничений на максимальное число потоков и увеличить параллелизм. Альтернативно, включение параметра GRPC_ARG_USE_LOCAL_SUBCHANNEL_POOL, дающего аналогичный эффект локального пула каналов, полностью устраняло проблему клиентского узкого места.
В итоге такой подход позволил добиться многократного увеличения пропускной способности — в опытном тестировании почти шестикратного роста для простых RPC и около четырех с половиной раз для стриминговых вызовов, при этом задержка вынужденно не увеличивалась по мере масштабирования нагрузки. Стоит подчеркнуть, что данное решение особенно эффективно и заметно лишь в условиях низколатентных сетей и высокой пропускной способности: в сетях с RTT порядка нескольких миллисекунд эффект клиента gRPC не проявляется столь явственно, и мультиканальный режим вносит лишь незначительное улучшение. Это связано с тем, что при высокой сетевой задержке основным фактором становится сетевой RTT, а не внутренние механизмы мультиплексирования gRPC. Полученные результаты имеют важное значение для разработки и эксплуатации высоконагруженных систем с применением gRPC. Часто внимание уделяется оптимизации серверной части, масштабированию инфраструктуры и сетевых ресурсов, но именно клиентская сторона может стать критическим узким местом при работе в условиях низкой латентности и высокой частоты запросов.
Для разработчиков и архитекторов систем важно учитывать такие ограничения и при необходимости проектировать соответствующие схемы взаимодействия: использовать собственные каналы с уникальной конфигурацией для каждого активного потока, а также выделять часть ресурсов на управление каналами, что позволяет достичь желаемой масштабируемости. Важно помнить, что сами рекомендации официальной документации gRPC, советующие создавать отдельные каналы для разных областей нагрузки или применять пул каналов с уникальными ключами, в реальности являются не альтернативными вариантами, а частью единого решения. Использование этих рекомендаций совместно даёт существенные преимущества и помогает избежать неожиданных деградаций. Далее современные приложения и базы данных с использованием gRPC могут применять результаты данной работы для более эффективного управления нагрузкой, особенно при снижении количества серверных узлов, что зачастую возникает из-за прекращения ненужных инстансов или обновления инфраструктуры. Благодаря предварительно описанным подходам можно гарантировать сохранение высокой производительности клиент-серверного взаимодействия и адекватное использование ресурсов.
В перспективе остаётся открытым вопрос поиска дополнительных оптимизаций на уровне самого gRPC, а также изучение влияния данной проблемы на различные языковые реализации и сценарии с длительными стриминговыми соединениями. Приглашается сообщество разработчиков вносить свой вклад и делиться опытом, что будет способствовать росту производительности масштабируемых распределённых систем. Наконец, выявление и устранение подобного рода неожиданного узкого места — наглядный пример того, что зачастую главной преградой на пути к высокой эффективности оказывается именно нематериальный клиентский софт и протоколы взаимодействия, а не железо или сеть. Глубокий анализ и экспериментальный подход помогают обнаруживать такие неочевидные эффекты, закладывая прочную основу для будущих достижений в области распределённых баз данных и сервисов с низкой задержкой.