Net GRPC, или «не осилили»

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

Начинали мы с нуля, с эскизов. Задача была реализовать систему, обладающую следующими функциональными и техническими требованиями:

  • поддержка разговора с абонентом в режиме реального времени

  • адаптация на высокую нагрузку + масштабируемость (тысячи одновременных звонков)

  • доп. фичи, в виде отправки расшифровки разговора абоненту, текстовые нотификации в телеграм и прочее.

В качестве основного технологического стека (именно под голосовую часть) была выбрана связка Asterisk +.Net (главным образом потому что у самого был такой опыт, те Asterisk не оспаривался а вот под стек для программного решения были споры), а для масштабируемости — микросервисная архитектура, в том числе со сложными сервисами‑оркестраторами.

Если кратко, описание решения выглядело примерно таким:

  • сервис управления состоянием звонка (Asterisk REST API + web‑сокеты). Управление звонком и получение его стейта в режиме реального времени;

  • ряд сервисов по управлению непосредственно голосом: вывод TCP трафика, преобразование в GRPC, распознавание голоса, получение текстового ответа, синтезация ответа, отправка голоса обратно в канал (TCP). Итого, цепочка из ~ 7 сервисов с дуплексной передачей трафика;

  • ряд сервисов по сохранению результатов — параметров звонка, расшифровки, самой голосовой записи.

Нас сильно подвела реализация.Net (6.0-7.0) GRPC, которая, как показала практика, не выдерживала нужных нам значений RPC. Если взять 1 звонок:

  • пакеты по 320 байт

  • каждые 20 мили секунд, или 50 раз в секунду (архитектура Asterisk)

  • дуплексная передача, итого Х2 

  • работа сервиса в режиме прокси, еще Х2 

  • 500 одновременных звонков на 1 под в цепочке

Итого RPC = 5022* 500 = 10 тыс / сек, те кол‑во сетевых операций чтения/записи GRPC пакетов.

.Net же показывал более‑менее стабильные результаты до 50 звонков (те в 10 раз ниже ожидаемого). Далее начинались деградации (задержка времени между пакетами), которая накапливалась и доходила до неприличных 70 мсек (вместо 20).

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

Прототип показал успех, а значит — можно развивать.

И почему нет?

Cервис (каждый) простой, минимум функционала

микросервисная архитектура адаптирована под «зоопарк», главное поддержать контракт «вход‑выход» и сборку в Docker

  • GO, несмотря на отсутствие объектно‑ориентированной парадигмы, хорошо рулит потоками (байты летают в обе стороны со свистом ;-)

  • и, кстати, в GO больше Linux‑адаптированных пакетов под работу с голосом (у.Net в этом плане все совсем скудно, даже Core в этой части подвязан на Windows).

  • И да, GO показывал полное отсутствие деградаций до 1000 звонков. 20мсек, иногда до 22 мсек, но больше мы никак не фиксировали.

  • Итого, некритичная к RPC часть системы так и оставалась жить на.Net, а критичные сервисы (7 штук) стали постепенно переезжать на GO.

Кстати, решение получилось годным, задержка в разговоре не превышала 1.5 сек в штатном варианте (если не рассматривать аномальные всплески нагрузки).

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

  • Во‑первых, у нас был грамотный архитектор) сам, когда увидел концепт такой системы, оценил его как «ненадежный и рискованный», те важна была первоначальна экспертиза и уверенность в возможности реализации такого решения.

  • Отладка. Никакой возможности подебажить даже теоретически. Ну а толку? Подключите вы этот свой под к сети, влезете дебагом — трафик будет тормозить, картина испорчена. Здесь как раз возникает необходимость писать понятные логи, еще и в режиме «агрессивной многопоточки».

  • Тесты. Тоже не имеют ценности. Ну те разве что писать внешний «эмулятор» в количестве двух штук, один подает трафик в сервис другой получает, и также обратно.

Про юниты/интеграционные можно вообще забыть, так как основной механизм — это чтение/запись пактов из/в сети/сеть и их перекидывание.

Логики почти нет.

Деплой и оркестрация

А вот тут уже начинаются сложности. Возможностей k8s из коробки не хватает для управления TCP трафиком (нет штатных механизмов балансирования нагрузки). В систему добавляются умные «поды‑оркестраторы», которые следят за нагрузкой подов голосовых, не отправляют обслуживание новых звонков на уже загруженные поды;

  • Стейты. Не осилили.

Во всяком случае, не в первой версии (дальше не знаю). Наработки — чтобы в случае «смерти» одного голосового пода весь его трафик мог направиться на иной под, были, но такие улучшения слишком сложны и нестабильны для первых версий.

