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

Yuichi Murata's Engineering Blog

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

Google Spanner のアーキテクチャを知る

Cloud Spanner

最近 Cloud Spanner のベータ公開によって話題の Spanner。 気になっていたので論文を読んだり勉強会などで情報収集していました。日本語のリソースもそこまで多くないので、調べてわかったことを纏めておきます。

簡単にまとめると特徴は以下のとおりです。

以下で、細かい説明を続けていきます。

続きを読む

タスクキューのリクエストのログを分ける

小ネタ

StackDriver Logging でログを見るときや、ログメトリクスを定義する時にタスクキューのリクエストを別途扱いたい時がある。たとえば、通常のユーザーリクエストはレスポンスタイムの遅延を許容したくないが、タスクキューの場合は問題ないなどのケースである。

一番思いつきやすいのが、タスクキューのパスをログフィルターで除外することだが、もっと簡単な方法があった。以下でタスクキューを除いたログを閲覧したり、ログメトリクスを定義できる。

resource.type = gae_app AND resource.labels.module_id = "xxxx" AND logName = "projects/xxxx/logs/appengine.googleapis.com%2Frequest_log" AND NOT protoPayload.taskName:*

Task Queue の処理には必ず protoPayload.taskName が入る。フィールドがあるかないかのチェックは protoPayload.taskName:* のようにすれば良い。

フィールド内の特定の値をテストせずにフィールドの有無をテストするには、:* 比較を使用します。 Advanced Logs Filters  |  Stackdriver Logging  |  Google Cloud Platform

つまりこれで OK。

App Engine は DoS 攻撃を勝手に防いでくれるのか

App Engine には DoS プロテクションサービスなるものがあります。なにやらいい感じに DoS 攻撃を防いでくれそうな気がします。App Engine は勝手に DoS 攻撃を防いでくれるのでしょうか。

Configuring DoS Protection Service for Go  |  App Engine standard environment for Go  |  Google Cloud Platform

答えは Yes であり No でもあります。 端的にいうと Layer4 以下の DoS 攻撃は防いでくれるが、正規の HTTP 通信を大量に送りつけてくれるような類の攻撃には手動で dos.yaml を書く必要があります。

続きを読む

App Engine で謎の Untraced Time が発生するときは

App Engine を利用していると、時たま何かに引っかかったように処理が詰まることがあります。こんな時にはもちろん StackDirver Trace を使ってボトルネックを探ったりするわけです。 しかしながら時に、何の RPC を呼び出しているわけでもないのに、何故か処理の開始が遅い場合や、レスポンスを返すのが遅いということがあります。

よくありがちな理由が、リクエストが殺到して新規インスタンスがスピンアップするときに処理が遅れることです。私の周りでは良く「スピンアップに引っかかる」と言っています。しかし、具体的に何が起こっているのか、本当にスピンアップが起因しているのかは曖昧のままでした。そのため、今回、真面目に調べてみることにしました。

続きを読む

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 として返すべし