Привет, Хабр! Я Владислав Кислый, разработчик отказоустойчивых нагруженных сервисов в Т-Банке. Расскажу страшную сказку о том, как в одной компании взялись разрабатывать сервис. 

В качестве протокола взаимодействия выбрали gRPC. Что из этого вышло, с какими сетевыми проблемами пришлось столкнуться и как мы их решили — читайте в статье. Описанные проблемы можно потрогать руками с помощью тестового проекта, докера и темной магии Toxiproxy, который будет портить нам жизнь.

Знакомство с сервисом

gRPC — протокол молодой, но прочно закрепившийся как один из механизмов для межсервисного взаимодействия. Он разработан компанией Google на базе HTTP/2 и Protobuf. Основной целью было снижение количества сетевого трафика и строгая спецификация. Это была попытка закрыть два основных недостатка REST — протокола популярного, но хаотичного и многословного. Подробнее можно почитать в статье.

gRPC
Привет, Хабровчане! Для тех, кто не в курсе, gRPC - это открытый фреймворк от Google, который был пр...
habr.com

С протоколом разобрались. А что же будет делать наш чудесный сервис? Он должен выполнять важнейшую функцию — адресно приветствовать клиентов, которые присылают ему запросы с именем или ID. Это действие повышает удовлетворенность жизнью и мировосприятие клиентов, ведь им часто не хватает вежливого приветствия. 

Важно учесть, что у нас серьезный сервис и клиентов будет много, а значит, у сервиса высокие требования к времени ответа и высокий RPS.

У нас есть репозиторий с сервисом. В нем на Java реализован сервис Greeter и простейший клиент, которые общаются между собой запросами-приветствиями. В Git код специально разбросан по веткам (P1/.../P7), чтобы можно было двигаться по решениям итерационно, с возможностью поэкспериментировать на каждом этапе.

Сложную логику в сервисе и протоколе накручивать особо смысла не было, так как разговор больше пойдет об обеспечении надежности самого взаимодействия по gRPC.

Выходим в прод

Открываем ветку Р1. Запускаем сервис:

mvn exec:java -Dexec.mainClass="ru.karlsoft.grpc.helloworld.GreeterServer"

Запускаем клиент:

mvn exec:java -Dexec.mainClass="ru.karlsoft.grpc.helloworld.GreeterClient"

Чтобы было удобнее наблюдать за сервисом, можно настроить дополнительный тулинг Grafana с Prometheus. 

Схема деплоя
Схема деплоя

Считаем, что на этом шаге мы отрелизились в прод. Клиенты подключаются к шлюзу, который занимается маршрутизацией соединений и их балансировкой между подами сервиса. 

Схема заведомо упрощена, чтобы не нырять в дебри балансировки k8s. В реальности же там все сильно сложнее и под гейтвеем лежат HAProxy + Cilium. Хочу сосредоточиться на том, как подключение выглядит для клиента, для которого шлюз — это «черный ящик», который как бы наш сервис.

Спустя неделю после релиза стали поступать тревожные сигналы о том, что иногда резко просаживается RPS и вырастает лейтенси, соединения после таймаута обрываются с ошибкой UNAVAILABLE. Клиенты негодуют, уровень тревожности растет.

Разберемся, в чем дело, и попробуем это исправить. Первым делом проверим сервис и клиент, достаточно быстро убедимся, что дело не в них. Исследование показало, что единичные соединения время от времени как будто зависают. Взаимодействие по ним останавливается до тех пор, пока клиент сам не разорвет его. 

На сервисе картина выглядит как спад нагрузки — будто клиенты просто перестали ходить. То есть все указывает, что проблемы где-то за нашим шлюзом, а возможно, и в нем самом. Траблшутинг таких проблем занимает недели, а клиенты страдают прямо сейчас. Поэтому нет времени объяснять — давайте что-то делать на стороне клиента!

Тюним таймауты, настраиваем ретраи 

Переходим к ветке Р2. Попробуем воспроизвести ошибки на стенде. Для этого нам понадобится помощник — Toxiproxy. Эта утилита нужна для создания сетевых проблем. Качаем и настраиваем. Дополнительный скрипты по настройке правил лежат в каталоге Сonfig/Еoxiproxy.

Меняем схему подключения, чтобы между клиентом и сервером вместо привычного нам шлюза расположился Еoxiproxy. Он перебрасывает все запросы клиента через себя на сервер, но может ломать их по нашей команде. 

Важно обратить внимание на номера портов: они нам еще пригодятся
Важно обратить внимание на номера портов: они нам еще пригодятся

Главное, что для клиента существует только одна точка входа, а за ней расположено несколько нод. Для тестового стенда нам хватит и одной ноды.

Включаем схему, собираем метрики, все вроде бы красиво. Примерно так будет выглядеть трафик по gRPC-протоколу:

Теперь включаем проблемы на Toxiproxy. В текущей симуляции Toxiproxy все запросы проваливаются в «черную дыру». 

