Современные распределённые системы и масштабируемые базы данных часто предъявляют крайне высокие требования к скорости обмена данными между компонентами. gRPC, как одна из ведущих платформ для межсервисного взаимодействия, обеспечивает надёжное и эффективное проведение удалённых вызовов процедур, основываясь на протоколе HTTP/2. Однако в условиях сетей с очень низкой задержкой оказывается, что производительность клиента gRPC далеко не всегда соответствует ожиданиям, и скрытые узкие места могут существенно ограничивать общую пропускную способность системы. В данной статье сделан акцент на неожиданную проблему, встречающуюся именно на стороне клиента gRPC, а также предложены пути её обхода и решения. YDB — это открытокодная распределённая SQL-база данных, ориентированная на сочетание высокой доступности и масштабируемости с поддержкой строгих гарантий согласованности и ACID-транзакций.
В этой системе gRPC используется в качестве интерфейса API для взаимодействия с клиентами. В процессе тестирования нагрузки и проведения стресс-тестов была замечена нелогичная зависимость: при уменьшении числа узлов кластера нагрузка на его ресурсы сокращалась, а количество простаивающих ресурсов росло, но при этом латентность запросов на стороне клиента неуклонно увеличивалась. Тщательное исследование выявило непосредственную причину — узкое место, связанное именно с клиентской частью gRPC. Такой феномен вызвал необходимость углублённого анализа и поиска путей повышения производительности без изменения конфигурации сервера. Говоря о gRPC, важно понимать его архитектурные основы.
gRPC построен поверх протокола HTTP/2, который поддерживает мультиплексирование — одновременное выполнение нескольких запросов и ответов через один TCP-соединение. Клиент gRPC может создавать множество каналов связи (channel), причём каждый канал может работать за одним или несколькими TCP-соединениями. При этом открытые каналы с одинаковыми параметрами могут использовать общее соединение, что не всегда очевидно и может создавать неожиданные ограничения. Основной проблемой в рассматриваемом случае оказалось то, что стандартный лимит на число одновременно активных потоков (RPC-streams) по одному HTTP/2 соединению по умолчанию равен 100. Если число конкурентных запросов достигает этого лимита, последующие запросы начинают ставиться в очередь на клиентах — это вызывает дополнительное ожидание и накопление задержек.
Таким образом поток запросов и ответов оказывается ограничен по количеству параллельных коммуникаций, что принципиально сказывается на производительности при высоких нагрузках в условиях низкой сетевой латентности. Согласно рекомендациям официальной документации gRPC, существует несколько путей смягчения этой проблемы. Во-первых, создание нескольких каналов с разными аргументами и распределение запросов между ними. Второй вариант — использование пула каналов для балансировки нагрузки и увеличения количества TCP-соединений. В реальных условиях обе эти практики рассматриваются как две градации одного подхода к масштабированию клиента.
Для экспериментального подтверждения и исследования проблемы была разработана простая микро-бенчмарковая система — grpc_ping_server и grpc_ping_client. Последний создан с использованием как синхронного, так и асинхронного API gRPC, позволяя гибко тестировать производительность с разным количеством одновременных воркеров и вариантов реализации каналов. Сервер и клиент запускались на двух выделенных серверах, оснащённых современными многоядерными процессорами Intel Xeon и высокой пропускной сетью со скоростью 50 гигабит в секунду, что обеспечивает минимальные ограничения со стороны аппаратных ресурсов и сети. Реальные измерения показали, что при работе с одним TCP-соединением и небольшим количеством одновременных запросов производительность далеко не достигала теоретически возможных значений, которые рассчитывались пропорционально числу запросов в полёте (in-flight). Латентность росла почти линейно с увеличением количества параллельных запросов, и даже при единичном числе in-flight запросов задержки были значительно выше, чем физическая задержка сети.
Анализ трафика с помощью сетевых инструментов выявил отсутствие проблем с сетью — отсутствовали задержки ACK, окна TCP были оптимальны, режим Nagle был отключён. При этом наблюдалась регулярная пауза около 150–200 микросекунд между передачей пакетов, что на порядок превышало минимально возможное время в высокоскоростной сети. При тщательном исследовании оказалось, что все запросы и ответы с множества потоков клиента фактически мультиплексировались по единственному соединению, что приводило к внутреннему бэтчингу и заторам на стороне клиента gRPC. Испытания с отдельными каналами на каждый рабочий поток без использования различных аргументов не улучшили ситуацию — все каналы по-прежнему шарили одно TCP-соединение, а ограничение одновременных потоков оставалось проблемой. Только при назначении уникальных для каждого канала аргументов или при включении специального параметра GRPC_ARG_USE_LOCAL_SUBCHANNEL_POOL удалось добиться распределения TCP-соединений и заметно повысить пропускную способность, одновременно снизив задержки.
Итоговые замеры на многоканальной версии клиента продемонстрировали рост пропускной способности почти в 6 раз для классических RPC и более чем в 4 раза при потоковом режиме. Латентность при этом увеличивалась гораздо медленнее с ростом числа одновременных запросов, что свидетельствовало об эффективном устранении клиентской блокировки. Интересно, что при тестах в сети с задержкой порядка нескольких миллисекунд — типичной для географически удалённых дата-центров — проблемы практически не возникали, и многоканальная архитектура давала лишь невысокий прирост производительности при больших нагрузках. Это подтверждает, что узкое место проявляется именно в условиях низкой сетевой латентности, где внутренние задержки клиента становятся заметным фактором. Выводы, которые можно сделать из этого опыта, заключаются в том, что рекомендации по использованию нескольких каналов оказываются не просто разными вариантами решения, а по сути совместимыми этапами одной комплексной оптимизации для клиента gRPC.
Настройка отдельных каналов с уникальными параметрами или применение локального пула субканалов позволяет обходить ограничения HTTP/2 и минимизирует внутренние задержки, что критически важно для систем с высокими требованиями к низкой задержке и высокой пропускной способности. При этом следует отметить, что данное исследование затрагивает лишь один из ключевых оптимизационных аспектов клиента gRPC, и возможны дополнительные улучшения, связанных с низкоуровневой оптимизацией библиотек, алгоритмами обработки вызовов и распределением ресурсов на уровне операционной системы. Специалисты и разработчики высокопроизводительных распределённых систем могут проводить дальнейшие эксперименты и вносить свой вклад, расширяя набор лучших практик и улучшая экосистему gRPC. Подводя итог, стоит подчеркнуть, что грамотное использование возможностей многоканальной архитектуры клиента gRPC — ключевой фактор для устранения неожиданных узких мест в условиях современных высокоскоростных сетей. Своевременные диагностика, понимание тонкостей протокола HTTP/2, а также корректная конфигурация каналов связи позволяют добиться максимальных показателей производительности и обеспечить плавную масштабируемость распределённых решений, таких как YDB и аналоги.
Такой подход помогает избежать неэффективных затрат ресурсов и простаивания аппаратных мощностей при максимальных нагрузках, что, в конечном итоге, повышает устойчивость и отзывчивость современных IT-инфраструктур.