Help us understand the problem. What is going on with this article?

NginxにgRPCとHTTP/1.1の共存の設定をする

この記事は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) のリクエストを振り分けをするわけです。

実現したいこと

image.png

動作環境

  • Macbook Pro
  • docker
  • go
  • grpcurl:gRPCリクエスト用
  • Postman:HTTPリクエスト用

Nginx設定

default.conf
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

doker-compose.yml
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

構成略図

image2.png

動作確認

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の出力が表示されていれば、期待通りのプロキシがされたという確認ができました。

注意

grpcurl50051ポートでもgRPCサーバはレスポンスが返りますが、Nginxを経由していないリクエストになります。Nginxの恩恵を受けるには必ずNginx側のListenポート(50055)からのアクセスが必要です。

補足:APIサーバ

リクエストパスはapi以下として、1188ポートでHTTPのリクエストを受けるように設定します。

server.go
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でリクエストを受けます。

server.go
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)
    }
}

詳しく知りたい方は、ソースコード:こちら で確認できます。

まとめ

Nginxの公式サイトなどをみながら、試行錯誤しながらなんとか動作確認までできました。
gRPCをはじめ、使い慣れていないツールなどもあり、色々と詰まる局面もありましたが、Nginxの利点を生かせるサービスができるようになってよかったです。また、本格的なマイクロサービス化への第1歩になりそう。
今回は、Nginxの話だったので、dockerとかgRPCの内容はさらっと書いてます。詳しくはレポジトリにコードあります。
最後に、本番環境での確認については書いていませんが、それぞれのデプロイ先の環境に合わせる必要があるため、必ず動作確認してからリリースしましょう!
:santa: 楽しいクリスマスをお過ごしください!:evergreen_tree:
明日は @ikeponsu さんによる「GASでGoogleドライブ監視システムを作成」です。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした