学習前イメージ
・最近よく聞くきがする。
・API規格なのかな?
・RESTとなにが違うの?
全然わからん。
ざっと調べた概要
マイクロサービスアーキテクチャ(サービスを小さい単位で区切って独立したコンポーネントとして動かす奴)において、サービス間のAPI呼び出しに使われるフレームワーク。
CNCF landscapeにも大々と載っている。
https://landscape.cncf.io/
RESTと何が違うの?
そもそもレイヤーが違う概念の模様。
両方HTTPを利用している点は同じだが、
RESTはHTTPでAPIを作る際の設計原則をまとめた規格(ルール群)であるのに対し、
gRPCはHTTPを利用したAPI作成のフレームワーク、要するにAPI作成スタータキットのこと。
gRPCのメリットは?
複数の言語に対応しており、「pythonで実装したAPIを、javaのクライアントアプリケーションから叩く」といったことが簡単に出来る。というのが売りのよう。
どうやって言語を跨いだAPIを作るのか
gRCPは定義言語(IDL)である.protoファイルにAPIの仕様を記載することで、その仕様を実現するためのソースコードを出力してくれる。しかもこの.protoファイルはサーバサイドとクライアントサイドで共通。
要するにサーバサイドで使った.protoファイルさえあれば細かい規格を気にすることなく、(ほぼ)自動でクライアントサイドを実装できるため、開発の労力を大幅に削減できるというわけ。
HelloWorldしてみる
コードは公式から持ってきたものからcore部分を抽出しました。
https://grpc.io/docs/languages/python/
.protoファイルからコード生成
まずは.protoファイルを記述していく。
リファレンスはこちら
https://protobuf.dev/programming-guides/proto3/
#.protoファイルのバージョン指定。バージョンごとに文法変わるので要注意
syntax ="proto3";
service Greeter {
#HelloRequestオブジェクトを受け取りHelloReplyオブジェクトを返すSayHello APIを作成。
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
#HelloRequestオブジェクトが持つ変数を定義。
message HelloRequest {
string name = 1;
}
#HelloReplyオブジェクトが持つ変数を定義。
message HelloReply {
string message = 1;
}
※変数に入っている数字はフィールド番号と呼ばれ、1~536870911の間を取り、メッセージ中で一意である必要がある。適当でOK。
.protoファイルから、PYTHON用にソースコードを生成
今回Pythonを使うため、.protoファイルをPythonのgrpcのモジュールに読み取らせる。
引数はoutput先dirとかなのでまぁ適当に。
python3 -m grpc_tools.protoc helloworld.proto --grpc_python_out=. --python_out=. -I ./するとカレントディレクトリにhelloworld_pb2.py, helloworld_pb2_grpc.pyというスクリプトが生成される。これをインポートするだけで、.protoファイルの記載に則ったclassやMethodを使えるようになる。簡単ですね。
※なお、gRPCのメリットに多言語対応を挙げておきながら、サーバ/クライアント 両サイドともPyrhonで実装した怠慢はご容赦ください。
APIを実装
サーバサイド実装
greeter_server.py
#grpcに必須なモジュール
from concurrent import futures
import grpc
#protoが生成したファイル
import helloworld_pb2
import helloworld_pb2_grpc
#クライアントから呼び出されるSayHello関数の動作を定義。
class Greeter(helloworld_pb2_grpc.GreeterServicer):
msg = "hogehogeHugaHuga"
def SayHello(self, request, context):
# 受け取ったrequest.name値を含む文字列を返すだけの処理を記載。
return helloworld_pb2.HelloReply(message="Hello, %s!" % request.name)
#サーバサイド立ち上げ。具体的なことはprotoから生成したファイルとgrpcモジュールがやってくれる。
def serv():
p = "4488"
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
helloworld_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
server.start()
server.wait_for_termination()
#実行
if __name__ == "__main__":
serv()
クライアントサイド実装
greeter_client.py
#grpcに必須なモジュール
import grpc
#protoから生成したファイル
import helloworld_pb2
import helloworld_pb2_grpc
def run():
with grpc.insecure_channel('localhost:4488') as channel:
#おまじない
stub = helloworld_pb2_grpc.GreeterStub(channel)
#API呼び出し
response = stub.SayHello(helloworld_pb2.HelloRequest(name='Nara'))
print("Greeter client received: " + response.message)
if __name__ == "__main__":
run()
はい、簡単に実装できました。
しっかり応答が返ってきます。
gRPCは普通のREST APIよりも高速?
「gRPCはprotocol bufferを使用する都合、シリアライズ処理が早いから高速だ。」みたいな情報をときどき目にします。
ぶっちゃけ、「根本でHTTPを使ってるんだから、そんなに性能変わらないんじゃない?」と思いませんか?思いますよね?
私は思います。ということで検証してみました。
RPC VS REST速度検証してみた
条件
・比較対象のRESTfull な APIを作れるフレームワークとしてFlaskを採用。
・テストデータからuuidを抽出して返すAPIをそれぞれ作成し、1000回叩いた際の速度を比較。
・なるべく(ここ重要!) 同じ処理をそれぞれのフレームワークでコーディングする
・flaskやgRPCの裏で動いているプロセスによる遅延は今回無視。そういうのも含めて比較対象とする。
見ての通りのざっくり検証です。
テストデータ作成ワンライナー
※後述のソースコードと同じdirに置いておけばOK
echo "[" > test.json ; for count in {1..1000} ; do echo " {"; echo " \"id\": $count ,"; echo " \"uuid\": \"$(uuid)\""; echo " },";done >> test.json ; echo "{}" >> test.json ; echo "]" >> test.json
gRPCのサーバサイド
test.proto
$ python3 -m grpc_tools.protoc test.proto --grpc_python_out=. --python_out=. -I ./syntax ="proto3";
service Bench {
rpc Hello (TestRequest) returns (TestReply) {}
}
message TestRequest {
int32 id = 1;
}
message TestReply {
string uuid = 1;
}
grpc_server.py
from concurrent import futures
import grpc
import test_pb2_grpc
import test_pb2
import json
f = open('test.json', 'r')
json_dict = json.load(f)
#print(json_dict[0]["uuid"])
class Bench(test_pb2_grpc.BenchServicer):
def Hello(self, request, context):
res = json_dict[request.id - 1]["uuid"]
return test_pb2.TestReply(uuid="%s" % res)
def serv():
p = "4444"
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
test_pb2_grpc.add_BenchServicer_to_server(Bench(), server)
server.add_insecure_port("[::]:" + p)
server.start()
server.wait_for_termination()
if __name__ == "__main__":
serv()
gRPCのクライアントサイド
grpc_client.py
import grpc
import test_pb2
import test_pb2_grpc
import time
def run():
with grpc.insecure_channel('localhost:4444') as channel:
stub = test_pb2_grpc.BenchStub(channel)
for i in range(1,1000):
response = stub.Hello(test_pb2.TestRequest(id=i))
print( str(i) + "/ received: " + response.uuid)
if __name__ == "__main__":
start = time.perf_counter()
run()
end = time.perf_counter()
print('{:.2f}'.format(end-start))
Flaskのサーバサイド
$ pip install flask
from flask import Flask, jsonify, request
import json
app = Flask(__name__)
f = open('test.json', 'r')
json_dict = json.load(f)
@app.route('/api/getuuid/<int:id>', methods=['GET'])
def get_uuid(id):
return jsonify({"uuid": json_dict[int(id)]["uuid"]})
if __name__ == '__main__':
app.run(debug=False)
Flaskのクライアントサイド
import requests
import time
def run():
for i in range(1,1000):
url = "http://localhost:5000/api/getuuid/%s" % i
response = requests.get(url).json()
print( str(i) + "/ received: " + response.get("uuid"))
if __name__ == "__main__":
start = time.perf_counter()
run()
end = time.perf_counter()
print('{:.2f}'.format(end-start))
比較結果
私の環境での平均結果は以下。
Flask:1.30s
gRPC:0.27
gRPC、爆速ですね。ナマ言って申し訳ございませんでした。
この検証だと条件を揃え切れていない部分も多いので、
もっと真面目に検証している方の1リンクを貼っておきます。こちらでも7-10倍程度早くなったようなので、「gRPCはRESTfull APIと比較して高速である」は真といって良いでしょう。
https://medium.com/@EmperorRXF/evaluating-performance-of-rest-vs-grpc-1b8bdf0b22da
学習後イメージ
gRPC鬼つええぇ!
これから全部のAPI gRPCでつくろうぜ!
いえ、筆者は開発分野には疎いので真に受けなくてよいのですが、、
学習コストの安さとマルチ言語対応が優秀。しかも速い。優秀なフレームワークだなと思いました。
REST APIの定義など、曖昧に認知していた部分について整理ができて有意義な学習だったと思います。
以上、ご一読いただきありがとうございました。