ADG Оставить заявку
Блог Интеграции 1 мин чтения

gRPC вместо REST: -40% latency и узкое место в JSON, которое Jaeger наконец показал

Перевели internal API между микросервисами клиента с REST/JSON на gRPC. Бинарный протокол сократил latency, а трейсинг через Istio и Jaeger вскрыл где именно тормозило.

Контекст момента

gRPC и Protocol Buffers активно принимаются как стандарт межсервисного взаимодействия в микросервисных архитектурах - 2018 год

Клиент пришёл с классической жалобой: «что-то тормозит, но непонятно где». Микросервисная архитектура, с десяток сервисов, REST/JSON между ними - всё как у людей. Latency на некоторых эндпоинтах была неприятной, но ничего конкретного не показывалось: приложения здоровы, БД не перегружена, сеть в порядке. Время разбираться.

Первым делом мы залезли в Jaeger - трейсинг уже был поднят через Istio, который клиент поставил несколько месяцев назад после нашего же совета. Именно здесь и начался весь разговор.

Что показал трейс

Картина была неожиданно наглядной. Цепочка вызовов между сервисами в трейсе выглядела примерно так:

[gateway] -> [svc-orders] -> [svc-catalog] -> [svc-pricing]
              |                  |
             ~4ms              ~18ms  <-- вот тут

Сервис каталога тормозил. Не потому что делал что-то сложное - он возвращал список из нескольких сотен позиций с атрибутами. Каждый вызов шёл по HTTP/1.1, ответ - JSON весом в несколько сот килобайт. Сервис pricing вызывался внутри этой же цепочки и делал то же самое, только поменьше.

Суммарная задержка на межсервисных вызовах складывалась в то самое «тормозит», которое клиент чувствовал на верхнем уровне, но не мог локализовать. Без трейсинга это практически нереально поймать: каждый сервис по отдельности отвечал в пределах нормы, и только сквозной трейс показывал реальную картину складывания задержек.

Решение: Protocol Buffers и gRPC

Мы предложили перевести внутренние вызовы между сервисами на gRPC. Аргументы были конкретные:

Бинарная сериализация вместо JSON. Protocol Buffers сериализует данные компактнее и быстрее. JSON красивый и читаемый, но на каждом вызове сервис тратит время на маршалинг/анмаршалинг строк там, где можно было обойтись бинарником.

HTTP/2 вместо HTTP/1.1. Мультиплексирование, сжатие заголовков - особенно заметно когда сервисы гоняют много небольших вызовов подряд.

Строгий контракт. .proto-файлы - это контракт между сервисами, который не живёт в голове разработчика и не расходится по репозиториям. Сломал контракт - сразу видно при генерации кода.

С клиентом договорились: переводим три самых нагруженных внутренних пути, смотрим на результат.

Что потребовалось сделать

Работа была итерационной. Для каждого сервиса написали .proto-схему, сгенерировали клиент и серверный код, переписали вызовы. REST-эндпоинты пока оставили - они используются для внешних вызовов и трогать их не было смысла.

Неожиданный момент: Istio с gRPC работает без дополнительной настройки - Envoy понимает HTTP/2 и трейсинг пробрасывается так же, через заголовки. Jaeger продолжал показывать трейсы, только теперь в спанах был правильный метод в нотации ServiceName/MethodName вместо HTTP-пути.

Одна вещь которую пришлось поправить руками - проброс grpc-timeout заголовка между сервисами. gRPC использует deadline propagation: клиент говорит «у меня есть 500мс на весь вызов», и этот дедлайн должен прокидываться по цепочке. Если не прокидывать - получаешь ситуацию когда вышестоящий сервис уже отвалился по таймауту, а нижестоящий продолжает работать вхолостую. Это не проблема gRPC, это просто надо учитывать при реализации.

Результат

После перевода трёх путей замерили. Latency на вызовах к сервису каталога упала заметно - где-то в районе 40% на P95, что совпало с нашими предположениями. Объём трафика между сервисами тоже сократился: бинарный формат ощутимо компактнее многословного JSON с повторяющимися строковыми ключами на каждом объекте в массиве.

Но самым ценным наблюдением оказалось другое. Трейсинг в Jaeger после перехода показал что оставшиеся задержки в цепочке - это уже не сериализация, а честная бизнес-логика: запросы к БД, внешние вызовы. То есть JSON-сериализация в нашем случае была реальным накладным расходом, который просто не было видно без инструмента.

До трейсинга это всё называлось «что-то тормозит». После трейсинга - стало понятно что именно, почему и сколько это стоит в миллисекундах.

Что осталось

На внешних API REST остался - клиентские приложения переписывать никто не собирается, да и не нужно. gRPC хорошо живёт внутри периметра, где оба конца контролируются одной командой и можно договориться о .proto-файлах.

Вопрос который пока открыт: перевод оставшихся внутренних путей. Там нагрузка поменьше, и насколько это оправдает трудозатраты - ещё считаем. Возможно, часть путей останется на REST и это будет нормально.

Работу по интеграции и переводу межсервисных API мы делаем в рамках проектного сопровождения - от аудита контрактов до реализации и настройки трейсинга. Этот кейс показал: иногда проблема решается не переписыванием архитектуры, а заменой протокола там, где он реально является узким местом - и без трейсинга это «реально» вообще не видно.

Контакт

Нужна такая же инженерная работа?

Опишите задачу и контекст. Ответим в течение рабочего дня, при необходимости подпишем NDA.