ラベル Go の投稿を表示しています。 すべての投稿を表示
ラベル Go の投稿を表示しています。 すべての投稿を表示

2024/02/09

GAEのインスタンス起動時間 #2

動機

Java17 + レガシーバンドルサービスを実装したところ、GAEインスタンスのスピンアップが若干遅いと感じました。いつの間にかJava21もプレビューになっていましたので、あわせてインスタンス起動時間を再測定してみました。

また前回(GAEのインスタンス起動時間)はログで確認していたのですが、その方法の精度にも疑問を感じたので、測定方法も対象も変更しています。


今回はJava8がすでにサポート終了しているので、対象外としました。


測定条件・方法

  • リージョン : 大阪
  • ネットワーク : NTT Flets光 隼、実効300Mbps程度
  • インスタンスクラスはF1。
  • ServletのアプリはGoogleのサンプルに入っていたHelloWorld。
  • SpringBootのアプリはGoogleのサンプルに入っていたHelloWorldそのままなので、バージョンは2.7.18。
  • レガシーバンドルサービスは、Googleのサンプルは余計なコードが入っていたのでpom.xmlのみ参考にし、Hello World相当を自作。
  • GoもHello World相当を自作、今回はフレームワークなしのパターンのみ。
  • ブラウザからAjaxでリクエストし、JavaScriptでレスポンスが返るまでの時間を測定。
  • スピンアップあり/なしの2パターンで、それぞれ10回測定し、平均値を算出。
  • 前記2パターンの差から、スピンアップ+VM起動+フレームワーク起動の時間を計算。

測定結果

単位はすべてmsです。測定結果をtableにすると横長になってレイアウトが崩れたので、画像です。
まずはスピンアップあり。

続いてスピンアップなし。

そして、スピンアップ+VM起動+フレームワーク起動の時間。
Servletspringレガシー
Java11625.24728.62098.6
Java17599.84769.21655.0
Java21630.24622.92451.5
Go1.20176.9--


所感

起動時間は 17<11≒21。わずかとはいえ、最新の21で悪化しているという意外な結果に。まだプレビューなのも影響しているのでしょうか。

SpringBoot

前回と比べてバージョンが2.6.6→2.7.18と変化していますが、結果はほぼ同じ。他はJava21で遅いのに、なぜかspringだけはJava21が最速と、一致しません。なぜ...

レガシーバンドルサービス

遅い気がしたのは気のせいではありませんでした。Java8のスピンアップが遅かったのはもしかして、Googleのカスタマイズのせいだったのではという疑惑が生じます。そしてJava21での遅延が目立って大きくなっています。残念過ぎる...

2023/10/18

CORSに対応する [Go]

背景

 HTML/JavaScript/TypeScriptのダウンロード元と Ajaxでのアクセス先が異なる場合、Cross Resource Origin Sharing(以下CORSと略)ポリシーに従う必要があります。AjaxやCORSについての詳細はいろいろなところで説明されているので、ここでは割愛します。

 Go+GinでのCORS対応の実装についてです。


前提

 使用するソフトウェアのバージョンは以下のとおりです。

  • Go : 1.20
  • Gin : 1.9.0


 また、すべてのアクセス先に対してCORSアクセスを許可する設定であるAccess-Control-Allow-Originヘッダへの"*"の設定ではなく、特定のアクセス先に対してのみ許可するものとします。


gin-contrib/cors

 最初に見つけたのが「gin-contrib/cors」です。Ginの公式なCORSのパッケージのようです。ドキュメントはこちら。スニペットを見る限り使い方も簡単で、Ginのミドルウェアとして設定するだけです。
 しかし「Canonical example」のスニペットを参考に実装してみたのですが、まったくCORSポリシーを解決できませんでした。OPTIONSリクエストに対して、レスポンスヘッダにはAccess-Control-Allow-Originヘッダが付いていません。しばらく設定を変えたりして試してみたのですが、全く解決できませんでした。

 この状況をググってみて見つかったのがStackOverflowの記事「Go gin framework CORS」です。質問者はGoの標準ライブラリのみを使用していますが、OPTIONSに続くアクセスが発生せず、期待動作しないと言っています。
 回答を見ると複数パターンあるようです。
  • Goの標準ライブラリのみで実装の修正。質問者はこれで解決したもよう。
  • gin-contrib/corsの使用を勧めるもの。これで動いたといっているのは1名のみ。
  • rs/corsを勧めるもの。
  • 独自ライブラリを勧めるもの。

 自分の使っているGo/Ginのバージョンに比べて、gin-contrib/corsの最新リリースは2022/07/05なので幾分か古く、そのためGo/Ginの更新について行っていない可能性があります。Go/Ginのバージョンダウンまでして確認するほどgin-contrib/corsにこだわることもないので、このパッケージは諦めました。

標準ライブラリでの実装

 変なライブラリを入れたくないので、結局標準ライブラリのみで実装することにしました。実際にCORSを解決できた最小限の実装が以下です。Ginのミドルウェアにしています。


