プロトコル

gRPCストリーミングとは?4方式の使い分けと切断対策を実装者向けに解説

GCPを利用したインフラ構築

1回のリクエストに1回の応答を返すだけなら悩む余地はありません。困るのは、結果が出るまで数十秒かかる処理の進捗を返したいとき、数十万件を1件ずつ送りたいとき、サーバー側から任意のタイミングで通知を押し出したいときです。gRPCはこの3つを、1本のRPCの中で複数メッセージを流す仕組みとして標準で持っている。扱う範囲は、4方式の違いと選び分け、streamキーワードの置き位置、書き込みが止まる仕組み、切断時の後始末までです。プロトコルそのものの基礎はgRPCの仕組みとRESTとの違いに譲り、ここでは方式選定と運用に絞ります。

まとめ|先に決める3項目とストリーミングが効く境界

着手前に決めるのは3つです。どの方式を使うか(送る向きと本数)。1メッセージの粒度と上限をいくつにするか。そして切断したときに誰がどこから再送するか。3つ目を決めずに書き始めた実装は、開発環境で動いても経路にプロキシが1枚入った瞬間に取りこぼしが出ます。gRPCは切れたストリームを自動で復元しません。

効くのは、送るメッセージが複数あり、接続の寿命が数十秒以上あり、受け側が届いた順に処理を始められる場面。この3つが揃うならストリーミングを選ぶ価値があります。逆に、1往復で終わる短い呼び出しにストリーミングを被せると、確定タイミングの制御と再接続の実装だけが増えて得るものがありません。ブラウザから直接叩きたい要件も、方式によっては成立しないため先に確認が必要になるでしょう。

gRPCストリーミングとは|1回のRPCで複数メッセージを流す通信方式

4つのRPC方式|unary・サーバー・クライアント・双方向

gRPC公式のCore conceptsは、RPCのライフサイクルを4種類に整理しています。unary(1対1)、サーバーストリーミング(要求1・応答N)、クライアントストリーミング(要求N・応答1)、双方向ストリーミング(要求N・応答N)の4つです。ストリーミングと呼ばれるのは後ろの3つで、「1本のRPCの中で複数メッセージを順に流す」点は共通です。

方式 要求 応答 代表的な用途
unary 1 1 通常の取得・更新
サーバー 1 N 進捗通知・大量取得
クライアント N 1 分割送信・集計
双方向 N N 対話・常時接続の制御

4つとも同じHTTP/2の上で動き、同じ.protoから生成され、認証もインターセプタも共通の仕組みに乗ります。切り替えのコストは、思っているより定義側に寄っています。

保証されるもの・されないもの|順序保証と2本のストリームの独立性

公式ドキュメントは「gRPC guarantees message ordering within an individual RPC call」と書いています。つまり、1本のRPCの中では送った順に届きます。ここは自前でシーケンス番号を振らなくても成立する部分です。

一方で保証されないものもはっきりしています。まず、別々のRPC間の順序は保証されません。並行して2本のストリームを開いたなら、到着順は制御外です。次に双方向では「the two streams are independent, so clients and servers can read and write in whatever order they like」とされ、要求と応答の対応づけは自分で設計する必要があります。要求1件に応答1件が返る前提でコードを書くと、サーバー側が2件まとめて返した瞬間に破綻するでしょう。

もう1つ、成功判定は両側で独立しています。サーバーが処理を完了したつもりでも、クライアント側ではデッドライン超過で失敗として扱われることがある。この食い違いは仕様どおりの挙動なので、片方のログだけを見て原因を探しても答えは出ません。

HTTP/2の1本のストリームに載るという設計上の意味と制約

gRPCの1回のRPCは、HTTP/2の1本のストリームに対応します。1つのTCPコネクションの上で多数のストリームが多重化されるため、ストリーミングRPCを10本開いてもコネクションが10本になるわけではありません。接続確立の往復やTLSハンドシェイクを繰り返さずに済むのは、この構造から来ています。

裏側もあります。コネクションを共有する以上、それが切れれば載っていた全ストリームが同時に落ちます。長時間つなぎっぱなしにする設計では、この点を先に潰しておきたいところです。

4方式の選び分け|どの場面でどれを選ぶかを実データ量と寿命で決める

サーバーストリーミング|進捗通知と逐次生成の結果を先に届ける

要求は1回、応答が複数返る方式です。使いどころは2つ。1つは、結果全体が確定するまで待たせたくない場面で、バッチの進捗、ログの追従、生成モデルのトークンを出来た端から返す用途が入ります。もう1つは、全件を1メッセージに詰めると上限を超える場面。10万件の一覧を1回で返す代わりに、1,000件ずつ100回に分けて流します。

後者では、受け側が届いた分から処理を始められるかどうかが分かれ目になります。すべて受け取ってから一括処理するなら、ページング付きのunaryと実質的な差は出ず、エラー処理はページングのほうが素直でしょう。

クライアントストリーミング|分割送信と受け側での集計処理に向く

要求が複数、応答が1回の方式です。大きなファイルを固定長のチャンクに割って送る、センサーの計測値を送り続けて最後に集計結果を1つ受け取る、といった形が典型になります。

注意したいのは、応答が最後に1回しか返らない点です。サーバーが最後まで受け取ってから失敗を返す実装にすると、不正なメッセージが2件目に混ざっていてもクライアントは全量を送り切ります。検証に失敗した時点でRPCを終了させ、送信側もエラーを検知して打ち切る作りにしておきたいところです。

双方向ストリーミング|制御チャネルと対話型の常時接続で使う場面

両方向とも複数メッセージを流せる方式で、4つの中でいちばん自由度が高く、いちばん設計の負担が重いものです。エージェント常駐型の管理サーバー、対話形式のやり取り、購読と配信を1本にまとめた通知チャネルなどで使われます。

自由度の代償は、対応づけと終了条件を自分で決めなければならない点にあります。要求と応答を紐づけるIDを載せるのか、順序で暗黙に対応させるのか。どちらが先にストリームを閉じるのか。これらを決めずに実装を始めると、正常系だけ動いて異常系で固まるコードになります。なお、多対多の配信が主目的ならMQTTによるPub/Subの常時接続のほうが素直に収まる場面もあります。

unaryのままでよい条件|ストリーミングを選ばない判断の線引き

メッセージが1件で足りるなら、ストリーミングにする理由はありません。加えて、応答が数百ミリ秒で返る、1メッセージが数百KB以内に収まる、クライアントがブラウザでgRPC-Web経由の呼び出しになる、のいずれかに当てはまるならunaryを選んでおくほうが運用は軽くなります。とりわけ最後の条件は仕様上の制約で、grpc-web公式は「Client-side and Bi-directional streaming is not currently supported」と明記しています。ブラウザから直接使えるのはunaryとサーバーストリーミング(grpcwebtextモード)だけです。

実装|protoのstream指定と各言語で受け取るインタフェースの形

protoの書き方|streamキーワードを置く位置で方式が決まる

方式の指定は.protoの1行だけで完結します。引数側にstreamを付ければクライアントストリーミング、戻り値側ならサーバーストリーミング、両方に付ければ双方向です。

service Telemetry {
  rpc GetOnce(Query) returns (Sample);
  rpc Watch(Query) returns (stream Sample);
  rpc Upload(stream Chunk) returns (UploadResult);
  rpc Session(stream Command) returns (stream Event);
}

ここを後から変えると生成コードのシグネチャが変わり、既存クライアントは動かなくなります。既存メソッドのstreamを足し引きするのではなく、新しいメソッドを追加して移行させる形が安全でしょう。

Goでの実装|サーバー側のSendとクライアント側のRecvの流れ

grpc-goでは、サーバーストリーミングのハンドラは第2引数にストリームを受け取り、戻り値を返した時点でRPCが終了します。Sendを呼ぶたびに1メッセージが流れます。

