Pythonの公式チュートリアルどおりに写経すると、gRPCのサーバとクライアントは30分で動きます。詰まるのはその先です。生成したファイルをパッケージの下に移した途端にimportが通らない、同期サーバのまま本番に出して同時接続で頭打ちになる、すでにFastAPIで動いているサービスにどう同居させるか決まらない。この記事はその3つを扱います。プロトコルの仕組みそのものはgRPCとは何かとRESTとの違いに譲り、ここではPythonでの実装工程だけに絞ります。
まとめ|実装に入る前に決める3つと最も詰まる1点
着手前に決めるのは3つです。サーバを同期で書くかgrpc.aioで書くか。生成したスタブをリポジトリに入れるか毎回生成するか。そしてHTTPのAPIと同じプロセスに置くか別プロセスに分けるか。この3つは後から変えると、import文・起動スクリプト・デプロイ単位まで芋づるで書き直しになります。
最も詰まるのはprotocが生成するコードのimportです。生成物は同じディレクトリに並んでいる前提の絶対importを吐くため、パッケージ配下に置くと解決できません。これはgRPCのリポジトリで長く報告されている既知の挙動で、2026年8月時点でも生成器側の既定は変わっていない。回避の型を先に決めておけば、後述の3工程は素直に進みます。
実装前に決める3項目|同期かasyncか・生成物の置き場・併存の形
grpcioとgrpcio-toolsの役割分担|実行時と生成時でパッケージが別
Pythonでは2つのパッケージの導入が必要です。grpcioが実行時のライブラリで、チャネル・サーバ・インターセプタなどの実体はこちらに入っています。grpcio-toolsは.protoからコードを起こすためのコンパイラで、protoc本体とPython向けプラグインを同梱しています。
この分離を意識せずにrequirements.txtへ両方を並べると、本番コンテナに生成器まで入ってしまう。生成をCIやビルド段階で済ませる構成なら、grpcio-toolsは開発用の依存に置くほうが実行イメージは軽くなります。逆に、起動時に.protoを読んで動的にスタブを組み立てる構成を採る場合は、実行環境にも生成器が必要です。
python -m pip install grpcio grpcio-tools
対応Pythonのバージョンと1.83系の位置づけを先に確認する
PyPI上のgrpcioは1.83.1が2026年8月28日公開で、要求するPythonは3.10以上と表示されています(2026年9月1日時点)。3.9以前が残っている環境では、gRPCの導入より先にランタイム側の更新計画が要る。ホイールはCPython 3.10から3.14まで用意されているため、この範囲であればビルドツールチェーンなしで入ります。
バージョン固定の粒度も先に決めておきます。grpcioとgrpcio-toolsとprotobufは互いに整合が要るため、3つをばらばらに上げると生成コードとランタイムで食い違いが出ることがある。3つまとめて上げ、テストで通してから固定する運用が扱いやすいでしょう。
スタブ生成|grpcio-toolsのprotocでprotoからコードを起こす
protoファイルの置き場所とパッケージ宣言を先に決めておく
置き場所の決め方が、後のimport解決をほぼ決めます。推奨は、リポジトリ直下にprotoディレクトリを1つ作り、その下をpackage宣言と同じ階層で切ることです。package greeter.v1;と宣言するなら、ファイルはproto/greeter/v1/greeter.protoに置きます。
syntax = "proto3";
package greeter.v1;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
バージョンをパスとパッケージ名の両方に入れておくのは、後方互換を壊す変更が必要になったときにv2を並走させるためです。フィールド番号の付け方や互換を壊す変更の線引きはProtocol Buffersのスキーマ設計で扱っているため、ここでは触れません。
python_out・grpc_python_out・pyi_outの3つを指定する
gRPC公式のPythonチュートリアルは、生成コマンドに3つの出力先を並べています。--python_outがメッセージクラス、--grpc_python_outがサービス側のスタブとサーバ基底クラス、--pyi_outが型スタブです。
python -m grpc_tools.protoc -I. \
--python_out=. \
--pyi_out=. \
--grpc_python_out=. \
proto/greeter/v1/greeter.proto
| 出力オプション | 生成物 | 役割 |
|---|---|---|
| –python_out | greeter_pb2.py | メッセージのクラス定義 |
| –grpc_python_out | greeter_pb2_grpc.py | StubとServicer基底 |
| –pyi_out | greeter_pb2.pyi | メッセージの型スタブ |
--pyi_outを省いた状態でmypyを走らせると、メッセージのフィールドがすべて未知の属性になります。型検査を入れる予定があるなら最初から3つ並べておくほうが手戻りがない。なお--pyi_outが出すのはメッセージ側の型だけで、_pb2_grpc.pyに対応する.pyiは生成されません。
生成物をリポジトリに入れるか毎回生成するかを運用時に決める2つの軸
判断軸は2つです。1つ目は、.protoを持つチームと実装するチームが同じかどうか。別チームなら、生成物をコミットしておいたほうが差分レビューでスキーマ変更に気づけます。2つ目は、CIのビルド時間に生成工程を足せるか。数十ファイル規模なら数秒で終わるため、多くの場合は生成をCIに寄せられる。
両者の中間として、生成物をコミットしつつCIで再生成して差分が出たら失敗させる方法もあります。手元での再生成忘れをそのまま検知できるため、複数人で.protoを触るなら実務的な落としどころになるでしょう。
importパス問題|生成コードのimportが解決できない原因と回避策
生成された絶対importがパッケージ配下で解決できない仕組み
_pb2_grpc.pyの先頭には、対になる_pb2モジュールを読み込む行が入ります。ここで生成されるのが暗黙の相対importに相当する形式で、Python 3ではモジュール検索パスの直下にファイルが無いと解決できません。
import greeter_pb2 as greeter__pb2
チュートリアルどおりカレントディレクトリで動かしている間は、カレントが検索パスに入るため通ります。問題は、生成物をapp/proto/のようなパッケージ配下へ移した瞬間です。greeter_pb2という名前はトップレベルには存在せず、ModuleNotFoundErrorになる。gRPCのリポジトリではこの挙動が課題として長く残っており、2026年9月時点でも生成器の既定出力は変わっていません。
-Iをリポジトリ直下に置いて完全修飾のimportで生成する
回避の本筋は、生成物の置き場所を変えるのではなく、-I(インクルードパス)の起点をリポジトリ直下に取ることです。protocは-Iからの相対パスをそのままモジュール名に変換するため、proto/greeter/v1/greeter.protoを-I.で食わせれば、生成されるimportは完全修飾になります。
from proto.greeter.v1 import greeter_pb2 as proto_dot_greeter_dot_v1_dot_greeter__pb2
この形なら、リポジトリ直下がモジュール検索パスに入っている限りどこから起動しても解決できます。条件は2つ。proto以下の各階層に__init__.pyを置くこと、そして-Iをprotoディレクトリではなくリポジトリ直下に取ること。-Iprotoとしてしまうと生成名がgreeter.v1.greeter_pb2になり、実際の配置と1階層ずれます。
生成後にsedで書き換えるスクリプトや、import修正を目的とした補助パッケージも公開されています。ただし後処理はプラグインを増やすたびに壊れやすく、CIで再生成する運用と相性が悪い。まず-Iの取り方で解けるかを試し、どうしても解けない場合の最後の手段として持っておくのが順序として堅いはずです。
パッケージング時に生成物を配布物へ含める書き方と実際に落ちる場面
wheelを作って配布する構成では、.pyは自動で拾われても.pyiと__init__.pyの無いディレクトリは落ちます。パッケージデータとして明示的に含める指定が要る。ここを忘れると、手元では通るのにインストール先で型スタブだけ消えるという追いにくい状態になります。
[tool.setuptools.package-data]
"proto.greeter.v1" = ["*.pyi"]
もう1つ落ちるのは、同じ.protoから生成した別々のwheelを1つの環境に入れた場合です。protobufは記述子をプロセス単位のプールに登録するため、同じ完全修飾名のメッセージが2度登録されると起動時に衝突します。共有する.protoは、生成物を1つのパッケージにまとめて配布し、利用側はそれを依存に取る形へ寄せてください。
サーバ実装|同期版とgrpc.aioのどちらを選ぶかを先に決める
同期サーバはスレッドプールの上限がそのまま同時実行数の上限になる
同期版はgrpc.server()にスレッドプールを渡して組み立てます。チュートリアルのmax_workers=10をそのまま本番に持っていくと、11本目のリクエストは先の10本が返るまで待ちます。ハンドラ1本がスレッド1つを占有するため、DBやHTTPの待ち時間が長いほど頭打ちが早く来る。
from concurrent import futures
import grpc
from proto.greeter.v1 import greeter_pb2, greeter_pb2_grpc
class Greeter(greeter_pb2_grpc.GreeterServicer):
def SayHello(self, request, context):
return greeter_pb2.HelloReply(message="hello " + request.name)
def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=32))
greeter_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
server.add_insecure_port("[::]:50051")
server.start()
server.wait_for_termination()
それでも同期版が向く場面はあります。呼び出しがCPU寄りで待ち時間が短い、既存のライブラリが同期APIしか持たない、同時接続数が数十で頭打ちしない。この3つが揃うなら、非同期に寄せる理由は薄いでしょう。
grpc.aioで書く非同期サーバとイベントループの扱いを押さえる
非同期版はgrpc.aio.server()で組み立て、ハンドラをasync defで書きます。gRPC PythonのAsyncIO APIドキュメント(v1.83.0)はこのAPIをstableと明記しており、get_stateやインターセプタなど一部の関数にのみEXPERIMENTALの注記が付く状態です。
import asyncio
import grpc
from proto.greeter.v1 import greeter_pb2, greeter_pb2_grpc
class Greeter(greeter_pb2_grpc.GreeterServicer):
async def SayHello(self, request, context):
row = await fetch_user(request.name)
return greeter_pb2.HelloReply(message=row.message)
async def serve():
server = grpc.aio.server()
greeter_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
server.add_insecure_port("[::]:50051")
await server.start()
await server.wait_for_termination()
asyncio.run(serve())
注意点は、ハンドラの中で同期のブロッキング呼び出しを踏まないことです。requestsや同期のDBドライバを1行混ぜるだけで、そのイベントループ上の全RPCが止まります。既存の同期ライブラリをどうしても呼ぶならrun_in_executorで別スレッドへ逃がす。長寿命の接続を多数抱えるストリーミングでは差が大きく出るため、ストリーミング4方式の選び分けを採る構成なら非同期側から検討するほうが素直です。
同期とasyncの選び分け|既存コードと待ち時間の割合で決める
判断は2軸で足ります。ハンドラの処理時間のうち外部待ちが占める割合が高いか。そして呼び出す既存資産が非同期に対応しているか。前者が高く後者も揃うなら非同期版、どちらか欠けるなら同期版でスレッド数を実測して決めるのが安全です。
1つのプロセスで同期版と非同期版を混ぜる構成は避けてください。grpc.server()とgrpc.aio.server()は内部の実行モデルが別で、同居させると停止処理やシグナル処理の見通しが悪くなる。サービス単位でどちらかに寄せる判断のほうが運用は楽になります。
クライアント実装と型|チャネルの寿命とpyiで型検査を効かせる
チャネルの寿命|毎回張り直すと接続確立の分だけ待ち時間が伸びる
チャネルはコネクションを内部で管理する重い資源です。リクエストのたびにinsecure_channel()を作り直すと、TCPとTLSのハンドシェイクを毎回払うことになる。プロセスの起動時に1つ作り、スタブを使い回す構成を既定にします。
import grpc
from proto.greeter.v1 import greeter_pb2, greeter_pb2_grpc
channel = grpc.insecure_channel("localhost:50051")
stub = greeter_pb2_grpc.GreeterStub(channel)
def say(name):
request = greeter_pb2.HelloRequest(name=name)
reply = stub.SayHello(request, timeout=3.0)
return reply.message
もう1つ落としやすいのがtimeoutです。省略すると期限なしになり、相手が固まったときに呼び出し側も一緒に固まります。gRPCの期限は呼び出し側が渡す設計になっているため、既定値をラッパ関数側で必ず入れておく。マルチプロセスでforkする構成では、fork前に作ったチャネルを子プロセスで使うと不安定になるため、子プロセス側で作り直してください。
pyi_outで生成した型スタブをmypyに読ませるときの限界
--pyi_outが出すのはメッセージの型だけです。_pb2_grpc.py側のStubクラスとServicer基底には型情報が付かないため、stub.SayHello()の引数と戻り値はmypyから見てAnyのままになります。「型スタブを入れたのに補完が効かない」という相談の多くはこの範囲差が原因です。
サービス側まで型を効かせたい場合は、外部プラグインでStubの型を生成する方法があります。ただしプラグインを足すと生成コマンドとCIの依存が増えるため、まずはメッセージ側だけ型が付く状態で運用し、Stubを薄いラッパ関数で包んで境界に型注釈を書く形でも実務的には足ります。
FastAPIとの併存|HTTPとgRPCを1サービスで持つときの構成
HTTPとgRPCを別プロセス・別ポートで並べる構成を既定にする
すでにFastAPIで動いているサービスにgRPCを足すとき、既定にすべきは別プロセスです。コンテナを2つに分け、HTTPは8000番、gRPCは50051番で受ける。理由は3つあります。片方の負荷でもう片方が巻き添えにならないこと、スケールの単位を別に取れること、そしてヘルスチェックとデプロイの手順を分けられることです。
コードは同じリポジトリに置いたまま、エントリポイントだけ分ければ済みます。ドメインロジックを両方から呼ぶ層に切り出しておけば、HTTPとgRPCは入口の違いに収まる。この分け方なら、後からどちらかを別リポジトリへ切り出す判断も取れます。
同一プロセスで動かす場合にasyncioのループを共有する形
プロセスを増やせない事情があるなら、grpc.aioとuvicornを同じイベントループに載せる形になります。両方ともasyncioの上で動くため、片方をブロックしない限りは共存できます。
import asyncio
import grpc
import uvicorn
from fastapi import FastAPI
app = FastAPI()
async def main():
server = grpc.aio.server()
server.add_insecure_port("[::]:50051")
await server.start()
config = uvicorn.Config(app, host="0.0.0.0", port=8000)
await uvicorn.Server(config).serve()
asyncio.run(main())
同期版のFastAPIエンドポイント(defで書いたもの)はスレッドプールへ回されるため、その中でgRPCの同期スタブを呼ぶとスレッドを二重に消費します。同一プロセス構成を採るなら、入口も呼び出しも非同期側へ揃えてください。
併存させずEnvoyやgRPC-Gatewayで前段変換する選択肢
3つ目の選択肢は、サービス自体はgRPCだけにして、HTTPが要る相手には前段のプロキシで変換させる方法です。Envoyのフィルタやトランスコーディング、あるいはブラウザ向けのgRPC-Webのプロキシ構成がこれにあたります。
この形が効くのは、HTTPの利用者が社外や別チームで、REST的なインタフェースを維持する必要がある場合です。反面、変換ルールという第2のスキーマを保守することになる。社内向けの呼び出ししか無いなら、前段を挟まず素のgRPCで通すほうが構成は軽く済みます。
採用の境界|PythonでgRPCを選んでよい条件と見送る場面
採用してよい3条件|内部通信・スキーマ共有・多言語構成が揃う
1つ目は、呼び出しがサービス間の内部通信であること。2つ目は、スキーマを両側で共有して壊れたら検知したいこと。3つ目は、呼び出す側と呼ばれる側の言語が違い、手書きのクライアントを言語ごとに保守したくないこと。この3つが揃うとき、Python側でgRPCを選ぶ価値が最も出ます。
この構成でサーバ側をRustに置き換える判断をするなら、gRPC Rustの実装手順とtonicの依存構成で依存クレートと相互接続の確認手順を確かめてください。
典型は、Pythonで書いた推論サーバをGoやJavaのアプリケーションから叩く構成です。.protoを1つ置けば両側のスタブが自動で揃い、フィールドを増やしても古いクライアントが壊れない。この性質は、JSONで受け渡す構成では自前の取り決めと検証コードで作り込むしかない部分です。呼び出し側をGoで書く場合の生成手順と接続APIの選び方はgRPC Goの実装手順で扱っています。
見送る3つの場面|ブラウザ直・公開API・単一言語で完結する構成
ブラウザから直接叩きたい要件は、素のgRPCでは成立しません。プロキシを挟む前提になるため、その運用コストを飲めないなら見送りです。公開APIも同様で、外部の利用者に.protoとコード生成を要求する時点で採用障壁が跳ね上がります。
3つ目は、呼び出し側も呼ばれる側もPythonだけで完結し、当面その予定が続く場合です。この条件では、スキーマ共有の利点をコード生成なしでも得られる選択肢があります。単一言語での型安全なAPIをどう組むかはtRPCと他方式の比較の整理が判断材料になるでしょう。
受託開発での取り決め|protoの所有者と後方互換の責務を先に決める
複数社が関わる案件では、.protoを誰が持ち、変更を誰が承認するかを契約の段階で決めておきます。決めずに走ると、フィールド番号の付け替えやフィールド削除が予告なく入り、既存クライアントが黙って壊れる事故が起きる。番号の再利用を封じる運用と、互換を壊す変更をCIで検知する仕組みを、最初のスプリントで入れておくのが安く済みます。
もう1つ決めるのは、生成物の配布方法です。各社が各自で生成するのか、1つのパッケージとして配るのか。前者は生成器のバージョン差で微妙な食い違いが出るため、規模が大きいほど後者が有利になります。要件整理から接続方式の選定、運用の取り決めまで含めて相談したい場合は、API開発・システム連携で対応しています。
よくある質問
gRPCのPython実装は同期版とasync版のどちらから始めるべきですか?
外部への待ち時間が処理時間の大半を占めるならgrpc.aioから始めてください。DBやHTTPの呼び出しが1リクエストに複数入るような構成では、同期版のスレッドプール上限がそのまま同時実行数の天井になります。逆に、既存の同期ライブラリに依存していて非同期版が無い場合は、同期版でスレッド数を実測して決めるほうが確実です。途中で乗り換えるとハンドラの書き方が変わるため、最初に決めておく価値があります。
生成した_pb2ファイルはGitに入れるべきですか?
スキーマを別チームや別社が持つならコミットを推奨します。差分レビューでスキーマ変更に気づける利点が大きいためです。自チームで完結し、CIに生成工程を足せるなら生成に寄せて構いません。折衷案として、コミットしたうえでCIで再生成し差分が出たら失敗させる方法があり、再生成忘れをそのまま検知できます。
protoを変更したとき、既存のPythonクライアントは壊れますか?
フィールドを追加するだけなら壊れません。古いクライアントは知らないフィールドを無視します。壊れるのは、フィールド番号を付け替えた場合、既存フィールドの型を変えた場合、そして削除した番号を別の用途で再利用した場合です。削除するときは番号をreservedで封じ、互換を壊す変更をCIで検出する仕組みを入れてください。
FastAPIとgRPCを1つのプロセスで動かしても問題ありませんか?
両方を非同期側で揃えるなら動きます。grpc.aioとuvicornは同じイベントループに載せられるためです。ただし片方でブロッキング処理を踏むともう片方まで止まるため、障害の切り分けが難しくなります。プロセスを分けられる環境なら、別ポート・別コンテナに置く構成を既定にするほうが運用は安定します。
型スタブを入れてもmypyがエラーを出すのはなぜですか?
--pyi_outが生成するのはメッセージ側の型だけで、_pb2_grpc.pyのStubとServicerには型が付かないためです。スタブ呼び出しの引数と戻り値はAnyとして扱われます。サービス側まで型を効かせたい場合は外部プラグインを足す方法がありますが、Stubを薄いラッパ関数で包んで境界に型注釈を書くだけでも実務上は足ります。
関連記事
- gRPCとは?仕組み・RESTとの違い・UDP/TCPの扱いを実装例つきで解説:プロトコルの基礎とRESTとの使い分け
- Protocol Buffersとは?protoファイルの書き方と採用判断を実装者向けに解説:protoの記法とスキーマ互換の運用
- gRPCストリーミングとは?4方式の使い分けと切断対策を実装者向けに解説:複数メッセージを流す方式の選び分け
- gRPC-Webとは?ブラウザからgRPCを呼ぶ仕組みとプロキシ構成・採用判断を解説:ブラウザから呼ぶ場合の制約と構成
- tRPCとは?REST・GraphQL・gRPCとの違いと型安全なAPI実装をv11の実コードで解説:単一言語で型安全に組む代替手段
- gRPC C#の実装|Grpc.AspNetCoreのビルド時生成とホスティング制約:同じprotoからC#側を組むときのビルド時生成と配置の条件