Real-Time Backend
gRPC Streaming
2018 год. Discord переходит с REST на gRPC streaming для внутренней communication между сервисами голосового чата. Результат: latency для voice setup упала с 200 мс до 40 мс, а количество TCP-соединений между микросервисами уменьшилось в 12 раз благодаря HTTP/2 мультиплексированию. Но к веб-клиентам Discord по-прежнему ходит через WebSocket - потому что браузеры не умеют gRPC. Эта двухпротокольная архитектура - не временное решение, а зрелый паттерн: gRPC streaming идеален для server-to-server, WebSocket остаётся лучшим выбором для browser-facing UI. Знать, когда какой - это знание разделяет middle и senior backend engineer.
- **Google Cloud Speech-to-Text**: bidirectional gRPC streaming - клиент шлёт PCM, сервер возвращает partial transcripts параллельно
- **Netflix metrics pipeline**: gRPC server-streaming от ~3000 микросервисов в централизованный aggregator, бинарный protobuf даёт 5-10x compact против JSON
- **Discord voice services**: gRPC bidi между сервисами, WebSocket к веб-клиенту - two-protocol architecture
- **Anthropic Claude API**: SSE (не gRPC) для streaming tokens браузеру - browser native, простота отладки, гибкость edge proxy
Server streaming: один запрос - поток ответов
Конкретный кейс: дашборд аналитики, который рисует обновления продаж в реальном времени. Клиент один раз отправляет запрос *"подпишись на канал sales-eu, регион FR"* - и сервер всю сессию шлёт обновления. Это **server streaming RPC**. В контракте `.proto` пишется один аргумент `keyword stream` напротив возвращаемого типа: `rpc SubscribeSales (SubscribeRequest) returns (stream SalesUpdate);`. На уровне HTTP/2 происходит следующее: клиент открывает один stream (то есть пару sequential id-frames), отправляет HEADERS + DATA с request body, потом сервер удерживает этот stream открытым и шлёт DATA frame за DATA frame с каждым `SalesUpdate`. END_STREAM ставится либо сервером (нормальное завершение), либо клиентом через cancel. Та же логика, что в SSE - но с бинарным protobuf вместо текста и со встроенным flow control HTTP/2.
Главное отличие от SSE - **flow control из коробки**. HTTP/2 имеет window-based credit на каждый stream: получатель объявляет, сколько байт он готов принять, отправитель не превышает. Если клиент медленный - сервер автоматически замедляется, без custom backpressure кода на сервере. В SSE такого нет: сервер шлёт текст в TCP buffer, и если клиент не успевает читать, у вас растёт kernel buffer и в итоге сервер блокируется на write(). В gRPC - предсказуемая модель: stream window 64KB по умолчанию, можно поднять до 16MB через `WithInitialWindowSize`. Аналогичная проблема возникает в ML inference серверах: если вы стримите токены LLM клиенту через HTTP/2, gRPC контролирует темп, а TCP-only - нет.
Канонический use-case server-streaming - дашборды и feeds: рассылка котировок, push-уведомления внутри корпоративной сети, server-sent ML inference (например, Anthropic Claude шлёт токены через streaming HTTP - это аналогичный паттерн, только на REST). Netflix использует gRPC server-streaming для метрик из ~3000 микросервисов в реальном времени, потому что бинарный protobuf даёт в 5-10 раз меньше bandwidth, чем JSON. На собеседовании в Netflix/Google полезный сигнал - упомянуть, что server-streaming не подходит для UI пользователей через интернет (нет browser support без gRPC-Web, которое имеет ограничения), но идеален для внутренней microservice-to-microservice связи.
В чём принципиальное отличие server-streaming RPC от обычного HTTP polling?
Client streaming: поток запросов - один ответ
Зеркальный паттерн: клиент шлёт **поток сообщений**, сервер отвечает один раз в конце. Контракт: `rpc UploadMetrics (stream Metric) returns (UploadResponse);`. Применение - агрегация: клиент стримит тысячи метрик с устройства, сервер копит их, делает aggregation, возвращает один итог. Без streaming пришлось бы либо посылать один огромный request body (плохо для interruption и для memory), либо тысячу отдельных RPC (overhead на handshake каждый раз). Client-streaming решает обе проблемы. В реальности Google использует client-streaming для загрузки telemetry с Android устройств: 100K событий в секунду, batched по 50 в один stream, и сервер агрегирует на лету через windowed aggregation.
Тонкое место: ошибка в середине stream. Если клиент послал 500 событий из 1000 и упал, сервер должен решить - сохранить эти 500 или откатить всё. По gRPC семантике сервер получает `io.EOF` только если клиент успешно вызвал `CloseSend()`; обрыв соединения даёт `code.Unavailable`. Поэтому корректный обработчик на сервере **всегда** должен идемпотентно обрабатывать стрим: каждое событие имеет client-side `event_id`, сервер дедуплицирует. Это та же логика, что и в Kafka producer с `enable.idempotence=true`: один сбой не должен приводить к дубликатам или потерям. И та же логика, что в ML training, когда чекпоинты должны переживать pre-emption GPU узлов - идемпотентность шагов превыше всего.
На собеседовании в Stripe/Shopify любят спросить: *"Чем client-streaming gRPC лучше batch HTTP POST с массивом в body?"* Сильный ответ - три причины. **Первая**: batch POST требует, чтобы клиент собрал всё в памяти; client-streaming позволяет производить и слать события on-the-fly (constant memory). **Вторая**: HTTP/2 flow control тормозит производителя если сервер медленный - в batch POST вы либо timeout, либо OOM на клиенте. **Третья**: при прерывании в batch POST вы теряете весь request; в client-streaming с server-side idempotent processing вы теряете только окно, которое сервер ещё не успел подтвердить. Тот же паттерн в data engineering: streaming pipelines (Flink, Spark Streaming) выигрывают у batch именно потому, что constant memory + bounded latency.
Когда client-streaming RPC предпочтительнее единого batch HTTP POST с массивом сообщений?
Bidirectional streaming: двусторонний канал
Финальный паттерн - **bidirectional streaming**: оба направления независимы, клиент и сервер шлют сообщения параллельно. Контракт: `rpc Chat (stream ChatMessage) returns (stream ChatMessage);`. Это полный full-duplex канал поверх одного HTTP/2 stream. Применение - реальный chat, voice assistant (Google Assistant отправляет аудио чанки клиент->сервер, получает обратно partial transcripts и responses), и distributed reconciliation (peer-to-peer sync через центральный gRPC хаб). Семантика: оба endpoint'а могут писать и читать независимо; END_STREAM на одной стороне не закрывает другую. Это то же поведение, что у WebSocket, только с типизированным protobuf вместо raw bytes и с авто-managed flow control на каждое направление отдельно.
Каноничный кейс - voice assistant. Клиент непрерывно стримит 16kHz PCM-чанки по 20ms (3.2KB/сек) на сервер; сервер параллельно стримит обратно partial transcripts (текст) и финальные responses (синтезированное аудио). Если бы это было два отдельных RPC - сервер не смог бы 'перебить' клиента ('подожди, я не понял'), потому что клиент в момент upload не слушает downstream. Bidirectional streaming разделяет эти два потока на программном уровне, но они идут по одному HTTP/2 stream - один TCP-соединение, один TLS handshake, один auth check. Google Cloud Speech-to-Text API именно так устроен. Ту же архитектуру использует OpenAI Realtime API для голоса с GPT-4, только поверх WebSocket - и это иллюстрация того, что выбор между gRPC bidi и WS - вопрос экосистемы, а не возможностей.
Подвох на собеседовании staff: *"Чем gRPC bidi отличается от 'просто двух server-streams в обе стороны'?"* Сильный ответ - **общий контекст и cancellation**. Один stream означает один gRPC context: если клиент отменяет, сервер сразу прекращает обработку на обеих сторонах. Если бы было два независимых stream - пришлось бы вручную координировать cancellation через third-party канал (например, через context_id в header). Эта общая идентичность - конкретное преимущество bidi перед 'двумя стримами'. Та же логика наблюдается в трактовке транзакций: один RPC vs два независимых - то же различие между ACID транзакцией и saga-паттерном; первое атомарнее, второе масштабируемее.
Какое уникальное свойство bidirectional streaming, которого нет у комбинации 'server-stream + client-stream'?
gRPC streaming vs WebSocket: когда что
Финальный и самый практичный вопрос - **когда gRPC, когда WebSocket?** Тех. отличия. WebSocket - upgrade с HTTP, после чего raw frames любого формата (text/binary). gRPC streaming - HTTP/2 stream с typed protobuf. Это даёт **gRPC**: типизированный контракт (`.proto` - schema, генерация client/server), built-in compression (gzip/snappy), flow control (HTTP/2 window), мультиплексирование нескольких stream через одно соединение. И даёт **WS**: universal browser support, простота для legacy frameworks, любой формат wire (binary, JSON, MessagePack), хорошая видимость в DevTools для отладки, простая интеграция через CDN/Cloudflare/AWS API Gateway. Tesla для in-car telemetry использует gRPC streaming (microservices-to-microservices), но для дашборда Customer App - WebSocket, потому что браузеры iOS/Android клиента нативно поддерживают WS.
Решающий критерий часто - **окружение**. Browser-facing UI - WS (нативная поддержка, нет need в gRPC-Web переходниках). Server-to-server в kubernetes mesh - gRPC streaming (мультиплексирование экономит TCP-соединения, типизация ловит ошибки на compile-time). Mobile native - gRPC отлично работает (Android Java + iOS Swift имеют официальные клиенты), но WS тоже работает; выбор по экосистеме (если бэкенд уже на gRPC, продолжать; если REST/JSON - WS логичнее). IoT - часто MQTT или CoAP, а не оба варианта. На собеседовании в Anthropic, OpenAI, Cohere ML инженеру могут задать конкретный кейс: *"streaming LLM tokens к пользователю в браузере"* - и правильный ответ WS (или SSE), потому что browser native; gRPC-Web ради этого ставить избыточно.
Тонкий вопрос - **gRPC-Web**. Это спецификация для запуска gRPC из браузера через прокси (Envoy транслирует HTTP/1.1 в HTTP/2). На бумаге - решение совмещения миров; на практике у gRPC-Web есть ограничения. Server-streaming работает; bidirectional streaming **не работает** (только server-streaming или unary над HTTP/1.1 + chunked). Поэтому если архитектура требует bidi - выбор сужается до 'gRPC между сервисами + WS для браузера' (двухпротокольная архитектура). Discord, Twitch, Cloudflare используют именно такую схему: gRPC внутри mesh, WS наружу к клиенту. Не идеально, но честно отражает то, что 'один протокол на всё' - миф.
gRPC streaming и WebSocket - это конкуренты, и один полностью заменяет другой
gRPC streaming и WebSocket - это два разных слоя стека с разными economic equilibria. gRPC выигрывает там, где есть типизированный контракт между сервисами (server-to-server, mobile native), мультиплексирование экономит соединения, и flow control нужен из коробки. WebSocket выигрывает там, где есть browser-facing UI, нужна гибкость wire format, или CDN/edge сервер в пути. Современные крупные системы используют оба одновременно: WS на границе с пользователем, gRPC внутри mesh, и edge gateway транслирует между ними. Это honest two-protocol architecture, а не недостаток одного из протоколов.
Архитектурные решения 'один протокол на всё' - типичный паттерн junior-инженера. Реальные системы (Discord, Twitch, Tesla, Cloudflare) живут в multi-protocol среде, потому что разные слои стека оптимизируются под разные ограничения: browser API, CDN routing, internal observability, type safety. Способность выбрать правильный протокол в правильном месте - сигнал staff-уровня инженера на собеседовании.
Связанные темы
gRPC streaming пересекается с несколькими ключевыми темами realtime-стека:
- WebSocket — Главный конкурент gRPC bidi для двусторонних каналов; разные слои стека, разные оптимизации
- SSE — Однонаправленный аналог server-streaming; SSE проще, gRPC даёт типизацию
- Экосистемы realtime фреймворков — Action Cable, Phoenix, SignalR, Centrifugo - все имеют свой выбор протокола
- Ограничения HTTP — gRPC streaming живёт поверх HTTP/2, продолжает решение проблем HTTP/1.1
Ключевые идеи
- **Server streaming**: один запрос - поток ответов через `stream` в .proto, HTTP/2 flow control автоматически предотвращает overrun медленного клиента
- **Client streaming**: поток запросов - один ответ; require идемпотентности через event_id, чтобы переживать обрывы; пример - upload telemetry, Google Android делает 100K events/sec
- **Bidirectional streaming**: full-duplex поверх одного HTTP/2 stream, общий context для cancel/timeout/auth; voice assistants - канонический use-case
- **gRPC vs WS**: gRPC для server-to-server с типизацией и mux; WS для browser-facing UI с native поддержкой; современные системы (Discord, Twitch) используют оба одновременно
Вопросы для размышления
- Если ваш backend ещё на REST/JSON, какова реальная цена миграции на gRPC streaming для inter-service комуникации - в человекочасах, в риске, в observability lock-in?
- Anthropic, OpenAI, Cohere используют SSE для streaming LLM tokens вместо gRPC streaming. Почему - и какие из аргументов 'SSE проще для браузера' остались бы валидны, если бы все клиенты были mobile native?
- Discord использует gRPC mesh + WS edge gateway. Какие операционные проблемы создаёт такая two-protocol архитектура (трассировка, метрики, deployment) и какие компромиссы стоит за это платить?
Связанные уроки
- rt-04 — WebSocket - конкурент gRPC streaming; сравнение определяет выбор
- rt-03-sse — SSE - однонаправленный аналог server-streaming
- rt-13 — Знание Action Cable / Phoenix / SignalR / Centrifugo для контекста сравнения
- rt-02-http-limits — gRPC живёт поверх HTTP/2 - продолжение разбора HTTP-ограничений
- ds-04-consistent-hashing — gRPC client-streaming + балансировка в шардированных бэкендах
- ml-28-optimizers — Streaming inference в ML - тот же паттерн backpressure
- net-54-rpc