Буквально сразу видим, что резко падает весь RPS и также резко подпрыгивает лейтенси. Ситуация очень похожа на проблемы, происходящие на проде. Кроме того, если разбираться на уровне TCP-пакетов, видно, что весь трафик идет в рамках одного соединения, а вот попыток создать новые соединения не наблюдается. 

Такое поведение связано с фичей мультиплексирования, когда в рамках одного TCP-соединения существует множество внутренних соединений. Это сокращает издержки на открытие и поддержание открытых соединений. К слову, полезную фичу мультиплексирования предоставляет протокол HTTP/2. 

Получается, мы можем гонять все наши тысячи RPS в рамках одного соединения и не тратить время на установку новых TCP-соединений. Круто! Но, как говорится, наши недостатки — это логическое продолжение наших достоинств, и, если что-то происходит с этим TCP-соединением, разрыв соединения равен увеличенному лагу. Страдают сразу все запросы, им обслуживаемые. 

Рассуждая, что сеть в целом штука ненадежная и никому нельзя верить, настраиваем ретраи. Внезапно оказывается, что все это поддерживается gRPC сразу из коробки. Тогда добавим ретраи и посмотрим, насколько это улучшит ситуацию.

Читаем доку: ретраи уже работают по умолчанию (transparent retries). Все равно добавляем и пробуем ?

Изменения, которые надо внести в код, чтобы включить ретраи
Изменения, которые надо внести в код, чтобы включить ретраи

На сетевом уровне видно, что клиент соединение не закрывает, а упорно шлет keep-alive-пакеты. И только когда выходит время дедлайна, заданного в Toxiproxy, идут попытки создать новое соединение. Если же выставить таймаут на 0, что значит не сбрасывать соединение, попытки использовать «мертвое» соединение будут идти бесконечно.

Сетевой трафик после блокировки Toxiproxy
Сетевой трафик после блокировки Toxiproxy

Взаимодействие в дампе выше и идет в обе стороны. Toxiproxy не настолько низкоуровневая утилита, чтобы блокировать взаимодействие на уровне пакетов, она управляет запросами выше транспортного уровня модели OSI. Становится понятным такое поведение протокола gRPC и почему нет попыток пересоздать соединение, так как фактически оно живо, но нам от этого не легче, ведь клиентские запросы не обрабатываются.

Добавляем дедлайны

Переходим на ветку Р3. Читаем еще доку, получается, что Read Timeouts в их обычном понимании нет. Вернее, они есть, но работают только на фазе установки соединения. А после надо пользоваться дедлайнами. Прикручиваем дедлайны, пробуем.

Стало не сильно лучше. Все запросы ушли в DEADLINE_EXCEEDED, лейтенси равно выставленному дедлайну, и сетевой трафик выглядит так.

Сетевой трафик после включения дедлайнов
Сетевой трафик после включения дедлайнов

Теперь трафик завален попытками запросов и их отменами RST_STREAM. Попыток открыть новое соединение опять не наблюдается, и это уже начинает напрягать. Попробуем покрутить настройки Keep-alive.

Keep-alive уровня gRPC

Переходим на ветку Р4. Включаем настройку Keep-alive на уровне GRPC. Пока пробуем без дедлайнов, так как они засоряют вывод, а поведение с ними идентичное.

Настройка Keep-alive на стороне клиента
Настройка Keep-alive на стороне клиента

Наконец-то поведение меняется. Видно, как отрабатывает Keep-alive(PING) и, после того как ответ не был получен, открывается новое соединение, а старое закрывается.

Сетевой трафик после включения Keep-alive
Сетевой трафик после включения Keep-alive
Сиквенс-диаграмма, чтобы разобрать ситуацию подробнее
Сиквенс-диаграмма, чтобы разобрать ситуацию подробнее

Совсем другое дело: клиент начинает реагировать на проблемы со стороны сервиса. Если после отправки пинга сервер не пришлет ответ, клиент переоткрывает соединениe. 

Но есть большая ложка дегтя ? Согласно доке, если сервер должным образом не настроен, он будет закрывать соединения часто пингующих клиентов. Часто, с точки зрения архитекторов gRPC, — 5 минут. Их логику можно понять: нечего отъедать бесполезными запросами канал обмена. 

C другой стороны, 5 минут — достаточно большой таймаут, и обнаружение мертвых соединений будет занимать минуты, что катастрофически скажется на всех SLO/SLA. Но главное, клиент уже стал реагировать — и мы будем развивать успех!

Уменьшаем влияние обрывов соединения

Переходим к ветке Р5. Мы нашли механизм, который помогает обнаруживать мертвые соединения, — Keepalive. Включаем его, уменьшаем период между пингами до 10 с, можно быстрее реагировать на проблемы плохой сети. 

Но все равно остается проблема одного соединения, через которое течет множество запросов в параллель. Как ни крути, оно только одно — и, если с ним хоть что-то случится, весь клиентский трафик пострадает. Ведь мы потратим время на обнаружение проблем, а потом — на открытие нового соединения. И все это время мы не сможем обрабатывать запросы. 

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

