gRPC – это современный инструмент для организации высокопроизводительной удалённой процедуры вызова (RPC), активно применяемый в микросервисных архитектурах и распределённых системах. Благодаря построению на базе HTTP/2 протокола, gRPC обеспечивает надежное межсервисное взаимодействие с возможностью мультиплексирования потоков и поддержки различных языков программирования. Несмотря на все преимущества, на практике в условиях сетей с низкой задержкой нередко возникают неожиданные проблемы производительности, связанные с клиентской стороной gRPC. Одним из ключевых примеров такой ситуации стала практика использования gRPC в проекте YDB – распределенной SQL базе данных с поддержкой высокой доступности, масштабируемости и строгой согласованности. YDB реализует API через gRPC, и нагрузочные тесты, а также бенчмарки базы данных, выполняемые клиентами gRPC, выявили интересную закономерность.
При уменьшении количества узлов в кластере наблюдается прогрессивное снижение эффективности нагрузки на серверную часть. И при этом нагрузка не возрастает, несмотря на в итоге свободные ресурсы в кластере. Одновременно с этим растёт и латентность на стороне клиента. Такая ситуация была нетипична и заставила более глубоко исследовать природу возникшей проблемы. Погружаясь в детали протокола и реализации gRPC, важно понимать, что внутри клиента задействованы каналы (channels), каждый из которых в состоянии поддерживать множественные RPC-запросы посредством HTTP/2 потоков.
Каналы обеспечивают установление TCP-соединений; вообще, каналы с разными аргументами конфигурации создают отдельные TCP-подключения, но без различий в параметрах каналы вынуждены делиться единственным TCP-соединением. Это поведение, по словам специалистов YDB, оказалось неожиданным и играет ключевую роль в появлении „узкого места“. Фактически, стандартные рекомендации производительности gRPC подразумевают ограничение числом одновременно активных потоков на TCP-соединение. Обычно этот предел равен 100. Вышедшие за рамки активные RPC просто ставятся в очередь на клиенте, ожидая освобождения.
Для сценариев с высокой нагрузкой или продолжительными потоковыми связями это может стать причиной задержек. Рекомендуемые решения включают создание отдельных каналов для разных областей нагрузки и управление пулом каналов с уникальными конфигурациями, позволяющими избежать совместного использования одного TCP-соединения. В YDB задействовали подход с пер-воркер каналами, предоставляя каждому потоку загрузки отдельный gRPC-канал. Тем не менее, проблемы не ушли, пока не было обнаружено, что все каналы, созданные с одинаковыми параметрами, фактически используют одно TCP-соединение, что подрывает преимущества параллелизма. Решением стало добавление уникальных параметров к каналам либо активация опции GRPC_ARG_USE_LOCAL_SUBCHANNEL_POOL, которая заставляет gRPC не реиспользовать субканалы, заставляя открывать больше TCP-соединений и благодаря этому разгружать клиентскую часть.
Для детального изучения проблемы был создан микро-бенчмарк, написанный на C++ с использованием последней версии gRPC (v1.72.0). В тестах клиент и сервер запускались на разных физических машинах с высокопроизводительными процессорами Intel Xeon и подключением по сети 50 Гбит/с с характеристиками задержек на уровне нескольких десятков микросекунд. При использовании одного канала на клиента с разным числом одновременно исполняющихся запросов попадался парадокс: увеличение числа параллельных запросов не вели к пропорциональному росту пропускной способности, а задержки на клиенте возрастали почти линейно с нагрузкой.
Трафик анализировался с помощью tcpdump и Wireshark, что не выявило проблем с сетью: TCP-поток лишён негативных эффектов вроде задержек ACK, сглаженных данных или проблем с TCP-оконным управлением. Сервер отвечал быстро, без узких мест на своей стороне. Исследователи обнаружили характерный эффект — после отправки клиентом батчированных запросов происходил небольшой, но заметный период простоя перед следующей отправкой данных, который возникал именно на клиенте. Это свидетельствовало о наличии тормозов на стороне программного стека gRPC, предположительно связанных с блокировками и очередями запросов внутри реализации каналов. Эксперименты с пулом каналов, где каналы создавались с уникальными параметрами, показали значительный прирост производительности — увеличение пропускной способности клиента почти в 6 раз и уменьшение роста задержек при возрастании числа запросов.
Особенно эффективно это проявлялось при выполнении как одиночных RPC, так и потоковых вызовов. Таким образом, обнаружилась фундаментальная особенность: для достижения высокой производительности в условиях низкой сетевой задержки необходимо на стороне клиента реализовать многоканальное взаимодействие с сервером, чтобы равномерно распределить нагрузку и устранить очереди внутри одного TCP-соединения. Проверка того же подхода в сети с типичной задержкой 5 мс показала, что эффект от использования нескольких каналов менее значителен. Здесь сетевые задержки доминируют, и клиентские внутренние задержки смазываются общей картиной. Это указывает на то, что выявленное узкое место актуально именно для сред с низкой задержкой — например, для дата-центров, высокоскоростных сетей или облачных инфраструктур.
Результаты, полученные командой YDB, имеют важное значение не только для пользователей gRPC в распределённых системах, но и в целом для понимания архитектуры эффективных клиентских приложений, взаимодействующих через RPC. В первую очередь, рекомендуется проектировать клиентские приложения с учётом необходимости создавать множество gRPC-каналов с уникальными параметрами конфиурации или активировать соответствующие флаги, позволяющие избежать подписок на один TCP-сокет. Это существенно повышает масштабируемость и снижает латентность. В заключение стоит отметить, что открытость к экспериментам и внимательный подход к профилированию являются критическими аспектами в разработке высоконагруженных распределённых сервисов. Учитывая, что gRPC продолжает активно развиваться и становится стандартом для межсервисного взаимодействия, понимание таких нюансов позволит создавать более надёжные и производительные системы.
Более того, доступный в открытом доступе и активно поддерживаемый микро-бенчмарк от YDB предоставляет платформу для дальнейших исследований и оптимизаций, в том числе и в других языках программирования. Общая тенденция показывает, что даже продвинутые технологии могут иметь скрытые ограничения в экстраординарных условиях, и устранение таких «узких мест» требует системного анализа и тщательного тестирования. Для разработчиков и инженеров, использующих gRPC в своих проектах, ключевым выводом является необходимость контроля над количеством TCP-соединений и их конфигурацией, что напрямую влияет на конечную производительность и отзывчивость приложений, особенно в условиях низкой сетевой задержки и высоких требований к скорости обработки запросов.