フリーキーズ | 独学プログラミング

PythonでgRPCとProtocol Buffersを使う方法

最終更新日
【画像】PythonでgRPCとProtocol Buffersを使う方法

gRPCとProtocol Buffersの強力な組み合わせは現代的なアプリケーションを構築する上で重要な技術です。ここでは、gRPC + Protocol Buffersの構築をPythonで実現する方法をステップバイステップでご紹介します。

アプリケーションを強化するgRPCとProtocol Buffers

サービス間通信の領域において、gRPCとProtocol Buffersは従来のRESTful APIやJSONよりも大きな利点を提供しています。
効率的なバイナリ形式、堅牢なインターフェース定義言語(IDL)、双方向ストリーミング、多数の言語サポートといった特徴は、現代のマイクロサービスアーキテクチャにとって理想的な選択となります。

Pythonの場合、言語のシンプルさと汎用性によりこれらの利点はさらに増幅されます。
PythonアプリケーションにgRPCとProtocol Buffersを組み込むことで、より速く、より軽く、より信頼性の高いサービス通信をを持つスケーラブルで高性能なシステム基盤を構築できます。

まずは基本的なコードを見てみましょう。
以下はPythonとgRPCアプリケーションのシンプルな例です。

from concurrent import futures
import grpc
import helloworld_pb2
import helloworld_pb2_grpc

class Greeter(helloworld_pb2_grpc.GreeterServicer):
    def SayHello(self, request, context):
        return helloworld_pb2.HelloReply(msg='Hello, %s!' % request.name)

def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    helloworld_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
    server.add_insecure_port('[::]:50051')
    server.start()
    server.wait_for_termination()

if __name__ == '__main__':
    serve()

gRPCとProtocol Buffersを使うメリット

Googleが開発したオープンソースの高性能RPCフレームワークであるgRPCは、インターフェース定義言語(IDL)としてProtocol Buffersを使用しています。
これにより、開発者はサービスやメッセージタイプを.protoファイルで定義し、複数の言語でコードを生成できるため言語間のコミュニケーションが容易になります。
Protocol Buffersはコンパクトなシリアライゼーションを提供し、ネットワーク通信をより効率的にします。型付けされたメッセージは、サービス間の契約履行を保証し、より良い安全性と信頼性を提供します。

PythonではgRPCの非同期およびノンブロッキングIOと多重化のサポートにより、高いパフォーマンスのサーバ実装が可能になります。
Protocol Buffersのシリアライズ/デシリアライズの効率は、Pythonの文字列処理と構文解析の典型的なオーバーヘッドを削減します。

以下は.protoファイルでサービスを定義する例です。

syntax = "proto3";

package helloworld;

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string msg = 1;
}

SayHelloメソッドはHelloRequestメッセージを受け取り、HelloReplyで応答しています。

ステップ1 gRPCとProtocol Buffersとは

gRPCとProtocol Buffersは、どちらもGoogleが開発した技術で、サービス間通信の高速化、信頼性の向上、設計・実装の容易化を目的としています。
gRPCはGoogle Remote Procedure Callの略で、高性能でオープンソースのフレームワークでありどこでも実行できます。
Protocol Buffersは、Protobufsとして知られ、構造化データをシリアライズするバイナリプロトコルです。

この2つを組み合わせることで、サーバーの使用言語に関係なくリモートサーバー上のメソッドを定義して呼び出すための堅牢な通信が実現します。

# gRPCを利用した別サーバの関数を呼び出す例
channel = grpc.insecure_channel('localhost:50051')
stub = helloworld_pb2_grpc.GreeterStub(channel)
response = stub.SayHello(helloworld_pb2.HelloRequest(name='World'))

gRPCの役割

gRPCは、クライアントアプリケーションとサーバアプリケーションの透過的な通信を可能にし、接続されたシステムの構築を簡素化します。
インターフェイスの定義としてProtobufsを使用しており、開発者は.protoファイルでサーバが提供するサービスを定義できます。認証、負荷分散、キャンセルなどさまざまな機能をサポートしており、幅広い用途に対応できる汎用性を持っています。