Пример использования нескольких соединений в случае проблем. Когда упадет соединение 1, начнем использовать соединение номер 2. После переключения трафика можно восстанавливать соединение номер 1
Пример использования нескольких соединений в случае проблем. Когда упадет соединение 1, начнем использовать соединение номер 2. После переключения трафика можно восстанавливать соединение номер 1

Управлять количеством соединений в gRPC можно через связку системы разрешения имен и балансировку нагрузкой (Loadbalancing Policy). 

Картинка из официальной документации: компоненты gRPC-клиента, которые управляют соединениями
Картинка из официальной документации: компоненты gRPC-клиента, которые управляют соединениями

Сделаю акцент на прямой связи балансировки с разрешением имен. Все политики балансировки опираются на то, что вернет DNS. Стандартных политик балансировки всего две: Pick_first и Round_robin. Есть еще устаревшая gRPCLB, но ее рассматривать не будем.

Pick_first — политика по умолчанию, никакого отдельного конфигурирования не требует. Получает список IP-адресов от резолвера и пробует подключаться к ним по порядку. Первый, к которому удалось успешно подключиться, в дальнейшем и используется для всех запросов. Так как соединение только одно, нам не подходит.

Round_robin конфигурируется при создании канала (channel). Как и в первом случае, получается список IP-адресов от DNS. Но клиентский код устанавливает отдельное TCP-соединение к каждому адресу, соединение будет жить своей жизнью, и для запросов они будут использоваться по очереди. Получается, что, если DNS вернет три адреса и пострадает одно соединение, остальные два останутся работать и катастрофического эффекта на клиентский трафик удастся избежать. 

Такое дефолтное поведение — вполне рабочий вариант, и для многих компаний его более чем достаточно. Правда, не всегда DNS нами управляется, а главное, не всегда DNS публикует реальное состояние сети. Часто там присутствует только один IP — адрес шлюза, который будет заниматься маршрутизацией. Это и есть наш случай. 

На этом этапе есть один из вариантов решения на уровне кубера — через Linkerd. Но хочется не зависеть от сторонних систем и самим управлять количеством соединений.

А раз так, копаем дальше.

Абстракции, которые использует gRPCклиент при подключении к серверу. Channel — сущность, с которой мы взаимодействуем при создании этого клиента, все остальное обычно скрыто под капотом. Subchannel — это фактически соединения, которые открывает клиент
Абстракции, которые использует gRPCклиент при подключении к серверу. Channel — сущность, с которой мы взаимодействуем при создании этого клиента, все остальное обычно скрыто под капотом. Subchannel — это фактически соединения, которые открывает клиент

Если мы хотим много соединений, нужно крутить LB policy. А еще ретраи расположены сразу же за балансировкой, и, если захочется организовать какие-то свои ретраи, следует располагать их там, а не за Channe.

Создадим кастомный Name Resolver, который обманет балансировщик. Он будет возвращать список одинаковых IP-адресов в нужном нам количестве. В таком случае балансировщик Round-robin создаст отдельные соединения к шлюзу и мы сможем получить все преимущества от round-robin-балансировки, несмотря на то что хосты скрыты за шлюзом. Запускаем, пробуем.

Изменений в коде намного больше, и стоит посмотреть в пакет ru.karlsoft.grpc.helloworld.resolver: основные изменения там.

Сейчас используется пять соединений, поэтому чуть меняем правила для Toxiproxy, чтобы затрагивало только часть работающих соединений (параметр Toxiciy):

./toxiproxy-cli toxic add --downstream -t timeout --toxicName="timeout_ms" -a timeout=0 --toxicity=0.2 grpc_test_hello

У меня зацепило два соединения в такой конфигурации.

Список соединений, установленных gRPC-клиентом, и их состояние
Список соединений, установленных gRPC-клиентом, и их состояние

В итоге трафик просел, но не до 0, как было до того.

Как снизилась нагрузка на сервере
Как снизилась нагрузка на сервере

Пробуем спекулятивные ретраи: путь для минимизации влияния

Идем дальше — на ветку Р6. Попробуем путь минимизации влияния — хеджирующие или спекулятивные ретраи. Это такой специфичный тип ретраев, когда клиент ждет ответа фиксированный период времени. Если по истечении указанного интервала результата нет, клиент отправляет еще один запрос, а потом еще один — в результате победит быстрейший. Главное отличие в том, что он не завязан на ошибки от сервера, как сделано в обычных ретраях.

Сиквенс-диаграмма, на которой подробнее показано, как работают спекулятивные ретраи
Сиквенс-диаграмма, на которой подробнее показано, как работают спекулятивные ретраи

Основной плюс такого способа ретраев в том, что обычно лейтенси сервера известно и мы можем очень быстро реагировать на любые лаги сети, почти не отбрасывая входящие запросы. 

Опасность такого вида ретраев заключается в том, что можно легко перегрузить принимающую сторону. Частичной защитой принимающей стороны выступает механизм retryThrottling, когда сервис слишком часто отвечает ошибками, часть ретраев не будет отправлена. И чем больше ошибок от сервера, тем больше ретраев будет отброшено без отправки. 

