В мире современных распределённых систем и масштабируемых баз данных gRPC занимает особое место как высокопроизводительный и надёжный протокол для общения между сервисами. Однако при работе в сетях с низкой задержкой, таких как современные дата-центры с гигабитными соединениями, разработчики могут столкнуться с неожиданными проблемами производительности, связанными не с сетью или серверной стороной, а именно с клиентской реализацией gRPC. В этой статье мы подробно разберём природу данного узкого места, расскажем о способах его воспроизведения и предложим эффективные методы обхода проблемы, позволяющие достичь одновременно высокой пропускной способности и низкой задержки в работе с gRPC. Современный взгляд на gRPC gRPC построен на HTTP/2 и используется для эффективной межсервисной коммуникации благодаря поддержке мультиплексирования потоков, строгой типизации с использованием Protocol Buffers и встроенной поддержке безопасности. При этом клиентская часть gRPC обычно открывает один или несколько каналов (channels), каждый из которых устанавливает TCP-соединение и служит для передачи множества RPC-запросов (streams).
По умолчанию в gRPC существует ограничение на максимальное число одновременных потоков в одном HTTP/2 соединении, которое часто равняется 100. Когда этот лимит достигается, дополнительные запросы на стороне клиента попадают в очередь ожидания до освобождения потоков. Такая особенность может привести к узким местам при высоких нагрузках или длительных потоках, особенно если все запросы идут по одному каналу. Практика показывает, что правильная организация каналов и настройка параметров — ключевой фактор в достижении оптимальной производительности. Многие руководства рекомендуют создавать отдельные каналы для разных частей нагрузки или использовать пул каналов с различными настройками, чтобы избежать сужения из-за очередей ожидания.
Появление проблемы в YDB В высокопроизводительной распределённой базе данных YDB, построенной с использованием gRPC для взаимодействия с клиентами, команда столкнулась с неожиданной ситуацией. Уменьшение числа серверных узлов в кластере приводило к накоплению неиспользуемых ресурсов на серверах и одновременно к увеличению задержек по клиентской стороне. Танец парадоксов заставил заняться углублённым исследованием клиентской реализации. Первоначально казалось, что снижение производительности связано с серверной частью или сетью, но доскональный анализ показал отсутствие проблем с сетью — пакеты передавались без потерь, с минимальными задержками, на TCP-уровне отсутствовали задержки на подтверждения и фрагментацию. Задержка появлялась на стороне клиента, между получением подтверждения и отправкой следующего запроса, что указывало на внутреннюю сериализацию или блокировки внутри gRPC клиента.
Эксперимент с микро-бенчмарком Для изучения проблемы был разработан простой микро-бенчмарк, реализующий gRPC сервер и клиента, поддерживающих как синхронные, так и асинхронные вызовы. Тесты проводились на двух мощных физических машинах с процессорами Intel Xeon и сетью 50 Гбит/с с низкой задержкой. Клиентские запросы имитировали «пинг» без нагрузки полезной нагрузки (payload), что позволяло минимизировать влияние бизнес-логики и сфокусироваться на сетевом стеке и самом протоколе. Измерения показали, что при росте числа одновременных активных запросов (in-flight requests) масштабирование по пропускной способности далеко от идеального — на порядке величины добавление клиентов даёт только минимальный рост по скорости. Задержка же росла практически линейно с ростом активности, что свидетельствовало о том, что gRPC клиент не позволяет эффективно использовать доступную пропускную способность сети.
Анализ трафика с помощью tcpdump и Wireshark подтвердил, что основной задержкой была именно пауза на клиентской стороне между получением ответа и отправкой следующего запроса. Это вызвало подозрение на внутренние механизмы батчинга или конкурентного доступа к ресурсам в клиенте. Почему один канал — это узкое место Глубокое погружение в клиентскую архитектуру gRPC выявило, что при использовании одного канала с множеством потоков, все запросы на самом деле отправляются по одному TCP-соединению, где одновременно существует ограничение на количество активных потоков. Более того, даже при создании нескольких каналов с одинаковыми параметрами, gRPC достаточно часто делает оптимизацию, объединяя трафик в один TCP поток (с помощью локального пула субканалов). Это приводит к тому, что параллелизм по сути не достигается и запросы подвергаются очередям и очередным замедлениям.
Решение — разные настройки каналов и собственные подключения Ключевой способ избавиться от узкого места — заставить gRPC клиента создавать действительно отдельные TCP-соединения, а не объединять трафик в пул. Для этого каналы должны отличаться по параметрам, либо должна быть явно отключена локальная агрегация каналов, например, установка аргумента GRPC_ARG_USE_LOCAL_SUBCHANNEL_POOL в false. Таким образом каждый поток клиента получает свой канал, и каждый канал — своё TCP-соединение. Это снижает эффект очередности и блокировок, даёт существенный прирост производительности. Запуск клиентов с разными параметрами каналов и режимами потоков показал впечатляющие улучшения: пропускная способность выросла до 6 раз для обычных RPC и около 4.
5 раза для стриминговых вызовов, при этом рост задержки с увеличением нагрузки стал минимальным. Это демонстрирует, что узкое место действительно было связано со способом организации каналов и соединений на клиенте. Как сетевые условия влияют на проблему Дополнительные эксперименты с сетью, где задержка возросла до 5 миллисекунд, показали, что при увеличенных задержках эффект узкого места проявляется не так сильно. В таких случаях время ожидания в сети доминирует над внутренними задержками клиента, и обе версии клиента — с одним каналом или с мультиканальным пулом — показывают близкие результаты. Это подчёркивает, что выявленная проблема критична именно для очень низколатентных сетей modern дата-центров и не проявляется в условиях более «традиционных» WAN.
Практические советы и рекомендации Для разработчиков и инженеров, использующих gRPC в высокопроизводительных системах, важно уделять внимание именно клиентской архитектуре подключения. Создание каналов с уникальными параметрами и явное управление пулом каналов может оказаться решающим для извлечения максимума из вычислительных мощностей и сетевой пропускной способности. Также стоит применять инструменты профилирования и мониторинга, чтобы вовремя обнаруживать подобные узкие места, которые зачастую не заметны при стандартных нагрузках или при работе через обычные сети с большим временем отклика. Дополнительные оптимизации и исследования Хотя описанное решение значительно улучшает ситуацию, это не означает, что gRPC достиг оптимума. Возможно, существуют внутренние механизмы реализации gRPC, которые могут быть оптимизированы для лучшей поддержки высококонкурентного обмена по одному TCP соединению.