バックエンド

gRPC を体験してみた

dai

はじめに

こんにちは、ダイです。
気づけば DWS に入社して 1 年が経過していました。未体験の技術や仕事に挑戦さえてもらえる環境で、日々大変でありながらも成長を感じられる環境に感謝しています。
REST API と比較する形でよく聞くけど、実際にgRPC を使ったことないなと思い、試してみました。

gRPC の特徴

  • Protocol Buffers によるスキーマの定義とコードの自動生成
  • 異なるプログラミング言語にも対応
  • その他にも「HTTP/2 上に構築されている」や「Protocol Buffers のバイナリフォーマットはデータサイズが小さく高効率」など様々な特徴があります。気になる方は公式ページを確認してみてください。

gRPC 通信を試す

実施内容と期待する結果

Python で ローカル環境にgRPC サーバを構築して、gRPC クライアントから通信し期待どおりの結果が帰ってくることを確認します。
公式のチュートリアルでは簡単に実行できる環境が用意されています。動作のみ試したい場合は、こちらがオススメです。

実行環境

OS:macOS Ventura(version 13.6)
python: 3.10.0

事前準備

作業用ディレクトリの作成と必要なツールのインストールを実施。

# 作業用ディレクトリ作成と移動
$ mkdir grpc_test
$ cd grpc_test

# Pythonで仮想環境を利用したい場合に必要に応じて実施
$ python -m venv venv
$ source venv/bin/Activate

# 必要なツールのインストール
$ pip install --upgrade pip
$ pip install grpcio grpcio-tools

# proto定義用のファイルの作成
$ touch personinfo.proto

proto ファイルの定義と自動コード生成

各定義はそれぞれ以下の役割を担っています。
service: ここに rpc メソッドの集合体を定義します。
rpc: rpc メソッドを定義します。今回は GetPersonInfo というメソッドを定義。
message: データ構造の定義に使用されます。今回は GetPersonInfo で利用されるリクエストとレスポンスを定義。

syntax = "proto3";

package personinfo;

service PersonInfoService {
  rpc GetPersonInfo(PersonRequest) returns (PersonResponse) {}
}

message PersonRequest {
  string name = 1;
}

message PersonResponse {
  string name = 1;
  int64 age = 2;
}

続いて、定義からサーバとクライアントで利用するコードを生成します。

$ python -m grpc_tools.protoc -I.--python_out=. --pyi_out=. --grpc_python_out=. ./personinfo.proto

以下の3つのファイルが生成されます。

これらをPythonのモジュールとしてインポートして利用します。

  • personinfo_pb2.py
  • personinfo_pb2.pyi
  • personinfo_pb2_grpc.py

自動生成コードを利用したサーバとクライアントの実装

自動生成されたコードからモジュールをインポートし、rpcメソッドの実装に利用できます。

from concurrent import futures

import grpc
import personinfo_pb2
import personinfo_pb2_grpc
from grpc import StatusCode

class PersonInfoService(personinfo_pb2_grpc.PersonInfoServiceServicer):

    # protoで定義したGetPersonInfoの実装
    def GetPersonInfo(self, request, context):
        if request.name == "taro":
            return personinfo_pb2.PersonResponse(name=request.name, age=20)
        if request.name == "jiro":
            return personinfo_pb2.PersonResponse(name=request.name, age=30)

        # 想定外の値の場合、エラーを返却する
        context.set_code(StatusCode.NOT_FOUND)
        context.set_details("Person not found")
        return personinfo_pb2.PersonResponse()

def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    personinfo_pb2_grpc.add_PersonInfoServiceServicer_to_server(PersonInfoService(), server)
    server.add_insecure_port('[::]:50051')
    server.start()
    server.wait_for_termination()

if __name__ == "__main__":
    serve()

クライアント側も同様にモジュールをインポートします。

スタブ生成後のメソッド呼び出しはリモートサーバの呼び出しであることをほとんど意識することなく、他のクラス呼び出しと同じ感覚で実行できます。

import grpc
import personinfo_pb2
import personinfo_pb2_grpc
import sys

def run(name):
    # サーバーへの接続を開始
    with grpc.insecure_channel('localhost:50051') as channel:
        stub = personinfo_pb2_grpc.PersonInfoServiceStub(channel)
        try:
            response = stub.GetPersonInfo(personinfo_pb2.PersonRequest(name=name))
            print(f"{response.name}'s Age is {response.age}")
        except grpc.RpcError as e:
            status_code = e.code()
            if status_code == grpc.StatusCode.NOT_FOUND:
                print(f"Person not found: {name}")
            else:
                print(f"Unexpected error: {e}")

if __name__ == '__main__':
    if len(sys.argv) != 2:
        print("Usage: python personinfo_client.py [name]")
        sys.exit(1)

    name = sys.argv[1]
    run(name)

動作確認

rpc呼び出しに成功し、結果が返却されるのが確認できました。

$ python personinfo_server.py # サーバを起動

# 新規にターミナルを起動してクライアントからサーバへ接続
$ python personinfo_client.py taro
jiro's Age is 30
$ python personinfo_client.py saburo
Person not found: saburo

感想

簡単な動作確認程度でしたが、proto ファイルを元にした API インターフェースの定義とスキーマ駆動開発のやりやすさが非常に印象的でした。
今回は Python を用いましたが、Go などの静的型付け言語では、proto ファイルから生成されるスタブが言語の型システムと密接に結合され、型エラーの検知などもできることから更に親和性も高いのではないかと感じました。

AUTHOR
dai
dai
記事URLをコピーしました