はじめに
最近、社内でマイクロサービスが話題に上がるにつれ、
gRPCという言葉をよく聞くようになりました。
前から興味があったことと、今後業務でgRPCで使う可能性がありそうとのことだったので、
概要と雰囲気を掴むために記事にまとめてみました。
記事の読み進め方
記事の内容を下2つに分けてgRPCを理解を深めていきます
gRPCってなに?
Google が開発した RPC(Remote Procedure Call)システムです。
特徴をざっくりとまとめますと、以下のようなもの
- HTTP/2 通信をベースに高速かつ効率的な通信が可能
- Protocol Buffers と呼ばれるIDLを利用して、クライアントとサーバーの通信で必要なソースコードを自動作成してくれる
- クライアントとサーバで異なる言語を利用可能
(※画像はgRPC公式より抜粋)
余談ですが、gRPCのgはバージョンによって意味が違うらしいです (googleのgではなかったのか・・・)
https://github.com/grpc/grpc/blob/master/doc/g_stands_for.md
そもそも、RPCとはなんぞや?
ローカルなどの環境から他サーバー上の関数を呼び出して、実行するようとするような仕組みです。
これだけでは分かりづらいので、RESTとRPCを整理して比較して見てみましょう。
-
REST
設計思想としては、リソース(オブジェクト)を中心に考え、これに対して HTTP メソッドで操作していく。
(特定のリソースに対してCURD操作を行うのが前提)
シンプルでスケーラブルな API を作ることが前提にあり、
REST では原則を踏まえた上で、仕様を決めて実装を行います。 -
RPC
メソッドの呼び出しが基点であり、結果としてレスポンスとして返される形。
データが副産物という立場であるため、RESTの設計思想としては大きく異なります。
また、RESTと比較して制約が緩いため、比較的な柔軟なシステム実装が可能。
なるほど、なるほど・・・
それぞれ、設計の思想は異なっていて、用途よって使い分ける感じでしょうか
実際にはRESTで対応しきれない部分をRPCで作成する感じになりそうですね
gRPCの4つの通信方式とユースケース
gRPCは4つの通信方式があり用途別に使い分けることが出来ます。
(※いづれもProtocol Buffersを利用して、自動的に作成されるコードを用いて実装が可能です!)
Unary RPC
1つのリクエストに対して1つのレスポンスを返す一般的な通信方式。
(ex. サーバ間通信、APIなど)
Server streaming RPC
クライアントから送られてきた一つのリクエストに対して、複数回に分けてレスポンスを返す方式。
(ex. 任意のタイミングでの通知。push通知など)
Client streaming RPC
クライアントからリクエストを分割して送信。
サーバーはすべてのリクエストを受け取ってからレスポンスを返す方式。
(ex. クライアントから大きいデータが送信する必要がある場合)
Bidirectional streaming RPC
クライアントからリクエストが送られてきた際に、サーバーとクライアントはコネクションを確立。
任意のタイミングでレスポンスを取り合う方式。
(ex. オンライン対戦やチャットなど)
gRPCのメリット・デメリットについて
メリット
-
通信はやい。効率的。
- HTTP/2通信対応
- ヘッダの圧縮
- デフォルトでバイナリ通信
- リクエスト/レスポンスを並列処理が可能
- リクエストの優先順位付が可能
-
通信部分のコード生成が簡単 (自動でできる)
-
特定言語に依存しない
デメリット
- 基本的にはHTTP/2のみしか対応していない (※一部除く)
- それぞれの言語をビルドするのが若干手間・・・
- RESTのようなパスによる親子構造などはできない
- 動作確認時に、どうやってRPCメソッドを呼び出すのか検討する必要がある (専用のテストコード書く or ツールを導入)
- 負荷検証の際に、メジャーなツールがデフォルトではgRPCに対応してなかったりする (ex. jmeter や locust)
- gRPCをwebでも利用できるが、ブラウザ上の制限などいろいろ制約があったりする
https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md
ハンズオン
gRPCの雰囲気をつかむためにも簡単なハンズオンを用意しました
(※gRPC の getting start と AWS の prescriptive-guidance を参考にしてます)
以下の2つのハンズオンを用意してます
サンプル1(EC2 to EC2)
やること
EC2上にクライアント・サーバ用のコードを配置して、gRPCの通信を行います
(※ベースはgrpcのサンプルコードで変更を加えたもの)
前提情報
- Goが実行できるEC2を用意しておくこと(作業用環境 + Clinet + Server)
- SG(TCP:50051)に疎通用の穴をあけておくこと
環境面の補足
-
作業環境について
- amazon linux 2
- Go v1.17.8
-
作業環境にインストールする設定
- libprotoc v3.20.0
- protoc-gen-go-grpc v1.1
- protoc-gen-go v1.26
-
クライアントとサーバの環境について
- amazon linux 2
- Go v1.17.8
出来上がるディレクトリパスの構成
$ tree
grpc_demo
├── example_01
│ ├── client
│ │ ├── go.mod
│ │ ├── go.sum
│ │ └── main.go
│ └── server
│ ├── go.mod
│ ├── go.sum
│ └── main.go
└── proto
├── echo.pb.go
├── echo.proto
└── echo_grpc.pb.go
1. 環境の用意と設定ファイルの作成
- ファイルを自動生成させるために、Protocol Buffersをインストールする
https://github.com/protocolbuffers/protobuf/releases
$ curl -OL https://github.com/google/protobuf/releases/download/v3.20.0/protoc-3.20.0-linux-x86_64.zip
$ unzip protoc-3.20.0-linux-x86_64.zip -d protoc
$ sudo mv protoc/bin/* /usr/local/bin/
$ sudo mv protoc/include/* /usr/local/include/
# ちゃんとインストール・パスが通っているか確認
$ protoc --version
libprotoc 3.20.0
- Goのプロトコルコンパイラ用のプラグインをインストール。パスを通しておく
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.26
$ export PATH="$PATH:$(go env GOPATH)/bin"
- protoファイルを用意する
syntax = "proto3";
option go_package = "<XXXXXXX/grpc_demo/proto>"; // GITで管理
package echo;
service grpc_echo_test {
rpc hello_msg (EchoRequest) returns (EchoResponse) {}
}
message EchoRequest {
string req_message = 1;
}
message EchoResponse {
string res_message = 1;
}
- 用意したprotc ファイルを元に、gRPC通信を利用するためのファイルを生成
$ protoc --go_out=. \
--go_opt=paths=source_relative \
--go-grpc_out=. \
--go-grpc_opt=paths=source_relative proto/echo.proto
$ ll ./proto
-rw-rw-r-- 1 web web 6323 3月 25 11:15 echo.pb.go # 作成されたコード(メッセージやシリアライズ)
-rw-rw-r-- 1 web web 225 3月 24 11:00 echo.proto # 元のコード
-rw-rw-r-- 1 web web 3317 3月 25 11:15 echo_grpc.pb.go # 作成されたコード(gRPCのサーバ/クライアント)
# 作成したコードはGIT経由で参照させるため、それぞれの個人カウントでコードをプッシュ & go install しておく
go install XXXXXXX/grpc_demo/proto
- gRPCの通信を行うためのクライアント・サーバーファイルを用意
package main
import (
"context"
"flag"
"log"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
pb "XXXXXXX/grpc_demo/proto" // 先ほど作成したコード作成したファイルをGITから参照
)
type server struct {
pb.UnimplementedGrpcEchoTestServer
}
var (
port = flag.Int("port", 50051, "The server port")
)
func (s *server) HelloMsg(ctx context.Context, in *pb.EchoRequest) (*pb.EchoResponse, error) {
log.Println("rceived:" + in.GetReqMessage())
return &pb.EchoResponse{ResMessage: "Hello " + in.GetReqMessage()}, nil
}
func main() {
flag.Parse()
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
reflection.Register(s)
pb.RegisterGrpcEchoTestServer(s, &server{})
log.Printf("server listening at %v", lis.Addr())
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
package main
import (
"context"
"flag"
"log"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
pb "XXXXXXX/grpc_demo/proto" // 先ほど作成したコード作成したファイルをGITから参照
)
var (
addr = flag.String("addr", "<サーバ側のprivate ip>:50051", "the address to connect to")
name = flag.String("msg", "world", "hello to name")
)
func main() {
flag.Parse()
conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("connection failed: %v", err)
}
defer conn.Close()
c := pb.NewGrpcEchoTestClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := c.HelloMsg(ctx, &pb.EchoRequest{ReqMessage: *name})
if err != nil {
log.Fatalf("could not msg: %v", err)
}
log.Printf("rceived msg: %s ", r.GetResMessage())
}
2. Cliet/Serverにファイルを配置して疎通確認
- 各サーバ(Client/Server)にファイルを配置。go module 周りのファイルを生成しておく
go mod init XXXX
go mod tidy
- gRPCによる通信を行い、処理が返ってくることを確認
# server 側での実施
$ cd example/server/
$ go run main.go
2022/03/29 15:11:37 server listening at [::]:50051
2022/03/29 15:14:02 rceived:world
# clienet 側での実施
$ cd example/server/
$ go run main.go
2022/03/29 15:14:10 rceived msg: Hello world
※ 通信が行えていることが確認できたら、サンプル1のハンズオンはOK!
サンプル2 (ローカルPCからEKSのポッドへ)
やること
ローカルPCからインターネットを経由して、EKSに配置したポッドと通信を行います
(※ サンプル1で利用したサーバ側のファイルはそのまま流用します)
前提情報
- EKSクラスターが用意されていること
- kubectlがインストールされ、 EKS クラスターのリソースにアクセスするように設定されていること
- AWS Load Balancer ControllerがAmazon EKS クラスター上でデプロイされていること
- 利用可能なEcr/ACM/route53(ホストゾーン)が存在していること
- 作業環境でDockerが利用できること
環境面の補足
-
作業環境について
- amazon linux 2
- Docker v19.03.6-ce
- aws cli v1.18.147
-
サーバ側の環境について
- eks (Kubernetes バージョン) v1.21
- 利用するノードグループ: EC2(ASG)
-
クライアント側の環境について
- Ubuntu v20.04
- Go v1.17.8
- grpcurl v1.8.6 (※手順内で追加)
出来上がるディレクトリパスの構成
$ tree
grpc_demo
├── example_01 # EC2 to EC2 のハンズオン用のファイル・ディレクトリ
│ ├── client
│ │ ├── go.mod
│ │ ├── go.sum
│ │ └── main.go
│ └── server
│ ├── go.mod
│ ├── go.sum
│ └── main.go
├── example_02 # ローカルPC to EKS(Pods) のハンズオン用のファイル・ディレクトリ
│ ├── aws_resources
│ │ └── route53_record.json
│ ├── container
│ │ └── server
│ │ ├── Dockerfile
│ │ └── go
│ │ ├── go.mod
│ │ ├── go.sum
│ │ └── main.go # example1のファイルをそのまま流用
│ └── manifast
│ ├── deployment.yaml
│ └── ingress.yaml
└── proto # 共通で利用するファイル・ディレクトリ
├── echo.pb.go
├── echo.proto
└── echo_grpc.pb.go
9 directories, 13 files
Server側の設定内容
1. Go と Docekr image用のファイルの用意
- サンプルケース1で用意したproto ファイルとgoファイル(server)をそのまま流用して配置
$ mkdir -p example_02/container/server/go
$ cp example_01/server/main.go example_02/container/server/go
# ディレクトリにもぐり、go moduleの管理するためのファイルを作成
$ example_02/container/server/go
$ go mod init XXXX
$ go mod tidy
- コンテナイメージ用のファイルを用意
FROM golang:1.17
WORKDIR /app
RUN apt-get update && \
apt-get -y install git unzip build-essential autoconf libtool
COPY go /app/grpc-server
EXPOSE 50051
ENTRYPOINT cd /app/grpc-server && go run /app/grpc-server/main.go
- ECRへイメージをプッシュ
# ログイン
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com
# イメージを作成して
docker build -t <タグ名> .
docker tag <タグ名>:latest <アカウント名>.dkr.ecr.ap-northeast-1.amazonaws.com/<リポジトリ名>:latest
# ecrへプッシュ
docker push <アカウント名>.dkr.ecr.ap-northeast-1.amazonaws.com/<リポジトリ名>:latest
2.pod/albを展開するためのマニフェストファイルの用意
- Deplpyment と Service 用のファイルを用意
apiVersion: apps/v1
kind: Deployment
metadata:
name: grpcserver
namespace: <任意のnamespace名>
spec:
selector:
matchLabels:
app: grpcserver
replicas: 1
template:
metadata:
labels:
app: grpcserver
spec:
containers:
- name: grpc-demo
image: <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/<ecrのリポジトリ名>:latest
imagePullPolicy: Always
ports:
- name: grpc-server
containerPort: 50051
restartPolicy: Always
---
apiVersion: v1
kind: Service
metadata:
name: "grpc-service-demo"
namespace: <任意のnamespace名>
annotations:
service.alpha.kubernetes.io/app-protocols: '{"grpc":"HTTP2", "health": "HTTP2"}'
spec:
type: NodePort
ports:
- port: 50051
targetPort: 50051
protocol: TCP
selector:
app: "grpcserver"
- ingress用の設定ファイルを用意
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
annotations:
alb.ingress.kubernetes.io/actions.ssl-redirect: '{"Type": "redirect", "RedirectConfig":
{ "Protocol": "HTTPS", "Port": "443", "StatusCode": "HTTP_301"}}'
alb.ingress.kubernetes.io/backend-protocol-version: GRPC
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS":443}]'
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/subnets: subnet-XXXXXXX, subnet-XXXXXXX # 任意のサブネット
alb.ingress.kubernetes.io/security-groups: sg-XXXXXXX, sg-XXXXXXX # 任意のSG (※443ポート開放済みのもの)
alb.ingress.kubernetes.io/healthcheck-path: /
alb.ingress.kubernetes.io/healthcheck-protocol: HTTP
alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:ap-northeast-1:<アカウントID>:certificate/XXXXXX # 任意のACM
labels:
app: grpcserver
environment: dev
name: grpcserver
namespace: <任意のnamespace名>
spec:
rules:
- host: <任意のホスト名>
http:
paths:
- backend:
serviceName: ssl-redirect
servicePort: use-annotation
path: /*
- backend:
serviceName: grpc-service-demo
servicePort: 50051
- マニフェストファイルからPOD/ALBを作成する
# depolymentが作成されポッドが配置されることを確認
$ kubectl apply -f manifast/deployment.yaml
$ kubectl get deploument -n <任意のNamespace名>
NAME READY UP-TO-DATE AVAILABLE AGE
grpcserver 1/1 0 1 4m
$ kubectl get pod -n <任意のNamespace名>
NAME READY STATUS RESTARTS AGE
grpcserver-678dcdd5b8-kzh78 1/1 Running 0 5m1s
# albが作成されることを確認
$ kubectl apply -f manifast/ingress.yaml
$ kubectl get ingress -n <任意のNamespace名>
NAME CLASS HOSTS ADDRESS PORTS AGE
grpcserver <none> <任意のホスト名> <alb名>.ap-northeast-1.elb.amazonaws.com 80 12m
3.外部からドメインを利用してアクセスできるようにRoute53レコードを作成
- レコード登録のためにjsonファイルを用意
{
"Comment": "Alias record for grpc",
"Changes": [
{
"Action": "CREATE",
"ResourceRecordSet": {
"Name": "<登録するドメイン>",
"Type": "A",
"AliasTarget": {
"HostedZoneId": "<ELBのホストゾーンID>",
"DNSName": "dualstack.<先ほど作成したALBのDNS>",
"EvaluateTargetHealth": false
}
}
}
]
}
- aws cli で route53にレコードを登録
$ aws route53 change-resource-record-sets --hosted-zone-id <Hostzone id(route53)> --change-batch file://route53_record.json --region=ap-northeast-1
{
"ChangeInfo": {
"Status": "PENDING",
"Comment": "Alias record for grpc",
"SubmittedAt": "2022-03-29T12:16:43.769Z",
"Id": "/change/C082484523D9BRL0OXFSD"
}
}
Client側の設定内容と疎通確認
- grpcurlのインストール。パスも通しておく
$ go install github.com/fullstorydev/grpcurl/cmd/grpcurl@v1.8.6
$ export PATH="$PATH:$(go env GOPATH)/bin"
- 外部からリクエストを飛ばし、想定したレスポンスが返ることを確認
# grpcサーバで利用しているサービス名を確認
$ grpcurl <登録したドメイン>:443 list
echo.grpc_echo_test
grpc.reflection.v1alpha.ServerReflection
# 中身のRPC名まで確認
$ grpcurl <登録したドメイン>:443 list echo.grpc_echo_test
echo.grpc_echo_test.hello_msg
# 上記で取得したRPCを指定して、リクエストを飛ばす
$ grpcurl -d '{"req_message": "hogehoge"}' <登録したドメイン>:443 echo.grpc_echo_test.hello_msg
{
"resMessage": "Hello hogehoge"
}
※ サーバから上記のようなレスポンスが返ってきたら、サンプル2のハンズオンはOK!
おわりに
今回 gRPCの雰囲気を掴むために、サンプルを用意しつつ、概要を追ってみました。
感覚としては、Protocol Buffersで生成されたファイルについて、ある程度の理解が必要そうですが、
中身を把握すればいろんな場面で応用出来そうな感じでした。
(動作確認の方法が少し面倒くさい形になりそう・・・)
今回で利用方法は把握出来たので、WEB 経由での gRPC の利用やHTTP/1 vs HTTPS/2 の性能比較なども、
今後記事などにしていきたいなと思いました
参考