# PythonにおけるgPRCサーバの例
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
helloworld_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
server.add_insecure_port('[::]:50051')
server.start()

Protocol Buffersの役割

Protocol Buffers(通称Protobufs)は、構造化データをシリアライズするための拡張可能な機構です。
XMLやJSONよりもわかりやすく効率的な構造になっています。Protobufsはサービス間の契約を保証し、送信のために構造化データをシリアライズするメカニズムを提供します。.protoファイルに定義されたデータは複数の言語にコンパイルでき、gRPCサーバーやクライアントで使用できます。

# .protoファイルの例
syntax = "proto3";
package helloworld;

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string msg = 1;
}

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

ステップ2 gRPCのためのPython環境のセットアップ

gRPCやProtobufsを理解したところで、字指にPythonを使ったgRPCの開発環境を構築してみましょう。

まず必要なのは互換性のあるPythonのバージョンです。
gRPCは複数の言語をサポートしていますが、最良の結果を得るためにはPython 3.5以上を推奨します。また、Pythonのパッケージインストーラであるpipがインストールされ、アップデートされている必要があります。

# Pythonのバージョン確認
python --version
# 3.5以上であることを確認

# pipのバージョンを確認
pip --version
# インストールされていなかったり、バージョンアップの必要があれば実施

必要なライブラリのインストール

PythonでgRPCとProtocol Buffersを扱うにはgrpciogrpcio-toolsのインストールが必要です。

これらはpipを使用してインストールできます。grpcioライブラリはgRPCの主要なランタイムを提供し、grpcio-toolsはProtobufsのコード生成とサポートツールを含んでいます。

# grpcioとgrpcio-toolsをインストール
pip install grpcio grpcio-tools

環境構築の注意点

gRPCの環境を構築する場合、PythonのバージョンとgRPCライブラリの互換性を確保することが重要であることを先に記載した通りです。
また、他のPythonプロジェクトとの競合を避けるために仮想環境の利用を検討してください。さらに、gRPCとProtocol Buffersはコード生成を伴うため、IDEやテキストエディタが複数のファイルタイプや言語を扱えるようにセットアップされていることを確認してください。

# Pythonの仮想環境のセットアップ
python3 -m venv grpc-env
source grpc-env/bin/activate

# grpc-env環境になっていることを確認
# 仮想環境で隔離できたらその環境にインストール
pip install grpcio grpcio-tools

ステップ3 Protocol Buffersサービスの定義

PythonでgRPCを使う上で重要なのは、Protocol Buffersサービスを定義することです。
そうすることによってサービス間の契約を形成し、リモートで呼び出せるメソッド、そのパラメータ、その戻り値の型を示します。これはgRPCのインターフェース記述言語である.protoファイルによって行われます。

syntax = "proto3";

// パッケージ定義
package greeter;

