gRPC & Go & Ruby SDK 作った

目的

現在属している会社では、UGCのサービスを運用していて、大きなDBに、主要な情報が詰まってる。
そのDBに対して、複数のアプリケーションから情報を登録していたが、主要なデータのインサートについては、専用のアプリケーションに任せたいとなった。
そこで、Go lang で、gRPCサーバ作って、DB操作ができるようにしてみた。

やったこと

gRPCサーバ側

proto ファイル作成

protoファイルを例をもとに作成した。

protoファイルから、go や ruby への変換は、下の docker ファイルを利用した。 github.com

環境構築

fresh を利用して、hot reloadできるようにした。

Dockerfile

FROM golang:1.13

WORKDIR /src

RUN go get github.com/pilu/fresh

ENV GO111MODULE=on

EXPOSE 50051

ADD . /src

CMD ["fresh"]

docker-compose.yml

version: '3'
services:
  app:
    build: .
    volumes:
      - .:/src
    ports:
      - "50051:50051"

go.mod

module main

go 1.13

main.go

package main

import "fmt"

func main() {
  fmt.Println("test")
}

起動

$ docker-compose up --build

go ファイル作成

main.go をこんな感じに書き換えて、あとは必要なファイルを作っていった。

package main

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
    "github.com/grpc-ecosystem/go-grpc-middleware"
    "github.com/jinzhu/gorm"
    "google.golang.org/grpc"
    health "google.golang.org/grpc/health"
    healthpb "google.golang.org/grpc/health/grpc_health_v1"
    "google.golang.org/grpc/reflection"
    "log"
    "main/infrastructure"
        "main/middleware"
    test1 "main/proto/go/test1"
    test2 "main/proto/go/test2"
    "main/service"
    "net"
)

const (
    port = ":50051"
)

func main() {
    log.Printf("process start")
    infrastructure.EnvLoad()

    lis, err := net.Listen("tcp", port)
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }

   // ORMは一旦Gormを使った。
    var db *gorm.DB
    db = infrastructure.RoomclipDb()
    defer db.Close()

   // middlewareをここで設定
    s := grpc.NewServer(grpc.UnaryInterceptor(
        grpc_middleware.ChainUnaryServer(
            middleware.NewZap(),
            middleware.NewRecovery(),
        )),
    )

    // health check
    healthpb.RegisterHealthServer(s, health.NewServer())

    // こんな感じにサービス複数持てる
    test1Service := &service.Test1Service{Db: db}
    test1.RegisterFolderServer(s, test1Service)

    test2Service := &service.Test2Service{Db: db}
    test2.RegisterLikeServer(s, test2Service)

    // Register reflection service on gRPC server.
    reflection.Register(s)
    log.Printf("grpc server running")
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

localで確認

こちらがすごく便利だった。 github.com

こんな感じで確認できる。

$ grpcurl -plaintext localhost:50051 list
$ grpcurl -plaintext localhost:50051 list test1.Test
$ grpcurl -plaintext localhost:50051 test1.Test.test
$ grpcurl -d '{ "name": "hogehoge" }' -plaintext localhost:50051 test1.Test.test

デプロイ

デプロイは、普段通り github => CodePipeline => CodeBuild(ECRへイメージpush)=> ECS
とした。

実際の環境はVPC内部のアプリケーションから呼ばれる想定なので、Service Discovery を用いてルーティングを一旦行うことにした。
ただ、社内で使うtest環境は、Service Discovery を用いて、VPC外からアクセスする方法ができなさそうだったので、CodePipelineの最後に、Route53のAレコードを書き換えるようにしてアクセスするようにした。

当初は、AppMesh使いたいなーと思っていたが、まだサポートしてないっぽいので、一旦諦めた。

呼び出し側

sdk作成

今回は、一旦rubyのみ作成した。
サンプル通り作れば思った以上に簡単にできました。

sdk利用するアプリケーション

Gemfileに書いて、Docker上で試そうとするとエラーが発生した...

alpine linux の imageを使ってたから発生したらしい。

Error loading shared library ld-linux-x86-64.so.2 というエラーが起きるから、
こちらのサイト通りに追加してみたら、/lib64/ld-linux-x86-64.so.2 ってファイルが追加されたけど、うまくいかなかった。
AlpineコンテナでNode.jsからgRPCを利用する場合、libc6-compatが必要 · tehepero note(・ω<) 2.0

最終的に、bundle install 時に、環境変数を渡したら上手くいった。
https://github.com/protocolbuffers/protobuf/issues/4460
BUNDLE_FORCE_RUBY_PLATFORM=1としたら、以下のようになるらしい。

現在のマシンのプラットフォームを無視し、Rubyプラットフォームのgemのみをインストールします。その結果、ネイティブ拡張を持つgemはソースからコンパイルされます。

一方、CFLAGS オプションの意味がよくわからないので、わかる人いらっしゃったらお教えいただけると...

# BUNDLE_FORCE_RUBY_PLATFORM を利用しないと、protobufで、segmentation faultが発生する。
# しかし、BUNDLE_FORCE_RUBY_PLATFORMを利用すると、grpc gem がインストールできない。
# CFLAGSをつけると、問題なくインストールできるようになった。
RUN CFLAGS="-Wno-cast-function-type" \
      BUNDLE_FORCE_RUBY_PLATFORM=1 \
      bundle install --jobs=4

終わり

初めてgo でサービス作ったが、違和感なく実装はできた。
最近、typescript触ってて、型のある言語になれてたからかも。
DBにインサートする処理がほとんどのサービスを、goで作る必要はあったのかと言われると、Active Recordの方が簡単だとは思うので、変更はあるかも。
一方で、gRPCというか、protoファイルによる定義は戻り値とか明確に定義できていいなーと思った。
今後は、ログ周りとか整備して、高負荷環境に耐えうるようにしていきたい。

個人的な面では、DB & gRPC周りのテストをどうやって書いたらいいのかを勉強しなきゃなと思った。