Вторая опасность — использование хеджирующих ретраев для пишущих ендпоинтов. Без идемпотентности это может привести к печальным результатам — например, начислить лишние деньги на счет. Стоит взвесить за и против, прежде чем их использовать.

Кроме того, добавлен класс HeaderClientInterceptor.java для наблюдения за поведением спекулятивных ретраев, так как через логгер это сделать нельзя. Этот класс будет печатать передаваемые заголовки запросов. Нас интересует заголовок grpc-previous-rpc-attempts, его клиент будет обрабатывать особо и печатать значение, сколько ретраев пережил конкретный запрос.

 Главные изменения, которые нужно внести в код, чтобы включить спекулятивные ретраи
 Главные изменения, которые нужно внести в код, чтобы включить спекулятивные ретраи

Посмотрим, какие результаты даст использование спекулятивных ретраев:

  • Проба 1. Затронуло 3 соединения.

  • Проба 2. Затронуло 2 соединения.

  • Проба 3. Затронуло 2 соединения.

  • Проба 4. Затронуло только 1 соединение. С дополнительной конфигурацией дедлайнов и сокращением времени на хедж-запросы.

Метрики сервиса в момент создания проблем для клиента. Цифрами отмечены провалы во время проб
Метрики сервиса в момент создания проблем для клиента. Цифрами отмечены провалы во время проб

По графикам видно, что процент успешных запросов зависит от того, сколько соединений остается на ходу. Кроме того, на графике RPC stream ops есть какие-то флуктуации во взаимодействии, которые не имеют к экспериментам никакого отношения. Особенно отрадно, что при потере одного соединения просадка практически равняется этим флуктуациям.

Проводим последнюю проверку 

Переходим на ветку Р7 — настройки такие же, как у Р6, но тесты на базе Iptables. 

Хочется исключить фактор обмана gRPC, чтобы протокол не воспринимал, что соединение по-прежнему живо. Поэтому сценарий, который будем рассматривать, немного отличается от предыдущего. 

Откроем пять портов (10051-55) через Toxiproxy (файл с настройкой Configure-5.sh), Toxiproxy останется перенаправлять запросы на тот же сервис.

Схема подключения клиента. Пунктиром показано сбойное соединение
Схема подключения клиента. Пунктиром показано сбойное соединение

Выбранный сценарий нужен для того, чтобы была возможность заблокировать один из пяти портов через Iptables (файл Config/iptables.sh), блокироваться будет порт 10053. Клиент же будет использовать пятое подключение, но уже на разные порты — по одному соединению на порт.

Проба 1. Блокировка трафика на 2 минуты и потом его возобновление.

Проба 2. Блокировка трафика на 5 минут и потом его возобновление.

Метрики сервиса в момент создания проблем для клиента. Красным отмечены интервалы блокировки трафика
Метрики сервиса в момент создания проблем для клиента. Красным отмечены интервалы блокировки трафика

Пятиминутная блокировка трафика очень быстро отыгралась и мало отличается по эффекту от двухминутной. Влияние коснулось только 20% трафика и на очень небольшой период. Часть запросов все-таки попали в Deadline, но их количество невелико.

Посмотрим, что происходило на сетевом уровне. 

Дамп трафика по двум портам: 10051 (слева) и 10053 (справа). Порт 10053 блокировался фаерволом
Дамп трафика по двум портам: 10051 (слева) и 10053 (справа). Порт 10053 блокировался фаерволом

Как видно по рабочему соединению, запросы проходят успешно. Благодаря спекулятивным ретраям часть запросов ушла на работающие соединения и эффект на клиентский трафик был минимальным.

Еще получили неожиданный эффект: влияние для двухминутной блокировки и для пятиминутной почти не отличается.

Состояние соединений в динамике: попытки закрыть-открыть и в середине отсутствие соединений
Состояние соединений в динамике: попытки закрыть-открыть и в середине отсутствие соединений

Сначала статус соединения порта 10053 прыгал между полузакрытым и полуоткрытым. В итоге попытки использовать порт 10053 вообще прекратились: соединение к этому порту просто было закрыто. И только спустя некоторое время порт опять начал использоваться заново. Даже на уровне пакетов достаточно долго не было попыток открыть новое соединение.

Состояние соединения на уровне пакетов. Красным подсвечена длительная пауза между попытками
Состояние соединения на уровне пакетов. Красным подсвечена длительная пауза между попытками

Выводы

gRPC — мощный протокол с кучей очень крутых фич, которые не грех перенять для взаимодействия по обычному HTTP. Кроме того, это очень быстрый протокол, который дает быструю сериализацию и десериализацию на базе Protobuf и низкое лейтенси за счет мультиплексирования на уровне HTTP/2.

gRPC — не только языковой, но и сетевой агностик, он может использовать коммуникацию, отличную от привычного TCP/IP-соединения, из-за этого он базируется на большом количестве абстракций. 

