gRPC давно завоевал репутацию высокопроизводительного и надёжного решения для коммуникаций между сервисами. Он построен на базе протокола HTTP/2, что позволяет эффективно мультиплексировать запросы и использовать преимущества современных сетевых технологий. Тем не менее, при эксплуатации gRPC в условиях сетей с низкой задержкой специалисты YDB столкнулись с неожиданным препятствием — узким местом клиента, которое серьёзно снижало производительность и увеличивало задержки. Компания YDB, разрабатывающая открытое распределённое SQL-хранилище с поддержкой строгой согласованности и транзакций ACID, активно использует gRPC для взаимодействия с клиентами. При проведении нагрузочных тестов и бенчмарков было замечено, что уменьшение количества узлов в кластере приводит не к ожидаемому росту эффективности, а к росту простоя ресурсов и увеличению клиентской задержки.
Это заставило разработчиков глубже исследовать поведение gRPC в таких условиях. Основная причина оказалась именно на стороне клиента. Несмотря на высокоскоростное соединение между серверами с RTT менее 0.05 мс, клиентская часть не могла масштабировать нагрузку не только линейно, но и вовсе демонстрировала возрастание латентности с увеличением числа одновременных запросов. При этом анализ сетевого трафика с помощью tcpdump и Wireshark не выявил никаких проблем с уровнем TCP — отсутствовали потери пакетов, задержки акков и другие типичные сетевые проблемы.
Также была подтверждена активная работа параметра TCP_NODELAY, отключение алгоритма Nagle, что исключало дополнительные буферизации на уровне TCP. Глубокий анализ показал, что в стандартной конфигурации gRPC клиент по умолчанию мультиплексирует все RPCзапросы с разных рабочих потоков поверх одного TCP соединения, используя единственный HTTP/2 канал. При этом в спецификации gRPC существует ограничение на количество одновременных стримов на одно HTTP/2 соединение, и по умолчанию это значение равно 100. Даже если количество одновременно «в полёте» запросов меньше этого, внутри gRPC наблюдаются очереди и конкуренция между потоками, что ведёт к задержкам и снижению пропускной способности. Дополнительным ограничением стала реализация взаимодействия каналов внутри клиента — каналы, созданные с идентичными параметрами, по факту использовали одно и то же TCP соединение.
YDB подошли к решению с позиции двух методик, рекомендованных официальной документацией gRPC. Первая заключается в создании отдельного канала на каждую высоконагруженную область приложения, вторая — в использовании пула каналов с разными параметрами подключения, чтобы добиться распределения RPC по нескольким TCP соединениям. Эксперименты показали, что только создание каналов с различающимися параметрами или включение специального аргумента конфигурации GRPC_ARG_USE_LOCAL_SUBCHANNEL_POOL позволяет добиться значительного скачка в производительности и снижении латентности. Для проверки этих гипотез была разработана мини-микробенчмарковая система на C++ (gRPC v1.72), подразумевающая ping-pong вызовы между клиентом и сервером без полезной нагрузки сообщения для максимального фокусирования на сетевой и программной логике.
Клиент запускался с разным количеством рабочих потоков и одновременно выполняемыми запросами, а сервер работал с выделением потоков и очередей завершения (completion queues) по оптимальным рекомендациям gRPC. Результаты были очевидны: при использовании единственного TCP соединения рост количества параллельных запросов приводил к линейному росту латентности и далеко не пропорциональному приросту трафика — при увеличении числа клиентов в 10 раз пропускная способность выросла лишь в 3.7 раза, а при увеличении в 20 раз — всего в 4 раза, что явно свидетельствовало о внутренней блокировке на стороне клиента. В то же время численный анализ задержек показал рост медианной и перцентильной латентности вплоть до миллисекундных значений, в то время как нагрузка и пропускная способность оставались заложенными ограничениями самого HTTP/2. Переключение на многоканальный режим с параметрами, задающими уникальные аргументы для каждого gRPC канала, либо включение локального подканального пула, обеспечило условно много соединений, что позволило эффективно распределить нагрузку внутри gRPC клиента.
Отмечена приблизительно шестикратная прибавка в скорости и минимум пятикратное снижение роста латентности при увеличении числа одновременных запросов. Вариант с потоковым (streaming) обменом сообщениями также показал значительное улучшение производительности и снижения задержек. Любопытно, что впоследствии эксперименты в сетях с большей задержкой порядка 5 мс показали, что в подобных условиях узкое место менее заметно, и мультикопийное распределение каналов даёт лишь незначительное преимущество. Это связано с тем, что при высокой сетевой задержке внутреннее программное время, связанное с конкуренцией каналов и очередями, становится не ключевым фактором производительности. В итоге специалисты YDB пришли к выводу, что для получения максимальной производительности gRPC клиентов в условиях низкой задержки и высокой пропускной способности, крайне важно тщательно конфигурировать каналы.
Многое решается не организацией нескольких каналов или пулов каналов как альтернатив, а правильной комбинацией этих подходов с учётом особенностей аргументов создания каналов. Другими словами, для каждого рабочего потока необходимо создавать отдельный gRPC канал с уникальным аргументом настройки или включённым локальным пулом, чтобы добиться истинной параллельной работы и минимальных издержек на взаимодействие внутри клиента. Данная стратегия позволяет реализовать узкопрофильную оптимизацию и поддержать сверхнизкие задержки даже при высокой нагрузке и масштабируемости базы данных, существенно улучшая пользовательский опыт и эффективность обработки данных. Кроме этого, обнаруженное узкое место подчёркивает важность комплексного тестирования и анализа HTTP/2-ориентированных систем, в особенности в распределённых базах данных и облачных сервисах, где высокая пропускная способность и низкая латентность критичны. Для разработчиков и инженеров по производительности ключевым выводом станет необходимость предусматривать многоуровневую оценку сетевых и клиентских ограничений.
Оказалось, что настройка каналов бывает важнее даже, чем совершенствование сети или самого серверного кода. Применение специальных флагов и аргументов gRPC, а также развитие собственного микробенчмарка для мониторинга, помогут своевременно фиксировать проблемы и эффективно их устранять. Наконец, стоит отметить, что gRPC, несмотря на свой статус проверенного инструмента, требует внимательного отношения к деталям реализации, особенно в масштабных кластерах и системах с низкой сетевой задержкой. Открытые исследования и обмен опытом между разработчиками существенно обогащают сообщество и ускоряют прогресс в области высокопроизводительных распределённых систем. Если вы заинтересованы в повышении производительности своих gRPC клиентов, стоит изучить возможности конфигурации каналов, сопровождая изменения измерениями латентности и пропускной способности.
Это позволит не только выявить скрытые узкие места, но и за счёт тонкой настройки существенно улучшить конечные результаты эксплуатации. Подводя итог, можно с уверенностью сказать, что открытие и устранение клиентского узкого места в gRPC позволяет повысить эффективность распределённых приложений, работающих в условиях современных высокоскоростных сетей. Это знание имеет практическое значение для инженеров и архитекторов, стремящихся добиться стабильной, быстрой и масштабируемой работы своих систем.