この記事はZOZOテクノロジーズ 4 Advent Calendar 2019 2日目の記事になります。昨日は @ararajp さんの「評価する際のマイルール 」でした。
はじめに
ZOZOの生産チームでは、Go言語でバックエンドのAPI開発をしています。今期になってgRPCを用いたAPIの開発を行おうということになり、既存のRESTのAPIとの共存ができるのかという課題がでてきました。その際に行った実装と検証について書こうと思います。
Nginxの1.13.10からgRPCがサポートされています(最新のバージョンは執筆時1.17です)。基本的には公式サイトのExample実装を弊社プロジェクトに置き換えるという流れで実装と検証をすすめました。
Introducing gRPC Support with NGINX
gRPCとは
Google社が開発をした通信プロトコルの1つです。高速に通信できたり非同期データ通信などが可能になる特徴があります。HTTP2を利用しています。gRPCについての詳細は、公式サイトの https://grpc.io/ にあります。
Nginxとは
Webサーバへのリクエストをさばくソフトです。同じ仕事をする仲間で、ApacheとかIISとかあります。
一番の特徴としてリバースプロキシという機能があります。この機能を使って、HTTPとHTTP2 (gRPC) のリクエストを振り分けをするわけです。
実現したいこと
動作環境
- Macbook Pro
- docker
- go
- grpcurl:gRPCリクエスト用
- Postman:HTTPリクエスト用
Nginx設定
upstream api-app {
server api:1188;
}
upstream mikan-grpc-app {
server grpc:50051;
}
server {
listen 80 default_server;
listen 50055 http2 default_server;
server_name _;
real_ip_header X-Forwarded-For;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
location / {
access_log /var/log/nginx/access.log main;
grpc_pass grpc://mikan-grpc-app;
}
location /api {
access_log /var/log/nginx/access.log main;
proxy_pass http://api-app;
}
}
upstream
:HTTP1用とHTTP2用をそれぞれ2つ定義しています。
upstream | 説明 |
---|---|
api-app:1188 | HTTPリクエストを処理するサーバ |
mikan-grpc-app:50051 | HTTP2リクエストを処理するサーバ |
server
ブロックのListenの設定
Listenポート | 説明 |
---|---|
80 | 通常のHTTPリクエストを待ち受ける |
50055 | HTTP2リクエスト(gRPC)を待ち受ける |
location
:リクエストに対してのプロキシ設定
パス | 説明 |
---|---|
/ |
デフォルトはすべてgRPCで処理されるようする |
/api |
api が含まれる場合は、通常のHTTPリクエストとして処理される |
プロトコル・パス設定
grpc_pass
はgRPC用、proxy_pass
は通常のHTTPリクエスト用。
- プロトコル・スキームが間違いやすいので注意する
docker-compose
APIサーバとgRPCサーバ、Nginxを設定します。
リクエストのListen設定は
- HTTPリクエスト(api):8080
- HTTP2リクエスト(grpc):50055
version: "3"
services:
api:
build:
context: .
dockerfile: "docker/Dockerfile.api"
volumes:
- .:/api
ports:
- 1188:1188
grpc:
build:
context: .
dockerfile: "docker/Dockerfile.mikan"
volumes:
- .:/grpcapi
ports:
- 50051:50051
nginx:
build:
context: .
dockerfile: "docker/Dockerfile.nginx"
volumes:
- "./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro"
- "./nginx/nginx.conf:/etc/nginx/nginx.conf:ro"
links:
- "api"
- "grpc"
ports:
- 8080:80
- 50055:50055
構成略図
動作確認
dockerで確認してみます。
# docker-compose up --build
api_1 |
api_1 | ____ __
api_1 | / __/___/ / ___
api_1 | / _// __/ _ \/ _ \
api_1 | /___/\__/_//_/\___/ v3.3.10-dev
api_1 | High performance, minimalist Go web framework
api_1 | https://echo.labstack.com
api_1 | ____________________________________O/_______
api_1 | O\
api_1 | ⇨ http server started on [::]:1188
grpc_1 | Starting Mikan Service
api(echo)とgrpcが起動していればOKです。
PostmanでHTTPリクエストする
リクエストするURL:http://localhost:8080/api/ping
nginx_1 | {"date_time": "2019-11-20T05:37:13+00:00","time_ms": "1574228233.287","ip": "","host": "localhost","req_time": "0.006","ups_time": "0.010","run_time": "","status": "200","bytes_recv": "262","method": "GET","request_body": ""}
とNginx
のログが出力されれば、リクエストが期待通りにプロキシされているという確認ができました。
gRPCのリクエストする
gRPCクライアントを実装するという選択肢もありますが、今回は、grpcurl
というツールを使います。
docker版などもありますが、macだとHomebrewなどでインストールできます。grpcurl
簡単にgRPCのリクエストをエミュレートできます。
別ターミナルからgRPCのリクエストをします。リクエストは.proto
に合わせてリクエストするので、例えばこんな感じになります。
grpcurl -plaintext -d '{"mikan": {"Name": "Request Mikan", "Kind": "Good", "Quality": 16} }' localhost:50055 mikan.MikanService/Mikan
grpcurlの使い方もいろいろありますが、上記の例を説明すると
項目 | 説明 |
---|---|
localhost:50055 | NginxがListenしているgRPCリクエストのポート |
-d | 送信データ(.protoの定義によって変わります) |
mikan.MikanService/Mikan | MikanServiceのMikanという関数・機能 |
という内容です。 | |
*後述のgRPCサーバのコード内に定義してあります。 |
grpc_1 | Mikan function was invoked with mikan:<Name:"Request Mikan" Kind:"Good" Quality:16 >
nginx_1 | {"date_time": "2019-11-20T06:04:58+00:00","time_ms": "1574229898.868","ip": "","host": "localhost","req_time": "0.001","ups_time": "0.000","run_time": "","status": "200","bytes_recv": "63","method": "POST","request_body": ""}
NginxのアクセスログとgRPCの出力が表示されていれば、期待通りのプロキシがされたという確認ができました。
注意
grpcurl
で50051
ポートでもgRPCサーバはレスポンスが返りますが、Nginx
を経由していないリクエストになります。Nginx
の恩恵を受けるには必ずNginx
側のListen
ポート(50055)からのアクセスが必要です。
補足:APIサーバ
リクエストパスはapi
以下として、1188ポートでHTTPのリクエストを受けるように設定します。
package main
import (
"net/http"
"time"
"github.com/labstack/echo"
)
func main() {
e := echo.New()
e.GET("/api/", func(c echo.Context) error {
return c.String(http.StatusOK, "API-/")
})
e.GET("/api/ping", func(c echo.Context) error {
layout := "2006-01-02 15:04:05"
loc, _ := time.LoadLocation("Asia/Tokyo")
now := time.Now().In(loc).Format(layout)
return c.String(http.StatusOK, "API:Ping - "+now)
})
e.Logger.Fatal(e.Start(":1188"))
}
補足:gRPCサーバ
リクエストパスは/
以下をすべてとして、ポートは50051
でリクエストを受けます。
package main
import (
"context"
"fmt"
"log"
"net"
"strconv"
"github.com/nakashimanh/mikans/mikanpb"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
)
type server struct{}
func (*server) Mikan(ctx context.Context, req *mikanpb.MikanRequest) (*mikanpb.MikanResponse, error) {
fmt.Printf("Mikan function was invoked with %v\n", req)
name := req.GetMikan().GetName()
kind := req.GetMikan().GetKind()
quality := req.GetMikan().GetQuality()
result := "Response Mikan= " + name + " kind= " + kind + " quality= " + strconv.FormatInt(quality, 10)
res := &mikanpb.MikanResponse{
Result: result,
}
return res, nil
}
func main() {
fmt.Println("Starting Mikan Service")
lis, err := net.Listen("tcp", "0.0.0.0:50051")
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
s := grpc.NewServer()
mikanpb.RegisterMikanServiceServer(s, &server{})
reflection.Register(s)
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
詳しく知りたい方は、ソースコード:こちら で確認できます。
gRPCサーバの構築については、こちらの記事 を御覧ください。
まとめ
Nginxの公式サイトなどをみながら、試行錯誤しながらなんとか動作確認までできました。
gRPCをはじめ、使い慣れていないツールなどもあり、色々と詰まる局面もありましたが、Nginxの利点を生かせるサービスができるようになってよかったです。また、本格的なマイクロサービス化への第1歩になりそう。
今回は、Nginxの話だったので、dockerとかgRPCの内容はさらっと書いてます。詳しくはレポジトリにコードあります。
最後に、本番環境での確認については書いていませんが、それぞれのデプロイ先の環境に合わせる必要があるため、必ず動作確認してからリリースしましょう!
楽しいクリスマスをお過ごしください!
明日は @ikeponsu さんによる「GASでGoogleドライブ監視システムを作成」です。