Не стоит ожидать в нем тонкой настройки TCP-соединений: это вне протокола и надо крутить настройки OS и Netty как транспорта. Из-за всех этих абстракций и механизмов управления коммуникацией настройка gRPC оказывается весьма запутанной, в ней легко можно заблудиться и ошибиться.

Retry целиком полагаются на ошибки, приходящие от нижних уровней модели OSI. Без настроек типа Keepalive практически бесполезны.

Deadlines — это не таймауты, это точка на временной шкале, когда запрос должен быть обработан. Их скорее надо использовать как механизм для предотвращения перегрузки сервиса, а не для обработки сетевых ошибок.

Keep-alive — важная часть обеспечения надежности, но сервер может сбрасывать часто пингующих клиентов.

Скажу банальность, но помним про сбор метрик, особенно если используем gRPC в условиях высокой нагрузки.

Несмотря на мощь, встречаются неохваченные моменты, например нет логирования для ретраев.

После долгого исследования (оно реально заняло полгода минимум) вышли на проблемы в инфре. Как оказалось, из-за баги в цилуме нарушалась маршрутизация за NAT и некоторые соединения терялись. Лечить такое на клиенте можно только путем сброса соединения и открытия нового. После чтения Git стало понятно, что багу если вообще исправят, то произойдет это очень не скоро. 

Отмечу, что все трюки, описанные в статье, мы успешно использовали и для HTPPp-соединений, так как и они страдали— только в меньшей степени.

Полезные ссылки:

  1. Код для воркшопа

  2. Toxiproxy

  3. gRPC deep dive: Efficient network communication using HTTP2

  4. What does setting GRPC once request read time out? 

  5. gRPC Timeouts & Deadlines

  6. TCP and gRPC Failed Connection 

  7. grpc channel not honoring connection timeout

  8. gRPC Retry Design

  9. How to get gRPC retry logs on client | java gRPC

  10. Client-side Keepalive

  11. gRPC is easy to misconfigure

  12. Load Balancing in gRPC

  13. gRPC Load Balancing

  14. gRPC Load Balancing on Kubernetes without Tears

  15. A lesson learned on GRPC Client side load balancing

  16. Implementing native gRPC load balancing with Kubernetes

  17. Performance best practices with gRPC

  18. How to connect to unix socket?

  19. Inter-process communication with gRPC

  20. In the DSR mode TCP reply may be sent from wrong IP when EP is reachable through multiple LB IPs

  21. No mapping for NAT masquerade when creating lots of short-lived connections 

  22. The SNAT/DNAT problem with injecting into another TCP session using BGP Anycast

