読者です 読者をやめる 読者になる 読者になる

Yuichi Murata's Engineering Blog

Go / App Engine / GCP とか書いていきます

Go + App Engine で作る API Gateway

こんにちは、むらたです。Advent Calender 初めてのチャレンジです。

自分は職務でよく、App Engine で Microservices 構成をとった開発をやります。例えばこんな感じの構成です。

DeNAでのGCP活用事例とGCP NEXTでの事例紹介 — Mobage Developers Blog

この時困るのが、各サービスのゲートウェイの設計です。App Engine の場合、各サービスにそれぞれ appspot.com ドメインが振られるので、例えば example-dot-project-id.appspot.com のようなドメインが振られます。このサービス用のドメインで直接アクセスすれば良いという話もありますが、そうも行かない場合もあります。同一のドメインAPI をさばいたり、コンポーネントとパスの構成を柔軟に行いたい場合などです。

dispatcher.yaml を使う手もありますが、dispatcher.yaml はエントリー数が制限されたり、他のプロジェクトのアプリに転送ることは叶わなかったりします。

そこで本稿では App Engine のデフォルトモジュールとして API ゲートウェイを動かし、バックエンドのサービスへのリクエストを中継する構成というのを考えてみます。

ソース

以下にソースコードをアップロードしてあります。

GitHub - yuichi1004/ae-gateway: App Engine API Gateway

仕組み

仕組みはいたってシンプルで gateway.yaml に書かれた設定に従って、urlfetch を用いてバックエンドのサービスにリクエストを中継するだけです。いっけん、レイテンシーなどが気になるところですが、appspot.com のドメインに対する urlfetch は Google のネットワーク外に出ないので、同一のリージョン内の通信であれば、とても高速に動作すると仮定できます。

func handleGatewayRequest(c GatewayRoute) {
    http.HandleFunc(c.Pattern, func (w http.ResponseWriter, r *http.Request) {
        ctx := appengine.NewContext(r)
        client := urlfetch.Client(ctx)

        dstUrl := strings.Replace(r.URL.Path, c.Pattern, c.Dest, 1)

        log.Debugf(ctx, "request to %s %s", r.Method, dstUrl)
        req, err := http.NewRequest(r.Method, dstUrl, r.Body)
        if err != nil {
            log.Warningf(ctx, "failed to make request: %v", err)
            w.WriteHeader(http.StatusInternalServerError)
            return
        }

        resp, err := client.Do(req)
        if err != nil {
            log.Warningf(ctx, "failed to process request: %v", err)
            w.WriteHeader(http.StatusInternalServerError)
            return
        }

        body, err := ioutil.ReadAll(resp.Body)
        if err != nil {
            log.Warningf(ctx, "failed to read upstream response: %v", err)
            w.WriteHeader(http.StatusInternalServerError)
            return
        }

        w.WriteHeader(resp.StatusCode)
        if _, err := w.Write(body); err != nil {
            log.Warningf(ctx, "failed to send response to downstream: %v", err)
        }
    })
}

実質的に行っている処理はこれだけ。至ってシンプルです。

もっと複雑なルーティングをしたいとか、認証・認可をはさみたいとか要望があれば、処理を追加することも簡単です。

気になるレイテンシー

簡単な負荷試験を行ってみました。ユーザーの情報を取得する API なるものをでっち上げ、直接アクセスする場合と、この Gateway を用いて API にアクセスした場合のレイテンシーを比較してみます。

== Gateway 通す版 ==
Server Software:        Google
Server Hostname:        *****.appspot.com
Server Port:            443
SSL/TLS Protocol:       TLSv1.2,ECDHE-RSA-CHACHA20-POLY1305,2048,256


Document Path:          /users/1
Document Length:        21 bytes


Concurrency Level:      10
Time taken for tests:   12.443 seconds
Complete requests:      1000
Failed requests:        0
Total transferred:      267008 bytes
HTML transferred:       21000 bytes
Requests per second:    80.37 [#/sec] (mean)
Time per request:       124.429 [ms] (mean)
Time per request:       12.443 [ms] (mean, across all concurrent requests)
Transfer rate:          20.96 [Kbytes/sec] received


Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:       36   79  20.7     79     190
Processing:    13   44  23.7     43     233
Waiting:       13   36  19.2     32     160
Total:         56  123  28.5    122     335


Percentage of the requests served within a certain time (ms)
  50%    122
  66%    130
  75%    135
  80%    140
  90%    154
  95%    163
  98%    187
  99%    236
 100%    335 (longest request)
 
== 生 Req ==
Server Software:        Google
Server Hostname:        users-dot-****.appspot.com
Server Port:            443
SSL/TLS Protocol:       TLSv1.2,ECDHE-RSA-CHACHA20-POLY1305,2048,256


Document Path:          /users/1
Document Length:        21 bytes


Concurrency Level:      10
Time taken for tests:   12.398 seconds
Complete requests:      1000
Failed requests:        0
Total transferred:      267008 bytes
HTML transferred:       21000 bytes
Requests per second:    80.66 [#/sec] (mean)
Time per request:       123.984 [ms] (mean)
Time per request:       12.398 [ms] (mean, across all concurrent requests)
Transfer rate:          21.03 [Kbytes/sec] received


Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:       34   86  22.7     85     249
Processing:    10   37  24.7     32     154
Waiting:        9   28  17.5     22     114
Total:         54  123  32.2    117     373


Percentage of the requests served within a certain time (ms)
  50%    117
  66%    130
  75%    139
  80%    144
  90%    159
  95%    185
  98%    210
  99%    227
 100%    373 (longest request)

Total のレイテンシーの中央値を見ると直接 117 ms に対して Gateway を通したものは 122 ms。もちろんレイテンシーは落ちていますが、5 ms とほぼ無視してよいレベルのようにも見えます。

まとめ

このような API Gateway を用いると柔軟に API のパスを配置しつつも、Microservices 構成をとることができそうです。また、ほとんど無視できるレベルのレイテンシーAPI Gateway を構築できました。またこの方式は複数のプロジェクトにまたがったリクエストのディスパッチも可能なので、より大規模なサービスにも適用できそうです。

さて、明日のエントリーは koki_cheese さんです。みなさんお楽しみに。