5
Help us understand the problem. What are the problem?

posted at

updated at

gRPC+Go を触ってみたので、ざっくり纏めてみた [ハンズオンつき]

はじめに

最近、社内でマイクロサービスが話題に上がるにつれ、
gRPCという言葉をよく聞くようになりました。

前から興味があったことと、今後業務でgRPCで使う可能性がありそうとのことだったので、
概要と雰囲気を掴むために記事にまとめてみました。

記事の読み進め方

記事の内容を下2つに分けてgRPCを理解を深めていきます :eyes:

gRPCってなに?

Google が開発した RPC(Remote Procedure Call)システムです。

特徴をざっくりとまとめますと、以下のようなもの

  • HTTP/2 通信をベースに高速かつ効率的な通信が可能
  • Protocol Buffers と呼ばれるIDLを利用して、クライアントとサーバーの通信で必要なソースコードを自動作成してくれる
  • クライアントとサーバで異なる言語を利用可能

gRPC_Image_01.png

(※画像は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で作成する感じになりそうですね :rolling_eyes:

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)

qiita-01.png

やること

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. 環境の用意と設定ファイルの作成
$ 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ファイルを用意する
proto/echo.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の通信を行うためのクライアント・サーバーファイルを用意
example_01/server/main.go
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)
	}
}
example_01/client/main.go
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のポッドへ)

qiita-02.png

やること

ローカル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
  • コンテナイメージ用のファイルを用意
example_02/container/server/Dockerfile
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 用のファイルを用意
manifast/deployment.yaml
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用の設定ファイルを用意
manifast/ingress.yaml
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ファイルを用意
route53_record.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にレコードを登録
example_02/aws_resources/route53_record.json
$ 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 の性能比較なども、
今後記事などにしていきたいなと思いました :writing_hand_tone3:

参考

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
5
Help us understand the problem. What are the problem?