Yuichi Murata's Engineering Blog

グローバル・エンジニアリング・チームをつくる

Auto Scaling と Basic Scaling は何が違うのか 【基本性能編】

App Engine の Scale Type には Manual Scaling / Basic Scaling / Auto Scaling の 3 種類があります。

https://cloud.google.com/appengine/docs/go/an-overview-of-app-engine#scaling_types_and_instance_classes

このうち Basic Scaling と Auto Scaling はいずれもユーザーからのリクエスト数に応じてインスタンス数が増減するものでありますが、果たして何が違うのでしょうか。気になったので調べてみました。

本稿では、Basic Scaling と Auto Scaling の基本性能に差があるのか調べてみたという話をします。

続きを読む

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 さんです。みなさんお楽しみに。

Go の 3 つのエラーハンドリングパターン

今回は go のエラーハンドリングのパターンについて書いていきたいと思います。以下の Post とかなり被る部分があるので合わせて参考にしてください。

Error handling and Go - The Go Blog

Pattern 1: エラーを値として定義する

一番単純な方法としてエラーを値として定義してしまう方法があります。

var ErrNeedCake = errors.New("error - i need cake not this")
func Func1(name string) error {
        if name != "cake" {
                return ErrNeedCake
        }
        return nil
}

Func1() は引数 name をとり、引数がケーキでなければエラーを返すというシンプルな関数です。ここで ErrNeedCake というエラー構造体の値をパッケージ変数として定義しておきます。この方法を使うと、最もシンプルにエラーを定義することが出来ます。以下のようにすれば、呼び出し元のエラー判定にも使うことが出来ます。

func main() {
        if err := Func1("chocolate"); err != nil {
                if err == ErrNeedCake {
                        fmt.Printf("Hmm, he seems to need a cake\n")
                } else {
                        fmt.Printf("Got unexpected error!\n")
                        panic(err)
                }
        }
}

いっぽう、固定のエラーメッセージしか返すことが出来ないため、エラーの内容の詳細を伝えるとができません。

Pattern 2: 動的にエラー値を生成する

fmt パッケージには Printf と同様のフォーマットでお気軽に error 値を作ることができます。これを用いて動的にエラー値を作ることができます。

func Func2(name string) error {
    if name != "cake" {
        return fmt.Errorf("error - i need cake not %s", name)
    }
    return nil
}

おそらく最も簡単なエラーの返し方でしょう。また、メッセージを動的に生成できるのでより詳細な情報を返すことができます。

func main() {
    fmt.Println("\n== Pattern 2: Creates ad-hok ==")
    if err := Func2("banana"); err != nil {
        fmt.Printf("Func2() got error: %v\n", err)
    }
}

ただしこの方法はエラーの内容を表示する分には良いですが、分岐させることが難しいことです。簡単な使い捨てプログラムを書いているときや、クライアントが賢く分岐しないような局面では最もシンプルな戦略です。

Pattern 3: 独自エラー型を定義する

最も真面目でしっかりとエラーハンドリングをするには error interface を実装する独自のエラー型を定義することです。

type ErrNeedElse struct {
    Got  string
    Need string
}

func (e ErrNeedElse) Error() string {
    return fmt.Sprintf("error - i need %s not %s", e.Need, e.Got)
}

func Func3(name string) error {
    if name != "cake" {
        return ErrNeedElse{name, "cake"}
    }
    return nil
}

この方法を使うと動的に詳しいエラーメッセージを生成して表示できるようになるだけではなく、エラー型や詳細なフィールドを返すことができます。これを用いてより賢いエラーハンドリングをすることが可能です。

func main() {
    if err := Func3("strawberry"); err != nil {
        switch err := err.(type) {
        case ErrNeedElse:
            fmt.Printf("Hmm, he seems to need %s not %s\n", err.Need, err.Got)
        default:
            fmt.Printf("Got unexpected error!\n")
            panic(err)
        }
    }
}

ちょっと手間ではありますが、再利用性の高いパッケージや複雑なエラーを返すパッケージはこの方法を検討すべきです。

Pattern 3 (番外編): 独自エラー型はかならず error interface で返す

状況によっては error interface ではなく、以下の様に独自エラー型をそのあまま値として返したい衝動に駆られます。

func Func4(name string) *ErrNeedElse {
        if name != "cake" {
                return &ErrNeedElse{name, "cake"}
        }
        return nil
}

もし返されるエラー型が独自型に限定されるのであれば、型アサーションの必要がありません。呼び出し元のエラーハンドリングも簡単になります。この場合、以下のようなシンプルな構成にすることが可能です。

func main() {
    if err := Func3("strawberry"); err != nil {
                 fmt.Printf("Hmm, he seems to need %s not %s\n", err.Need, err.Got)
    } 
}

しかし、この方法はあまりおすすめ出来ないです。Go 言語は error interface を使うことによって、様々なパッケージのエラーハンドリングを共通化することができています。しかし、独自のエラー型の値をインタフェースとすることなく、そのまま返してしまうと辻褄の合わないケースがでてきます。例えば以下のケースを見てみましょう。

func Func4(name string) *ErrNeedElse {
        if name != "cake" {
                return &ErrNeedElse{name, "cake"}
        }
        return nil
}

type GiftToYou struct {
        Name string `json:name`
}

func (g *GiftToYou) UnmarshalJSON(data []byte) error {
        type alias *GiftToYou
        var this alias = alias(g)
        json.Unmarshal(data, &this)

        return Func4(g.Name)
}

ある構造体 GiftToYou の UnmarshalJSON() 関数を定義することによって、json の Bind 時に独自のロジックを走らせることができます。この時にエラーが発生したら、エラーを返すという処理を考えてみましょう。呼び出し側はこのようにしてみます。

func main() {
        v := GiftToYou{}
        if err := json.Unmarshal([]byte(`{"name":"cake"}`), &v); err != nil {
                switch err := err.(type) {
                case *ErrNeedElse:
                        fmt.Printf("Hmm, he seems to need %s not %s\n", err.Need, err.Got)
                default:
                        fmt.Printf("Got unexpected error!\n")
                        panic(err)
                }
        }
}

さて、このプログラムを実行すると、正常に処理が終了するはずが、なぜか以下の様な panic が置きます。

panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xb code=0x1 addr=0x0 pc=0x30af]

goroutine 1 [running]:
panic(0x10d8c0, 0xc82000a1d0)
        /usr/local/Cellar/go/1.6.2/libexec/src/runtime/panic.go:481 +0x3e6
main.main()
        /Users/yuichi.murata/go/src/github.com/yuichi1004/go-error-handling/main.go:100 +0xb3f
exit status 2

結論から言うとこれは nil の方情報が一致しないためです。go の nil には型情報があって、Func4() の戻り値に nil が返された場合、それは error interface の nil ではなくて、*ErrNeedElse の nil という扱いになってしまいます。それが最終的に json.Unmarshal() の戻り値で返ってきたときに、型の不一致で正しく判定できなくなってしまうのです。

nil の不思議な挙動は例えば以下のエントリーで詳しく解説されています。

絶対ハマる、不思議なnil - Qiita

結論

  • 簡単な分岐のみが必要な場合はエラーを値として定義する
  • 再利用性のないコードであれば動的にエラー値を生成する
  • ある程度再利用性のあるコードは独自エラー型を定義して error interface として返すべし

Go で Web アプリを作る

思いつきでブログを初めて見る。 最近 Go 言語を使って Web アプリを作成していたので、それ周りで少し記事を書いてみようと思う。

Go

最近になって様々なプロダクトで使われるようになってきた Go。Web アプリを作るに当たっては以下のような利点があると思っている。

  • 生産性が高い
  • パフォーマンスが良い
  • 覚えるのが楽

Go はコンパイル型の言語であるが非常に生産性が高いと思う。よく「Go はコンパイル型の言語だから手軽にプログラミングするのに向かない」という人がいるけど、実際に手を付けてみると、予想以上に生産性が良いことを実感できる。言語自体最近できたものであるから、言語仕様はシンプルで洗練されている。標準ライブラリも充実していて、シンプルなアプリケーションを作るのならばわざわざサードパーティのライブラリを使わずとも済む。

Go は特に並行処理を意識して言語仕様が設計されているので、特に Web アプリのような同時に複数のリクエストをさばくアプリケーションの開発にとても向いている。標準ライブラリをはじめ、さまざまな仕組みが Goroutine と呼ばれる並列処理の機構の上に動作する。そのため、特段プログラミングする側が意識しなくても、勝手に並行処理をしてくれる仕組みだ。従来の多くの言語では並列処理をしようとすると、アプリケーション側が意識してプログラミングをしたり、特別なライブラリなどを導入することが多くあった。しかしながら、Go を使うと基本的に素直にプログラムを書いていれば、うまいこと並行処理を行ってくれる(もちろん諸々の罠があったりするので、完全に意識しないで済むわけではないのだが…)。

先にも触れたが、Go の言語仕様はシンプルで洗練されている。それ故、覚えるのがとても簡単である。単純が故にときたまもどかしくなることもあるほどである。しかしながら、そのシンプルさ故に最低限の勉強をしておけばコードを読めるし、誰が書いても比較的似たコードになりやすい。これはチーム開発などにおいても便利な特性だと思う。

Hello World

Go のプログラムを書く場合には GOPATH を構成し、その GOPATH の上にプログラムを配置する必要がある。今回はホームディレクトリ以下に GOPATH を配置してみた。

$ GOPATH=~/go
$ mkdir -p ${GOPATH}/src/github.com/yuichi1004/go-webapp

さて定番の Hello World プログラムを書いてみる。go には強力な net/http パッケージが標準で備わっているので、高度な Http サーバーであっても、このパッケージを使って書くことが出来る。今回はこのパッケージのサンプルプログラムを拝借して、簡単なプログラムを書いてみよう。

http - The Go Programming Language

最もシンプルなプログラムは以下のようになる。

package main

import (
        "io"
        "net/http"
        "log"
)

func HelloHandler(w http.ResponseWriter, req *http.Request) {
        io.WriteString(w, "hello, world!\n")
}

func main() {
        http.HandleFunc("/hello", HelloHandler)
        log.Fatal(http.ListenAndServe(":8000", nil))
}

これで完了。あとはコードを実行してみる。

$ go run main.go
$ curl localhost:8000/hello
hello, world!

想定通りのレスポンスが返ってきた。