// Greeterサービスを定義
service Greeter {
  // 挨拶を送る
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// リクエストは相手の名前を含むとする
message HelloRequest {
  string name = 1;
}

// レスポンスは挨拶を含む
message HelloReply {
  string msg = 1; // msgはメッセージの略だが、予約語と同じになるためmsgで定義
}

.protoファイルの定義方法

.protoファイルは、構文とパッケージ名の宣言で始まりサービス定義が続きます。
このファイルでは、サービスのインターフェイスを記述し、gRPCで呼び出すことのできるメソッドとその入出力パラメータのタイプを指定します。予約語であるmessageはリクエストとレスポンスのメッセージタイプを定義するために使用されます。

syntax = "proto3";

// 別のプロジェクトとの命名による衝突を防げるので定義が必要
package mypackage;

// 単純なサービス
service MyService {
  // シンプルなRPC
  rpc CallMe (Request) returns (Response) {}
}

// リクエスト
message Request {
  string request_data = 1;
}

// レスポンス
message Response {
  string response_data = 1;
}

サービス定義とメッセージ定義の理解

.protoファイルでは、serviceがサービスを定義し、その中にrpcがリモートで呼び出すことのできるメソッドを指定します。

すべてのrpcメソッドには特定のリクエストとレスポンスのタイプがあり、これらはメッセージを使って定義されます。
メッセージはPythonのクラスのようなもので、メッセージのフィールドはインスタンス変数を表します。メッセージのフィールド宣言の「=1」は、各フィールドを区別するために使われる一意の識別子を指します。

syntax = "proto3";
package math;

service Math {
  rpc Add (AddRequest) returns (AddResponse) {}
  rpc Multiply (MultiplyRequest) returns (MultiplyResponse) {}
}

message AddRequest {
  int32 num1 = 1;
  int32 num2 = 2;
}

message AddResponse {
  int32 result = 1;
}

message MultiplyRequest {
  int32 num1 = 1;
  int32 num2 = 2;
}

message MultiplyResponse {
  int32 result = 1;
}

grpcio-toolsを使ってコード生成

先ほどのgreeter.protoファイルをベースに、.protoファイルからgrpcio-toolsでコード生成しておきましょう。
ここで生成されるファイルを利用することで、手軽にgRPCアプリケーションをテストすることが可能になります。

python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. greeter.proto

ステップ4 gRPCサーバーの実装

Protocol Buffersサービスが定義されたら、gRPCアプリケーションのサーバ側を実装する必要があります。

サーバはサービスをホストし、クライアントからの呼び出しを待ち、処理して応答を返します。
PythonではgRPCサーバはgrpcモジュールを使用し、サービスは.protoファイルから生成されたベースクラスを継承するPythonクラスとして実装します。

from concurrent import futures
import grpc
import greeter_pb2
import greeter_pb2_grpc


class Greeter(greeter_pb2_grpc.GreeterServicer):
    def SayHello(self, request, context):
        return greeter_pb2.HelloReply(msg='Hello, %s!' % request.name)

def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    greeter_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
    server.add_insecure_port('[::]:50051')
    server.start()
    server.wait_for_termination()

if __name__ == '__main__':
    serve()

サービスは生成されたベースクラス(GreeterServicer)を継承するクラス(Greeter)として定義されています。サービスメソッドは通常のPythonメソッドとして実装され、入力パラメータ(request)と制御フローのためのコンテキストオブジェクトにアクセスできます。serve()関数はサーバーの作成と起動をして、特定のポートでリクエストを受け入れます。

計算を返すサーバーの例

from concurrent import futures
import grpc
import math_pb2
import math_pb2_grpc


class Math(math_pb2_grpc.MathServicer):

    def Add(self, request, context):
        return math_pb2.AddResponse(result=request.num1 + request.num2)

    def Multiply(self, request, context):
        return math_pb2.MultiplyResponse(result=request.num1 * request.num2)

def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    math_pb2_grpc.add_MathServicer_to_server(Math(), server)
    server.add_insecure_port('[::]:50051')
    server.start()
    server.wait_for_termination()

if __name__ == '__main__':
    serve()

ステップ5 gRPCクライアントの実装

サーバーの実装に続いて、次はサーバーが提供するサービスを利用するgRPCクライアントを実装します。

クライアントはサーバへのチャネルを作成し、サーバのスタブ(代用品の意、またはプロキシ)を作成し、スタブを使用してサービスメソッドを呼び出します。クライアントはgrpcモジュールを使用し、生成されたクラスからスタブを作成します。

import grpc
import greeter_pb2
import greeter_pb2_grpc

def run():
    with grpc.insecure_channel('localhost:50051') as channel:
        stub = greeter_pb2_grpc.GreeterStub(channel)
        response = stub.SayHello(greeter_pb2.HelloRequest(name='[自分の名前]'))
    print("Greeter client received: " + response.msg)

if __name__ == '__main__':
    run()

クライアントスクリプトではgrpc.insecure_channel()関数を使ってサーバーへのgRPCチャネルを作成し、サービスクラスからスタブを作成します。
スタブメソッドは通常のPythonメソッドと同様に呼び出すことができ、応答オブジェクトを返します。使用後にチャネルが閉じられるように、withを使用していることに注意してください。

別クライアントの例

import grpc
import math_pb2
import math_pb2_grpc

def run():
    with grpc.insecure_channel('localhost:50051') as channel:
        stub = math_pb2_grpc.MathStub(channel)
        response = stub.Add(math_pb2.AddRequest(num1=5, num2=3))
        print("Math client received: " + str(response.result))

if __name__ == '__main__':
    run()

ステップ6 gRPCアプリケーションのテスト

サーバーとクライアントの両方を実装したら先ほどのGreeterアプリケーションをテストしてみましょう。

両方のスクリプトを異なるターミナル(ウィンドウ、またはタブ)を立ち上げ、同時に実行することでこれを行うことができます。まず、サーバースクリプトを起動します。
このスクリプトはクライアントの要求を待って実行し続けます。

python3 greeter_server.py

次に、別のターミナルウィンドウまたはタブで、クライアントスクリプトを実行します。サーバーに接続し、リクエストを行い、サーバーからのレスポンスを表示します。

python3 greeter_client.py

テスト結果の解釈

クライアントスクリプトの出力には、gRPC呼び出しに対するサーバーの応答が表示されます。
サーバーが動作しており、クライアントが正常に接続して通信できる場合、クライアントの端末に適切な応答が表示されます。これにより、gRPCアプリケーションが正しく動作していることが確認できます。

# クライアントを実行するターミナルの出力
My client received: Hello, [自分の名前]!

エラーメッセージが表示された場合はアプリケーションをデバッグする必要があります。.protoファイル、サーバーとクライアントのスクリプト、システムをチェックして、すべてが正しく構成されていることを確認してください。

gRPCの実践的なアプリケーションとベストプラクティス

gRPCは非常に強力で、効率的かつスケーラブルな双方向通信が必要な様々なシナリオで適用できます。マイクロサービスアーキテクチャ、リアルタイムシステム、または低レイテンシーと高スケーラビリティを必要とするシステムでよく使用されます。

ベストプラクティスとしては、構造化されたProtocol Buffersメッセージの定義、効率的なエラー処理、ニーズに応じて4種類のサービスメソッド(下記参照)をすべて活用することが挙げられます。
また、Protocol Buffersは強く型付けされているため、不正なデータ型に関連するバグを回避できます。

  • Unary RPC = 単一データを受け取って単一データを返すこと(単純なサーバー間通信など)
  • Server Streaming RPC = サーバーからクライアントへ複数のリクエストを送ること(通知など)
  • Client Streaming RPC = クライアントからサーバへ複数のリクエストを送ること(アップロードなど)
  • Bidirectional Streaming RPC = 双方向でデータのやりとりをすること(チャットなど)

実際の使用例

gRPCを最も多く採用しているのはGoogleで、同社は多くのサービスで内部的にgRPCを使用しています。
また、Netflixやその他多くの企業も、その効率性と堅牢性から自社のAPIにgRPCを使用しています。

例えば、チャットアプリケーションではgRPCの双方向ストリーミングを利用して、サーバーとクライアントの間でリアルタイムに通信できます。この場合、サーバーとクライアントの両方が互いに独立して読み書きできますが、メッセージの順序は保持されます。

service ChatService {
  rpc Chat (stream ChatMessage) returns (stream ChatMessage) {}
}
class ChatService(chat_pb2_grpc.ChatServiceServicer):
  def Chat(self, request_iterator, context):
    for note in request_iterator:
      yield note  # 受信したメッセージをクライアントに返送する

PythonでgRPCとProtocol Buffersを効果的に使用する

PythonでgRPCとProtocol Buffersを効果的に使うには、これらの技術の能力と限界を十分に理解する必要があります。例えば、いつどのタイプのサービスメソッドを使うかを知ることでアプリケーションのパフォーマンスに大きな影響を与えることができます。

また、gRPCは言語に依存しないことも覚えておいてください。
言い換えると、異なるプログラミング言語で書かれたアプリケーション間の通信に使用できます。これは異なるサービスがそれぞれの目的に最も適した異なる言語で書かれている可能性があるマイクロサービスアーキテクチャにおいて特に有用です。

.protoファイルを定義するときは整理して再利用できるようにしてください。そうすることでコードベースがすっきりとし、管理しやすくなります。
さらにアプリケーションのデバッグや監視に役立つように、gRPCサービスに対して適切なロギングを設定することが有益です。

関連するコンテンツ