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