func main() {
    router := gin.Default()
    router.Use(corsSetting)
    // リクエストハンドラを設定 router.GET(...) etc.
}

func corsSetting(ginctx *gin.Context) {
    // リクエスト元の確認
    requestheader := ginctx.Request.Header
    requestorigin := requestheader.Get("origin")
    if リクエストの確認 {
        // レスポンスヘッダの設定
        responseheader := ginctx.Writer.Header()
        responseheader.Set("Access-Control-Allow-Origin", requestorigin)
        responseheader.Set("Access-Control-Allow-Headers", "Content-Type")
    }
}


 OPTIONSに続くアクセスのために、Access-Control-Allow-Headersレスポンスヘッダに"Content-Type"を設定しています。


2023/05/07

GCSからHTMLテンプレートを読み込んでHTTPレスポンスで返す [Go]

 ほぼ自分用の忘備録です。


背景

 Go+Gin構成のwebアプリでは、ファイルとして用意したテンプレートをHTTPレスポンスで返すのは簡単です。以下のようなスニペットになります。


func main() {
    router := gin.Default()
    router.LoadHTMLFiles("HTMLテンプレートファイルのパス")
    router.GET("リクエストエンドポイントのuri", handler)
}

func handler(ginctx *gin.Context) {
    ginctx.HTML(http.StatusOK, "HTMLテンプレートファイル", gin.H{
        キー: バリュー
    })
}


課題

 HTMLテンプレートをGoogle Cloud Storage(以下GCSと略)に置いておき、そこから読み込んで返したい場合、直接GCSから取得してくれる関数はGinにはありません。


解決

 以下のスニペットで可能になりました。エラー処理のコードは省略しています。これでいいのかちょっと不安な箇所もありますが、とりあえず期待動作はしています。


func main() {
    router := gin.Default()
    router.GET("リクエストエンドポイントのuri", handler)
}

func handler(ginctx *gin.Context) {
    // GCSからHTMLテンプレートをロード
    ctx := context.Background()
    storageclient, _ := storage.NewClient(ctx)
    reader, _ := storageclient.Bucket("バケット名")
    	.Object("HTMLテンプレートのオブジェクト名").NewReader(ctx)
    content_bin, _ := io.ReadAll(reader)
    reader.Close()
    
    // GCSから読み出した結果はバイナリなので文字列に変換
    content_str, _ := *(*string)(unsafe.Pointer(&content_bin)
    
    // テンプレートとして登録
    router := gin.Default()
    template, _ := template.New("").Delims("{{", "}}").Funcs(router.FuncMap).Parse(content_str)
    router.SetHTMLTemplate(template)

    // レスポンスで返す
    html := router.HTMLRender.Instance("", gin.H{
        キー: バリュー
    })
    ginctx.Render(http.StatusOK, html)
}

 バイナリ→文字列の変換には「忘れがちなGoでbyteをstringに変換する方法をベンチマークしてみた。」を参考にしました。

2022/07/06

GAEのインスタンス起動時間

動機

GAEのスタンダード環境では、言語によってインスタンスの起動時間に差があるのは、Java8を含む第1世代VMを利用している人には、良く知られた事実です。既に起動しているインスタンスがあっても、リクエストが増えて新たにインスタンスが起動される場合にも、この起動時間がかかります。そのためインスタンス起動が遅いと、時々レスポンスが悪く感じることになります。

昨年公式に公開されたJava17がいつの間にかGAEでもプレビュー版として使えるようになっていましたので、バージョンアップで起動時間がどう変化したか確かめてみたくなりました。それにSpringの起動時間も気になります。で、言語とそのバージョン + Webフレームワークの違いで、実際のインスタンス起動時間を計ってみました。


最初に断っておきますが、私が使用する(可能性のある)組み合わせのみの測定です。


測定条件・方法

  • 基本的にアプリはGoogleのサンプルに入っていたHelloWorld、それに最低限のログを加えたもの。
  • ただしJava8/Goは手元にあった単純なアプリ。Slim3やObjectifyなどは入っていないので、結果には影響しないはず。
  • SpringBootはGoogleのサンプルに入っていたHelloWorldそのままなので、バージョンは2.6.6。
  • GoのフレームワークはGin。
  • インスタンスクラスはF1。
  • インスタンスが立ち上がっていないことをGCPコンソールで確認してから、ブラウザからリクエスト開始。
  • Cloud Loggingに記録されたログにより、リクエストの到着から、リクエストハンドラの先頭に入れたログが出るまでの時間を計算。
  • それぞれ5回測定し、平均値を算出。
  • 1回測定ごとにGCPコンソールから手動でインスタンスを停止。次の実行まで数分間放置。

測定結果

フレームワーク
なしあり
言語Java82.063-
Java111.0553.737
Java170.9393.551
Go 1.160.1760.205

単位はms。


所感

Java8

過去の経験から3秒くらいと思っていたのですが、なんか早いという結果になりました。意外でしたが、ちょっと嬉しい。

Java11

VMの起動が早いのでしょうか。フレームワークなしならVM起動してても、ほとんど待たされている感覚はありません。PHP/Pythonと同じくらいの起動時間だと思われます。


Java17

まだプレビュー版であることはご理解ください。しかしJava11より少し早くなっているのはいいですね。


SpringBoot

JavaVMに1秒+Springに3秒弱という結果でした。ブラウザを触っていても「あれ、遅いかな」と感じたころに応答が描画される感じです。これだけで十分「使えない」と判断できます。
起動時間の4秒というのは、GAEのJavaというと8しかなかった時代に、起動が遅いといわれていた状況よりも悪くなっています。Springはサーバレスという考えのなかった時代の、「起動しておいて口開けて待っている」ことが前提の時代の産物だけに、時代遅れの遺物になってしまった感があります。
サーバレスでやりたいなら、潔くSpringは捨てましょう。


Go

インタプリタもVMも存在しない、ネイティブ実行されるだけあって、爆速です。ブラウザを触っていても、インスタンス起動が起きていることを全く感じることがありません。


Gin

「フレームワークというよりライブラリ」と言われるだけあって、遅延は30ms程度と、ほとんど影響がありません。SpringBootと対照的な結果ですが、時代が違うのでサーバレスも考慮された結果なのかもしれません。


(2022/07/14追記)

現在、GAEのJava17はプレビューから一般提供に格上げされています。

2022/05/21

GoでGCPプロジェクト管理者を識別する

動機

GCPで動作するアプリケーションを作っていると、アプリケーションの管理者=プロジェクトの管理者 であることが多いと思います。開発フェーズか運用フェーズかでそこに割り当てられている人は変わったとしても、この関係は変わりないのが理想と思います。

であれば、プログラムでプロジェクト管理者のアカウントを判断できれば、「アプリケーション管理者のアカウント」をアプリケーションで保持する必要ながなくなって便利です。


プロジェクト管理者のアカウントをコードで取得する手段を探してみたところ、Goで発見しましたので紹介します。


コード

サービスの名称と機能からIdentity and Access Managementあたりかと あたりをつけて探し始めて、たどり着いたのはResource Managerでした。プロジェクト管理者のアカウントを取得するには、以下の様に複数のAPIを経由します。

import ( "context" "os" "strings" "google.golang.org/api/cloudresourcemanager/v1" ) ... // context.Contextを取得 context := context.Background() // Cloud Resource Managerサービスを取得 crmservice, err := cloudresourcemanager.NewService(context) // (1)Resource Managerサービスを取得 if err != nil { // エラー処理 return } // プロジェクトのIAMポリシーを取得 projid := os.Getenv("GOOGLE_CLOUD_PROJECT") // (2)プロジェクトIDを取得 request := new(cloudresourcemanager.GetIamPolicyRequest) iampolicy, err := crmservice.Projects.GetIamPolicy(projid, request).Do() // (3)IAMポリシーを取得 if err != nil { // エラー処理 return } // このプロジェクトの管理者に含まれているか確認 adminuser := false check_end: for _, binding := range iampolicy.Bindings { // (4)各ポリシー for _, member := range binding.Members { // (5)各メンバ if strings.Contains(member, email) { // (6)Eメールアドレスで判断 adminuser = true break check_end } } } // adminuser == tureならプロジェクト管理者

  1.  Cloud Resource Managerサービスを取得します。
  2.  環境変数GOOGLE_CLOUD_PROJECTからプロジェクトIDを取得しています。Cloud Run/Goではこれで取得できましたが、その他のサービスでは同様に取得できるか分かりませんので、確認してください。
  3. プロジェクトのIdentity and Access Managerポリシーを取得します。メソッドチェインの最後のDo()メソッドが実行結果としてPolicyを返してくれます。
  4. Bindingsプロパティがポリシーにバインドされているプリンシパルの配列です。
  5. 各プリンシパルは複数のメンバーを含んでいます。このメンバーには個人アカウントだけではなく、サービスアカウントなども含まれます。
  6. Eメールアドレスでプロジェクト管理者かどうかを判断しています。個人アカウントの場合は先頭に"user:"が付いているので、Eメールアドレスを含んでいるか否かで判断しています。


ライブラリ

今回使ったライブラリのパッケージは以下。

一方でGoogle公式サイトには以下も載っています。

cloud.google.com/go/resourcemanager/apiv2

後者のライブラリは実装されている機能が少なすぎて、今回のように実装できません。というかAPI仕様を見る限り、ほとんど使い物にならないのではないかと思います。

GoのResource Managerに限らず、どのライブラリを使えばいいのか、こんなの見つけたけど使って大丈夫なのかと、迷うことがよくありますが、今回は以上の実装で目的を果たせました。また前者のライブラリは機能も十分そろっていそうなので、その他の用途にも使えると思います。