func (s *server) Watch(q *pb.Query, stream pb.Telemetry_WatchServer) error {
    for _, v := range s.collect(q) {
        if err := stream.Send(v); err != nil {
            return err
        }
        if stream.Context().Err() != nil {
            return stream.Context().Err()
        }
    }
    return nil
}

クライアント側はRecvを繰り返し、io.EOFで正常終了、それ以外のエラーで異常終了と判断します。この2つを同じ扱いにすると、途中で切れたのか最後まで受け取ったのかが区別できません。stream.Context().Err()の確認は、クライアントが去った後も送り続けないための保険です。

Pythonでの実装|ジェネレータで送受信を書くときの注意点

Pythonではサーバーストリーミングのハンドラがジェネレータになり、yieldした値が1メッセージとして流れます。クライアントストリーミングでは逆に、引数として渡されたイテレータを回して受け取ります。

class Telemetry(pb_grpc.TelemetryServicer):
    def Watch(self, request, context):
        for v in collect(request):
            if not context.is_active():
                return
            yield v

    def Upload(self, request_iterator, context):
        total = sum(len(c.payload) for c in request_iterator)
        return pb.UploadResult(bytes_received=total)

同期版のPython実装では、ハンドラ1本がスレッドを1つ占有します。長寿命の双方向ストリームを多数受けるなら、スレッドプールの上限がそのまま同時接続数の上限になる。接続数が伸びる見込みなら非同期版(grpc.aio)を選んでおくほうが後で楽です。

詰まりどころ|フロー制御とバックプレッシャで書き込みが止まる

書き込みがブロックする仕組み|受信側の処理速度が上限を決める

公式はフロー制御を「Flow control is a mechanism to ensure that a receiver of messages does not get overwhelmed by a fast sender」と説明しています。受信側が読み進めると送信側へ余力の通知が戻り、余力がなくなると「the gRPC framework will wait before returning from a write call」=書き込み呼び出しがその場で待たされます。

この挙動を知らないと、送信ループが遅い原因をネットワークやCPUに探しに行きがちです。送信側のスループットは受信側の処理速度で決まる、と考えて計測箇所を選ぶと切り分けが早く済みます。なお、フロー制御が関係するのはストリーミングRPCだけで、unaryには適用されません。

同期の読み書きで起きるデッドロック|双方向で踏みやすい落とし穴

公式ドキュメントには明確な警告があります。「There is the potential for a deadlock if both the client and server are doing synchronous reads or using manual flow control and both try to do a lot of writing without doing any reads」。双方が読まずに書き続けると、互いの受信バッファが埋まって両方の書き込みが戻らなくなる、という状態です。

回避策は単純で、双方向ストリームでは送信と受信を別の実行単位に分けます。Goなら受信ループをgoで切り出す、Pythonなら非同期版でタスクを分ける、という形です。1つのループの中で送って受けてを交互に書く実装は、片側が想定より多く送ってきた瞬間に固まります。

メッセージ上限4MiBの既定値とチャンク分割の粒度を決める基準

gRPC Coreのチャネル引数リファレンスによると、GRPC_ARG_MAX_RECEIVE_MESSAGE_LENGTHの既定値は4MB(4×1024×1024バイト)で、-1を指定すると無制限になります。ストリーミングでもこの上限は1メッセージごとに効くため、大きなデータは分割が前提です。

上限を引き上げて1メッセージを巨大にする対処は、受信側がメッセージ全体をメモリに載せる構造を変えないため、同時接続が増えた分だけメモリを食う。分割の粒度は64KBから256KB程度で試し、実測で決めるやり方が扱いやすいでしょう。遅延の大きい経路でスループットが伸びないときは、GRPC_ARG_HTTP2_STREAM_LOOKAHEAD_BYTES(既定64kb)を広げると改善する場合があります。

切断と再接続|長寿命ストリームを運用に載せるための設定と設計

keepaliveの既定値|クライアントは無効・サーバーは2時間