Те опять же, к системе работающей «целевым образом» добавляется система для «разруливания» «нештатных ситуаций», а это уже по сути «второй контур».

  • Риски. Да, корневую часть пришлось переписать на GO что значительно увеличило бюджет.

Но кто ж знал? Это область неизведанного — платформа не вытянула/архитектурно ошиблись/и так далее. Те пока не попробуешь — не узнаешь, а «явных противопоказаний» на теоретическом уровне к такому решению (.net) не было.

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


  1. Kerman
    21.10.2025 08:11

    -пакеты по 320 байт

    То есть вы взяли gRPC/protobuf, только чтобы кидаться бинарными пакетами по 320 байт? Странное решение.

    Тут бы подошёл обычный советский TCP. Отключаем Nagle (NoDelay = true) и вперёд кидаться друг друга пакетами практически безлимитно.

    Протобуф - он какбы не для этого. Это суровый сериализатор сложных структур. И он отлично и быстро работает. Но вам он зачем? Если нет структуры - не нужен и сериализатор.


    1. Mihail37 Автор
      21.10.2025 08:11

      Не совсем так. Голый TCP - это стандарт Asterisk а, и мы должны были его придерживаться только в первой точке в цепочке по работе с голосом, которая взаимодействует именно с Астером. Далее - по GRPC цепочке - пакеты можно было укрупнять и обогащать, увеличивая задержки + появляются сквозные идентификаторы, признаки попутного анализа пакетов голоса (например, есть ли в них речь или пакет пустой), те в структуре GRPC пакетов уже не только голые аудио-байты. К тому же же yandex-speech-recognition или text-to-speech работают по протоколу GRPC.


      1. Kerman
        21.10.2025 08:11

        Вам бы неплохо было бы определиться с требованиями к системе. gRPC работает через протобуф, протобуф - через https. Последний работает через TCP. Угадайте, включен ли у него алгоритм Нагла? Спойлер - он включен у всех по умолчанию. Соответственно, у вас будут задержки, лишние буфферы, лишний расход цпу. Если вас задержки не устраивают, то у вас путь один - к TCP. Это не значит, что нельзя "обогащать пакеты". Можно и ещё как. Простую информацию можно встраивать прямо в пакет (ну кто запретит пару байт с флагами вписать?), сложную и несинхронную оставить на gRPC.


        1. Mihail37 Автор
          21.10.2025 08:11

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

          Тут скорее вопрос к реализации сетевого стека в части GRPC на .net под Linux, если при прочих равных такое же решение на GO прекрасно едет.

          Так как в реальном проекте главное - это именно решить проблему, с гарантией, что лучше чем пытаться тюнить текущий стек «на костылях». Хотя именно в .Net такую критику - что "вы не умеете его готовить" - по данной проблеме, встречаю постоянно)


          1. Kerman
            21.10.2025 08:11

            Я не знаю, почему у вас не едет дотнет под линукс. Я давно и плотно работаю с gRPC и у меня всё прекрасно едет. Что дотнет, что линукс, что gRPC - технологии, отлаженные годами. И это мейнстрим. И я сильно сомневаюсь, что в их связке есть какие-то проблемы. И я сильно сомневаюсь, что Го намного быстрее. Скорее всего вы умалчиваете какой-то интересный факт, а может и не заметили что-то.

            Хотя именно в .Net такую критику - что "вы не умеете его готовить" - по данной проблеме, встречаю постоянно)

            Я вам то же самое скажу. Вы не умеете его готовить. Прекрасно работает под большими нагрузками. В том числе и под линуксом.


            1. Mihail37 Автор
              21.10.2025 08:11

              Могу добавить что информация актуальна на конец 2022, может быть в .Net 8 - более стабильной мажорной версии - это поведение поменялось в лучшую сторону.

              PS помню еще .net 5, где попытки обратиться к GRPC-стриму, который был переведен в Dispose состояние (программистские ошибки, да), но все же такой баг в коде приводил к дедлокам на уровне платформы и stopper-у всего приложения.

              Так что в те времена складывалось впечатление о "сырости" .net-ного GRPC.


              1. Kerman
                21.10.2025 08:11

                В конце 22го был уже .NET 6. Который LTS. И я на нём строил сервер для приложения на основе gRPC/protobuf. Под линуксом, да. И работал он прекрасно, полностью выжимая канал. Без перерасходов памяти, без сильной нагрузки на cpu. Без дедлоков. Хотя я не пробовал никогда лезть в задиспоженый стрим. Это как-то странно.

                По мотивам той системы я уже писал статью на хабре.

                Так что не знаю. Похоже, у нас разные дотнеты, разные линуксы и разные gRPC.