gRPC уже давно завоевал репутацию стабильного и производительного механизма для межсервисного взаимодействия, широко используемого в масштабируемых распределённых системах, таких как современные базы данных. Однако недавние исследования и практические испытания выявили неожиданный узкий коридор пропускной способности на стороне клиента, который становится особенно заметен в высокоскоростных сетях с низкой задержкой. Распознать и преодолеть его крайне важно для обеспечения максимально эффективной работы систем, использующих gRPC. Основная проблема связана с тем, что несмотря на низкую сетевую задержку и превосходные характеристики железа, производительность клиентов gRPC начинает ограничиваться внутренними механизмами реализации протокола HTTP/2 и особенностями управления каналами связи. При попытках увеличить нагрузку на сервер, минимальное количество узлов кластера вызывает неожиданные простои ресурсов, тогда как задержка на стороне клиента заметно возрастает.
Такое поведение парадоксально, ведь при уменьшении числа узлов должен, наоборот, снижаться общий накладной расход и повышаться эффективность. Понимание архитектуры gRPC и HTTP/2 раскрывает причины такого феномена. gRPC опирается на концепцию каналов, каждый из которых в идеале соответствует одному TCP-соединению. Интересно, что все каналы gRPC, созданные с одинаковыми параметрами, по умолчанию используют одно TCP-соединение, а внутри него HTTP/2 позволяет мультиплексировать несколько RPC-запросов одновременно. Каждое TCP-соединение налагает ограничение на количество одновременно открытых HTTP/2 потоков, стандартно оно равно 100.
Если количество параллельных запросов превосходит этот лимит, новые вызовы ставятся в очередь, что приводит к задержкам и снижению производительности. В публикации, посвящённой оптимизации gRPC, указываются два эффективных метода обхода данной проблемы. Первый метод основывается на создании отдельных gRPC-каналов для различных областей приложения, испытывающих высокую нагрузку. Второй предполагает использование пула каналов с разными параметрами, что тем самым расширяет число TCP-соединений и позволяет распределять нагрузку по ним. Однако на практике оказалось, что эти методы на самом деле взаимосвязаны и являются последовательными этапами одной и той же оптимизации.
Для более глубокого понимания заинтересовавшей проблемы была реализована упрощённая микробенчмарк-система, включающая сервер и клиент gRPC, написанные на C++. Сервер применял асинхронный API gRPC, задействуя несколько completion queue и заданное количество рабочих потоков и колбэков для максимальной производительности. Клиент осуществлял параллельные синхронные вызовы с распределением нагрузки между воркерами. Такой подход позволил точно оценить влияние количества одновременно выполняющихся запросов и структуры каналов на общую пропускную способность и задержку. Проведённые эксперименты выявили, что при использовании одного TCP-соединения (одного канала с одинаковыми параметрами) увеличение количества параллельных запросов приводит к значительным задержкам, значительно превышающим минимальную сетевую задержку.
Анализ пакетов показал отсутствие проблем с сетью: не было потерь, задержки на уровне сетевого стека минимальны, отключён алгоритм Нейгла, TCP-окна оптимальны. Вся дополнительная задержка возникала из-за клиента и глубоко внутри слоя gRPC. В результате даже при сравнительно низкой нагрузке система оказывается ограничена задержками внутренней очереди запросов и конкуренции за ресурсы внутри одного TCP-соединения. Следующий этап исследования показал решение, позволяющее практически в шесть раз увеличить пропускную способность и существенно снизить задержку. Для достижения этого нужно было создавать отдельные каналы с различающимися параметрами для каждого рабочего потока клиента или включать флаг GRPC_ARG_USE_LOCAL_SUBCHANNEL_POOL, который заставляет клиента создавать локальные подканалы внутри пула.
Такой подход позволяет добиться распределения нагрузки между несколькими TCP-соединениями, снимая ограничение на количество одновременно работающих HTTP/2 потоков внутри одного соединения и, как следствие, устраняя внутри клиента образующийся узкий горлышко. Важно отметить, что при высоких сетевых задержках, например, в несколько миллисекунд, такая оптимизация не приносит значительного прироста производительности. В условиях медленной сети и с задержками в миллисекундном диапазоне внутренние ограничения клиента не являются ключевым фактором, и эффективность достигается за счёт оптимизации серверной обработки и сетевой инфраструктуры. Однако в современных дата-центрах и кластерах с высокоскоростным оборудованием, где RTT может составлять доли миллисекунды, устранение клиентского узкого места gRPC приобретает критическую важность. Данная работа имеет большое значение для разработчиков распределённых систем и баз данных, стремящихся к максимально объективной и высокой производительности.
YDB как пример открытой распределённой SQL-базы данных активно использует контроль и оптимизацию клиентской части gRPC для повышения общей отзывчивости системы. Понимание внутренних механизмов и ограничений коммуникационной платформы позволяет оптимизировать сложные нагрузки без дорогостоящих аппаратных улучшений и сложных изменений архитектуры. Также данный случай подчёркивает важность изучения и тестирования систем не только с точки зрения компонентов сервера и сети, но и внимательное отношение к клиентской части, которая часто может стать главным фактором снижения скорости и увеличения латентности взаимодействия. В современных реалиях, когда масштабируемые системы работают в условиях огромного потока мелких вызовов с минимальной задержкой, инженерные решения на уровне клиента приобретают первостепенное значение. Для распространения полученных знаний был открыт репозиторий с тестовым микробенчмарком, где каждый желающий может воспроизвести результаты, провести свои эксперименты и предложить дополнительные улучшения.
Это способствует эволюции gRPC и связанным с ним технологиям, открывая возможности для совместной работы над устранением выявленных узких мест. В итоге, решение проблемы узкого места на клиенте gRPC заключалось в отказе от использования единого TCP-соединения для множества параллельных запросов. Создание множества каналов с разными аргументами или применение локальных подканалов позволяет эффективно масштабировать пропускную способность, сокращать задержки и использовать потенциал низколатентных сетей на полную мощность. Такой подход может оказаться решающим для систем, где на кону стоит производительность и скорость отклика при огромном количестве запросов в секунду. Перспективы дальнейших улучшений содержат глубокий анализ внутренней архитектуры gRPC и реализации HTTP/2, экспериментальную проверку с другими языками и средами, а также интеграцию данных оптимизаций в реальные производственные приложения и базы данных.
В конечном счёте, опыт YDB и данного исследования является ценным уроком для сообщества разработчиков и инженеров, стремящихся к максимально эффективному использованию современных коммуникационных протоколов.