数分から数時間ストリームを張り続ける設計では、既定値のままだと沈黙した接続が検知されません。公式のkeepalive設定表では、クライアント側のKEEPALIVE_TIMEはINT_MAX(=既定で無効)、KEEPALIVE_TIMEOUTは20000ms、KEEPALIVE_WITHOUT_CALLSは0。サーバー側はKEEPALIVE_TIMEが7200000ms(2時間)、PERMIT_KEEPALIVE_TIMEが300000ms(5分)です。

ここで踏みやすいのが、クライアントのping間隔をサーバーのPERMIT_KEEPALIVE_TIMEより短くしてしまう組み合わせです。サーバーは許容間隔より頻繁なpingを不正とみなして接続を切りにきます。クライアント側でpingを有効にするなら、サーバー側の許容値も併せて調整するのが前提。通話がない間もpingを送るかどうかは両側のWITHOUT_CALLS系の設定で決まり、片側だけ有効にしても噛み合いません。

デッドラインとキャンセル|途中まで進んだ副作用は自動で戻らない

クライアントはRPCごとにデッドラインを指定でき、超過するとDEADLINE_EXCEEDEDで終了します。長寿命の双方向ストリームでは、この値を短く設定すると正常な接続まで切られるため、方式ごとに使い分けが要ります。

キャンセルの意味論には注意が必要です。公式は「Changes made before a cancellation are not rolled back」と書いている。クライアントストリーミングで500件送った時点で切れたなら、その500件はサーバー側に届いており、書き込み済みなら残ります。巻き戻したいなら、アプリケーション側でトランザクション境界を設けるか、後述の冪等キーで重複を吸収するかのどちらかを選びます。

自動リトライの効かない範囲|応答ヘッダ受信で確定してしまうため

gRPCにはサービス設定で書ける自動リトライがあり、maxAttemptsinitialBackoffmaxBackoffbackoffMultiplierretryableStatusCodesを指定します。待ち時間には±20%のジッタが入り、retryThrottlingmaxTokenstokenRatio)でサーバー単位の再試行を絞る仕組みもあります。

ただし境界がはっきりしています。公式の記述は「Once the response header is received, the RPC is committed. No further retries will be attempted」。応答ヘッダを受け取った時点でRPCは確定し、以後は再試行されません。サーバーストリーミングは最初の応答が返った直後にこの状態へ入るため、100件目で切れたケースは自動リトライの対象外です。長いストリームの復旧は、フレームワーク任せにはできないと考えておくのが実際に近いでしょう。

再開設計|再開トークンと冪等キーで取りこぼしを防ぐための実装手順

そこで、切れた続きから再開する仕掛けを自分で用意します。手順は3つ。まずサーバーからのメッセージに、続きの位置を示すトークン(連番・カーソル・タイムスタンプのいずれか)を載せる。次にクライアントは最後に処理し終えたトークンを保持し、再接続時の要求に載せて送ります。最後にサーバーが、受け取ったトークンの次から流し直します。

送信方向にも同じ設計が要ります。クライアントストリーミングで再送すると切断前に届いた分と重複するため、メッセージごとに冪等キーを持たせて受け側で落とす。保証水準が「少なくとも1回」か「ちょうど1回」かを先に決めておけば、この判断で迷いません。ちょうど1回を通信層で達成しようとせず、重複を許して受け側で畳む形に寄せるほうが実装は落ち着きます。

採用の境界|双方向ストリーミングを選んでよい条件と見送る場面

採用してよい3条件|寿命・件数・遅延の3つが揃っているときだけ

双方向ストリーミングを選んでよいのは、次の3つが同時に成り立つ場合だと考えています。接続の寿命が数十秒以上あること。片方向あたり複数のメッセージが行き交うこと。そして、往復ごとに接続を張り直す遅延が要件上許容できないこと。3つとも当てはまるなら、実装の複雑さに見合う効果が出ます。

2つしか当てはまらないなら片方向で足ります。対応づけと終了条件の設計が要らないぶん、事故が起きる箇所が減る。1つしか当てはまらない場合はunaryで書き、必要になってから方式を足すほうが早く終わるでしょう。

