gRPC давно зарекомендовал себя как мощный и надёжный инструмент для межсервисного взаимодействия, особенно в распределённых системах и облачных сервисах. Его архитектура, базирующаяся на HTTP/2, обеспечивает мультиплексирование запросов, что в теории должно способствовать высокой производительности и масштабируемости. Однако при работе в сетях с низкой задержкой специалисты из YDB столкнулись с неожиданной проблемой — узким местом в клиентской части gRPC, существенно влияющим на общую эффективность системы. Поговорим о том, как была обнаружена эта проблема, причины её возникновения и практические решения, которые помогут достичь высокой пропускной способности при минимальной задержке. YDB — это распределённая SQL база данных с открытым исходным кодом, которая ориентирована на высокую доступность, масштабируемость и поддержку строгой согласованности и транзакций ACID.
Для взаимодействия с клиентами YDB использует gRPC API, что делает производительность этого протокола критической для нагрузки на кластер и быстрого отклика. В ходе тестов и нагрузочных проверок была выявлена существенная разница в поведении системы при изменении количества узлов кластера. Чем меньше узлов, тем сложнее было нагрузить кластер, и при этом клиентская задержка только росла, несмотря на наличие свободных ресурсов на стороне сервера. Такой феномен заставил провести глубокое исследование с целью выявления внутренних причин и мест возникновения узких мест. Чтобы разобраться в проблеме, потребовался тщательный разбор архитектуры gRPC.
gRPC каналы, соединяющие клиента и сервер, основаны на HTTP/2 и бизнес-логика каждого RPC запроса отображается на HTTP/2 потоки. Важно отметить, что каналы могут создавать отдельные TCP-соединения, при этом в случае одинаковых параметров соединения gRPC объединяет их в одно, используя мультиплексирование потоков поверх одного соединения. Однако в данном случае именно этот механизм оказался главной причиной деградации производительности. Официальная документация gRPC содержит рекомендации по повышению производительности для высоконагруженных приложений. Там отмечается, что каждое HTTP/2 соединение имеет ограничение на количество одновременных потоков (по умолчанию около 100).
При достижении этого порога новые RPC-запросы ставятся в очередь и вынуждены ожидать освобождения потоков, что замедляет отклик. Для обхода этой проблемы предлагается создавать отдельный канал по каждому участку высокой нагрузки или использовать пул каналов с разными аргументами конфигурации, чтобы обеспечить параллелизм и распределение нагрузки по нескольким TCP соединениям. В YDB команда взяла на вооружение первый подход и для каждого рабочего потока на клиенте создавался отдельный gRPC канал. Тем не менее, даже при таком подходе задержка и пропускная способность стабильно не соответствовали ожиданиям, особенно при увеличении числа параллельных запросов. Для более глубокого анализа разработчики создали простой микро-бенчмарк gRPC на C++, который запускался на мощных серверах с современными процессорами Intel Xeon и высокоскоростной сетью 50 Гбит/с.
Эксперименты проводились с минимальными нагрузками и без payload в сообщениях, чтобы максимально исключить влияние самой бизнес-логики и сосредоточиться на поведении протокола и клиентской реализации. Результаты измерений показали, что с ростом количества параллельных запросов (in-flight) пропускная способность масштабируется далеко не линейно. При десятикратном увеличении параллелизма наблюдалось лишь около четырёхкратного роста пропускной способности, а пиковая задержка на клиенте упорно росла, в несколько раз превышая сетевую задержку. Такое поведение свидетельствовало о явных внутренних задержках и контенциях в клиентской части gRPC, которые накладывались даже на идеальную сетевую инфраструктуру и оптимизированный сервер. Дальнейший анализ с помощью tcpdump и wireshark подтвердил, что все запросы проходят по одному TCP соединению, а активность в канале свидетельствовала о долгих промежутках без передачи данных между запросами.
При этом выключенный Nagle, оптимальный TCP-окно и отсутствие задержек со стороны сервера исключали сетевые проблемы. Значит, причиной оставался внутренний механизм gRPC клиента, связанный с обработкой и очередностью запросов поверх одного HTTP/2 потока. Для устранения узкого места в клиенте было предложено две ключевые меры. Во-первых, создавать отдельный gRPC канал для каждого рабочего потока с использованием уникальных параметров, чтобы gRPC не объединял их в одно TCP соединение. Во-вторых, активировать параметр GRPC_ARG_USE_LOCAL_SUBCHANNEL_POOL, который позволяет локально пулить сабканалы и лучше управлять параллелизмом внутри канала.
Оба способа позволяют распределить нагрузку по нескольким TCP соединениям и увеличить эффекты параллелизма. После применения этих изменений наблюдалось значительное улучшение: почти в шесть раз выросла пропускная способность для обычных вызовов и более чем в четырёх раз — для стриминговых RPC. Одновременно с этим пиковая задержка перестала расти экспоненциально и увеличивалась очень плавно при возрастании числа in-flight запросов. Это позволило комфортно использовать gRPC и в условиях низколатентных сетей, не боясь упирания в клиентские ограничения. Чтобы проверить универсальность найденного решения, эксперименты провели также в сети со средней задержкой около 5 миллисекунд.
В таком случае разница между одиночным и многоканальным режимом была минимальна, что указывает на то, что внутренний клиентский узел проявляет себя именно в условиях очень низкой сетевой задержки, где влияние сетевых факторов минимально, и задержки на стороне клиента становятся решающими. Результаты исследований и рекомендации команды YDB имеют большое значение для разработчиков, использующих gRPC в реальных проектах с высокими требованиями к производительности. При оптимизации коммуникаций в высокоскоростных системах важно принимать во внимание внутренние ограничители протокола и реализации клиента, а не только характеристики сети и сервера. Правильная организация каналов и продуманное распределение нагрузки могут вывести приложение на новый уровень эффективности. В заключение стоит отметить, что хотя выявленный узел на клиенте является существенным фактором снижения производительности в современных низколатентных сетях, это далеко не единственная возможная точка оптимизации.
Команда YDB открыта для сотрудничества и приглашает сообщество к обсуждению и совместной работе над улучшением инструментов. Благодаря совместным усилиям можно будет сделать gRPC ещё более быстрым и надёжным, сохраняя при этом простоту и удобство использования. Таким образом, обращая внимание на настройки каналов, использование многоканальной архитектуры с уникальными параметрами и локальными пулами сабканалов, разработчики имеют мощные инструменты для устранения неожиданных узких мест в gRPC клиентах. Эти меры помогают раскрыть весь потенциал современных сетей и железа, достигая баланса между высокой пропускной способностью и низкой задержкой — важнейшими критериями для интерпретируемых распределённых систем и масштабируемых сервисов будущего.