Комментарии (29)


  1. powerman
    29.08.2025 15:28

    Вы бы блок TL;DR добавили в начало статьи:

    • На gRPC обязательно нужно включать KeepAlive (с цифрами порядка 5-15 сек. для min_time/timeout/interval), причём делать это стоит и на клиенте и на сервере по умолчанию, не дожидаясь проблем. Если сервер мешает использовать такие KeepAlive на клиенте - нужно бить по попе его авторов. В абсолютном большинстве случаев этого будет достаточно.

    • Если будут проблемы то стоит задействовать встроенный loadbalance для открытия нескольких одновременных соединений, причём от этого может быть польза даже если инстанс сервера у вас один.

    • Если и этого окажется недостаточно, то на помощь придёт отправка нескольких запросов вместо одного с использованием первого ответа - но это в разы увеличит нагрузку на сервер и делать такое надо очень-очень осторожно.


    1. vladislav2103 Автор
      29.08.2025 15:28

      а как же интрига? + хотел показать решение проблем в динамике


      1. powerman
        29.08.2025 15:28

        Ну… одно другому не мешает. Статья интересная, но управление ожиданиями тоже нужная вещь. И наличие TL;DR редко приводит к тому, что статью вообще не читают, разве что из него сразу видно, что тема не твоя - но тогда это к лучшему.

        Ещё лично мне большую часть статьи хотелось ругаться на то, что вместо поиска реальной проблемы пошли лепить одну заплатку на другую в надежде что это поможет вместо понимания что происходит и почему. А в самом конце выяснилось, что оказывается реальную проблему всё-таки параллельно искали и таки нашли - вот что стоило это раньше сказать, и я бы читал спокойно. ;-)


        1. vladislav2103 Автор
          29.08.2025 15:28

          что вместо поиска реальной проблемы пошли лепить одну заплатку на другую в надежде что это поможет вместо понимания что происходит и почему

          Я даже отчасти рад, потому как смог передать настроение )
          К сожалению, у нас именно так и было, много времени было потрачено на использование заплаток, чтобы хоть как-то улучшить ситуацию. Но в свою защиту могу сказать, что инфрой занимаются одни люди, а пишут сервисы другие люди и поиск проблем когда завязано несколько отделов это отдельный вид искусства . "И нельзя просто так взять и сделать дамп трафика" чтобы найти проблему, увы.

          Если не сложно, то может быть у вас есть пример статьи с оформлением этого самого TL;DR? Попробую добавить. Я прямо скажу новичек в этом деле.


          1. powerman
            29.08.2025 15:28

            Это не есть что-то прям обязательное, и не то, чтобы часто используется конкретно на хабре - мой коммент был скорее шутливый, нежели серьёзной рекомендацией. Тем не менее, если погуглить site:habr.com "tl;dr" то находится достаточно примеров. Полезность TL;DR сильно зависит от содержимого статьи - и да, на мой взгляд для данной статьи это было бы полезно.


  1. yokotoka
    29.08.2025 15:28

    О, может вы подскажете — почему в grpc до сих пор нет нормального способа пользоваться им из браузерного приложения? В теории он транспорт-агностик, а на практике grpc-web до сих пор кусок нерабочего кринжа. Что мешает им нормально уже сделать двухсторонние стримы поверх ws/http2/quic?

    UPD: Я нашёл вот такое "объяснение", но это же какой-то стыд и позор для серьёзной технологии, которой grpc пытается казаться
    https://github.com/grpc/grpc-web/blob/master/doc/streaming-roadmap.md

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

    UPD2: Похоже RSocket выглядит лучшей потенциальной альтернативой gRPC
    https://rsocket.io/


    1. powerman
      29.08.2025 15:28

      почему в grpc до сих пор нет нормального способа пользоваться им из браузерного приложения?

      Потому что gRPC активно использует низкоуровневые фичи HTTP/2, доступа к которым браузер просто не предоставляет. Поэтому тут проблема не столько gRPC или HTTP/2, сколько специфических ограничений песочницы браузеров для JS. Как следствие из браузера приходится вместо настоящего gRPC использовать другие протоколы, которые можно конвертировать в/из gRPC, но этим конвертированием во-первых кто-то должен заниматься, и во-вторых использование "другого протокола" автоматически лишает возможности использовать море готовых инструментов созданных для gRPC и обычно ограничивает доступные возможности самого gRPC (напр. лишая возможности использовать двухсторонний stream и т.п.).

      Я нашёл вот такое "объяснение"

      Это не сам gRPC, это gRPC-Web - один из вышеупомянутых конвертеров. И да, он малополезен. На Go обычно используют https://github.com/grpc-ecosystem/grpc-gateway, и он достаточно хорош (для не-Go можно на Go написать простейший прокси-сервер на grpc-gateway, который будет принимать из веба запросы и проксировать их в реальный бэк уже по gRPC). Ещё есть Buf Connect https://connectrpc.com/ (но этим я пока не пользовался).

      Плюс такого подхода - кроме мелкого модуля на grpc-gateway весь остальной бэк может быть написан на gRPC, а для фронта/браузера будет автоматически генерироваться привычный им swagger.yml (ведущий в grpc-gateway прокси). При этом мобильное приложение при желании может либо использовать тот же swagger.yml либо подключаться мимо этого прокси по обычному gRPC. Решение рабочее, удобное, но не идеальное - определённые ограничения по функционалу относительно полноценного gRPC есть, как и потеря скорости на проксировании (на сериализации/десериализации JSON).


    1. vladislav2103 Автор
      29.08.2025 15:28

      не стоит забывать, что grpc затачивался прежде всего под межсервисное взаимодействие, так что да, проблемы с UI у него очевидные. Обычно все через дополнительную проксю организуется.
      Насчет rsocket, пробовал лет 5 назад, не очень впечатлился + он сыроват еще был. Наличие protobuf под капотом grpc идет большим плюсом, так как это сразу готовый контракт. Использование же голого TCP не всегда является плюсом. В общем, grpc более консервативное решение, но всегда надо смотреть на задачу и инфру.


      1. Kanut
        29.08.2025 15:28

        не стоит забывать, что grpc затачивался прежде всего под межсервисное взаимодействие, так что да, проблемы с UI у него очевидные.

        grpc задумывался как remote procedure call. И он без проблем используется в UI в куче языков. Как минимум мы его вот вообще без проблем используем в UI в C#, Java и C++. Вплоть до того что он поддерживает асинхронность и другие подобные вещи.

        А проблемы с использованием grpc в браузерах это, как выше уже правильно написано, конкретно проблемы ограничений в браузерах.


        1. vladislav2103 Автор
          29.08.2025 15:28

          Спасибо за дополнение. Может у вас есть недавний опыт использования rsocket? особенно в связке с UI


          1. Kanut
            29.08.2025 15:28

            rsocket мы не используем.


      1. Jijiki
        29.08.2025 15:28

        если про высоконагруженные не возьмусь защищать TCP, для простой страницы открыть обновить может хватить TCP, просто сокет не должен быть блокирующим и там, я делал простой thread(server) в части client-server для себя всё работало хотя на тот момент тестил одну библиотеку поэтому подключался к 127 0 0 1, но сообщения между клиентами не пропадали кстати, тоесть клиенты обновлялись относительно друг друга (я тестил 30 клиентов только ))


  1. mrxak
    29.08.2025 15:28

    Всем привет, хочу гонять по 100 гигов repeated-полей через gRPC с mTLS по Infiniband. Плюсы-минусы-подводные камни?


    1. vladislav2103 Автор
      29.08.2025 15:28

      привет, 100 гигов за какой промежуток времени? и какой тип нагрузки? более размазана или всплесками?


      1. mrxak
        29.08.2025 15:28

        Готовые пачки на стороне клиента, именно такого объёма, по 10-12 пачек/сутки для отправки на сервер (в далёкой перспективе до 50), а потом их выборочно обновлять и стримить обратно.. вообще думал после приёма их складывать на ramfs. Архитектура пока ни с той ни с другой стороны не прописана, 100 гигов+gRPC+mTLS требование заказчика, librdmacm через infiniband в соседнем проекте реализовано просто, тут на прототипе скорее всего ограничатся 10 GB Ethernet.


        1. vladislav2103 Автор
          29.08.2025 15:28

          Сразу оговорюсь, с Infiniband не работал, только с Ethernet сетями. По скоростным характеристикам особых проблем не вижу, затраты на открытие соединения будут только вначале, сериализация быстрая, так что должно хорошо получиться. Я бы только посмотрел в сторону стриминговой передачи с возможностью backpressure, чтобы можно с серверной стороны чуть прижать передающего, если серверная часть начнет захлебываться под нагрузкой. А с другой стороны возможность распараллеливания передачи, чтобы эффективно утилизировать канал.

          Если вылезут проблемы, я бы почитал )


          1. mrxak
            29.08.2025 15:28

            Огромное спасибо!

            Статью на Хабре не обещаю, но если всё будет успешно, то через пару лет будут выступления на не очень профильных для этого (медицинская физика) конференциях. Можно условно переформулировать задачу, что голые данные с кабеля УЗИ (принципиально, там по 10 ГБит/с может лететь, совсем крутые модели до 70 и FPGA ставят) за несколько секунд после агрегирования через вот такой относительно высокоуровневый стек прогнать.


    1. sdramare
      29.08.2025 15:28

      Зависит ещё от формата данных. Если это одно сообщение в 100 гигов, то будет огромный оверхэд на сериализацию. Если вам при этом нужно только поменять отдельные поля, посчитать аггрегацию по полю и сохранить данные на диск, то тот Apache Arrow Flight может быть заметно эффективней.


      1. mrxak
        29.08.2025 15:28

        Спасибо за комментарий.

        Пока предполагается либо

        message package {
        repeated double value1 = 1;
        repeated double value2 = 2;
        }

        либо

        message single {
        double value1 = 1;
        double value2 = 2;
        }

        message package {
        repeated single value = 1;
        }

        и соответственно сервис

        service Compute {
        rpc PerformSingleOp(stream package) returns (stream package);
        }

        Замеры проведём и так и так, посмотрим. Естественно в реальности ещё коды команд, коды возвратов.

        Сохранять как раз ничего не нужно, данные не будут (не должны) храниться на этой ноде больше часов 10, надо обработать и вернуть на другую ноду, при этом на вход прилетает все 100 гигабайт, а в зависимости от кода обработки вернуться может от вообще единственной пары value1+value2 во всём repeated до всех 100 гигабайт.


        1. sdramare
          29.08.2025 15:28

          Ну это не очень хороший вариант в том плане, что для большинства реализаций gRPC у вас будет происходить перенос парсером данных из сетевого буффера в double[] конечного рантайма(джавы, сишарпа, го и т.д.), т.е. дополнительные расходы на перенос данных, что на 100 гб будет довольно заметно.


          1. mrxak
            29.08.2025 15:28

            Да понятно, там в реальности надо вообще cudaMemcpyAsync над ними, но ограничения на оперативку нет, в ноде 24 плашки по 64 Гб. И реализовано уже librdmacm между другим клиентом и подобным сервером с видеокартой, но заказчик сказал давайте модно-молодёжно в gRPC обернём, а то нам вот этот клиент rdma писать лень, да и библиотека эта ваша 10 лет как не обновлялась. Поэтому вариант 1 - просто гоняем вот так repeatedы c командой, вариант 2 - один раз этот пакет 100 Гб принимаем, кладём на ramfs, отдаём его хеш-ид, дальше присылают команду и хеш над которым провести, а отдаём уже что получится. Других вариантов особо не вижу.


  1. upcFrost
    29.08.2025 15:28

    Ходили по всем этим граблям плюс по некоторым докероспецифичным (вернее свармоспецифичным). Прошли тот же путь кроме п.6 (хедж ретрай) потому что в питонячьем клиенте его тогда не было. Хз, по итогу пришёл к мысли что рест (а лучше gql-федерация) и минимум сервисов лучше чем вся эта боль

    Про идемпотентность сделали свой кодогенератор который набивает конфиг по спеке. Ну и protoplus вместо ванильной, она отвратна


    1. vladislav2103 Автор
      29.08.2025 15:28

      Мы пока копали эту проблему, на части сервисов перешли на rest+protobuf, потребление ресурсов выросло на 30-40%. Цифры не маленькие, учитывая, что джаву и так критикуют за прожорливость, а в свете большого кол-во подов так и вообще. К GraphQL тоже присматривались, но так и не решились, уж больно много клиенту позволено.


      1. upcFrost
        29.08.2025 15:28

        Мы пока копали эту проблему, на части сервисов перешли на rest+protobuf, потребление ресурсов выросло на 30-40%

        Мы начали из-за этого постепенно сползать обратно к монолиту. Но там была проблема слишком большой разбивки когда часть сервсов натурально становилась db-table-over-rpc.

        Про gql - у нас главный затык был забыть весь рест, иначе схема будет rest-style и смысла в gql будет. Вот когда подошли к схеме апишки как к схеме монги и прочих document db и обмазали это дело федерацией с парой кастомных модулей - вот тогда попёрло.

        А про "много позволено" - есть читерский метод с хранимками. Суть - есть rfc на "хранимые запросы", когда фронт сначала посылает полный запрос, который бэк кладёт в кеш, а потом шлёт только его хеш/id плюс переменные и бэк уже знает что к чему. Изначально сделано чтоб не гонять развесистые запросы с трафик-ограниченных клиентов (мобильное соединение часто ассиметрично на загрузку). Трюк в том что запросы и кеш можно сделать статикой и тупо запретить не-кешевые запросы. Скинуть их в базу, вносить туда только после ревью, подсасывать в локальную мапу раз в минуту - и фронт будет сидеть в четко огороженной песочнице.


        1. vladislav2103 Автор
          29.08.2025 15:28

          интересная идея насчет кеширования, спасибо!


        1. powerman
          29.08.2025 15:28

          А разве суть GraphQL не в том, чтобы ускорить разработку новых фич на клиенте избежав написания под каждую фичу/изменение соответствующих изменений в API бэка? И разве Вы не убили всё это своим подходом? Я к тому, что сам подход-то, на лично мой взгляд (я не фанат GraphQL из соображений безопасности и производительности с первого дня как про него узнал) вполне здравые, но - а зачем при этом подходе вообще нужен GraphQL?


          1. upcFrost
            29.08.2025 15:28

            Не совсем. Суть в том что схему/резолверы/код вам трогать не надо. Условно говоря вот у вас есть сайт типа линкедина: шапка с кнопкой профиля, публичный профиль, реальный профиль. Схема одна, типы одни, фронт просто берет что нужно и в конце говорит какие запросы нужно "записать". При том жёсткая привязка нужна только в проде, на дев-среде можно оставить полный доступ.

            Далее по "новым фичам". Например фронту сказали что шапка говно и надо добавить "уровень юзера" как на букинге, резолвер которого в полном профиле уже есть. Что делает бэк? Просто перед препродом/продом добавляет запрос в базу. Не пишет новую ручку, не меняет апишку, не пытается понять лопнут ли клиенты от нового поля и надо ли пилить новую версию или можно просто данных докинуть. По сути все действия бэка сводятся к ревью и утверждению запроса.


            1. powerman
              29.08.2025 15:28

              Что именно нужно сделать на бэке и насколько это просто/быстро - зависит от используемого инструмента (протокола/фреймворка/кодогенератора/etc.). Выглядит так, что Вы преобразовали GraphQL в аналог такого фреймворка, который ускоряет обновление бэка под новый функционал, вместо того чтобы отменить необходимость обновлять бэк в принципе.

              Плюс разрешая любые запросы на дев-среде всегда есть вероятность что-то забыть выкатить на прод из нужных новых запросов. Конечно, это зависит от поставленных процессов, но обычно рано или поздно такие висящие на стене ружья всё-равно стреляют.

              P.S. Я в районе 99 года как-то сделал проект довольно схожий с GraphQL - там фронт на бэк передавал куски SQL-запросов. ;-) В принципе оно работало, с безопасностью и производительностью известных проблем не было, но… что-то больше такого делать не хочется. Теряется ощущение контроля, функционал начинает развиваться "не туда" из-за слишком лёгкого и прямого доступа фронта к БД, растворяется довольно полезный слой абстракции в виде конкретного и специализированного API бэка. В общем, получается типичное простое быстрое неправильное решение, которое в будущем всё-равно возьмёт свою цену.


              1. upcFrost
                29.08.2025 15:28

                который ускоряет обновление бэка под новый функционал, вместо того чтобы отменить необходимость обновлять бэк в принципе.

                Смотря что считать обновлением. Контроль и согласование все равно необходимы, иначе будет ситуация когда левая нога не знает что делает правая. Уже были в такой, особенно смешно было когда ручка делала одно, фронт на неё повесил кнопку и думал что она делает другое, а саппорты думали что она делает третье и рассказывали это юзерам. Добавление запросов в кеш можно автоматизировать, но лишняя автоматика на выкладке может выстрелить в ногу ни чуть не хуже чем человеческий фактор.