見送る場面|ブラウザ直・プロキシ越え・短命リクエストの3パターン

逆に見送りを勧める場面は3つあります。1つ目はブラウザから直接呼ぶ要件です。クライアントストリーミングと双方向はgrpc-webが対応しておらず、プロキシを挟んでも方式そのものが成立しません。ブラウザで双方向の通信が要るなら、HTTP/3上で双方向通信を扱うWebTransportのような別系統を検討する順番になります。

2つ目は、経路に手を入れられないプロキシやゲートウェイが挟まる構成です。応答を最後までバッファしてから返す中間装置が1つでもあると、逐次配信の利点は消える。3つ目は1往復で終わる短命なリクエストで、ここにストリーミングを被せても終了の合図と再接続の実装が増えるだけです。方式を足すのは、unaryで測って足りないと分かってからで遅くありません。

受託開発での取り決め|切断時の責務と再送の合意を契約前に決める

受託でストリーミングAPIを作る場合、見積書に「リアルタイム通信機能一式」とだけ書かれることがあります。この書き方は後で揉める。切断はネットワーク側の事情でも起きるため、どこまでを実装側の責務とするのかが曖昧なまま検収に入るからです。

先に文書化しておきたいのは3点です。再接続を誰が実装するか(クライアント側の責務か、提供するSDKに含めるか)。取りこぼしの保証水準を「少なくとも1回」とするか「ちょうど1回」とするか。そして、切断が一定回数を超えたときを障害と数えるかどうか。この3つを着手前に決めておけば、運用開始後に判断できる人が不在という事態を避けられます。ストリーミングを含むAPI設計から実装、切断時の運用設計までの相談はAPI開発・システム連携で受け付けています。

よくある質問

gRPCストリーミングとWebSocketはどちらを選ぶべきですか?

サーバー間の通信ならgRPCを、ブラウザとの双方向ならWebSocketを起点に考えると整理しやすくなります。gRPCは.protoで型が定まり、生成コードで両側の実装が噛み合う点が強み。WebSocketは枠組みだけを提供するので、メッセージ形式も順序も再接続も自分で決めます。ブラウザ側の要件が双方向なら、gRPCの双方向は成立しないため実質的にWebSocketか別系統を選ぶことになります。

ストリーミングの途中でエラーを返すにはどうしますか?

サーバー側はハンドラからステータス付きのエラーを返せば、その時点でRPCが終了します。すでに送ったメッセージは取り消せないため、クライアント側は「途中まで受け取った状態でエラーになった」ケースを扱える構造にしておきます。部分的な結果を捨てるか、途中まで確定させて再開トークンから続けるかは設計事項です。

1つのストリームで何件くらいまで流せますか?

件数そのものに仕様上の上限はありません。効いてくるのは1メッセージの上限(既定4MB)と、接続を維持できる時間のほうです。数十万件を1本で流すと、途中で切れたときの再送コストが件数に比例して膨らむ。数千件ごとに区切って複数のRPCに分け、区切りごとに確定させるやり方が扱いやすいでしょう。

Rustでの実装はどの選択肢が使えますか?

公式ブログによると、2026年5月21日にTonicがCNCF傘下のgRPCプロジェクトへ合流し、同年5月28日にgRPC-Rustの最初のプレビュー版が公開されています(2026年8月時点)。ただしプレビュー版は本番利用を推奨しないと明記されているため、いま新規に着手するならTonic系を選ぶ判断になるでしょう。

ロードバランサ配下でストリーミングを使うときの注意点は?

接続単位で分散する構成では、長寿命ストリームが特定のサーバーに固定されて偏りが出ます。加えて、アイドルタイムアウトを持つ装置が経路にあると、メッセージが流れない間に切断されます。対策は、ping間隔を装置のタイムアウトより短くしておくこと、サーバー側で接続の最大寿命を設けて計画的に切り直すこと、そして偏りを前提に容量を見積もることの3点です。

関連記事

資料請求

RELATED POSTS 関連記事