Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 10926] ファイルの概要

このコミットは、Go言語のダッシュボードアプリケーションにおいて、フロントページ(UI)のレンダリング結果をGoogle App EngineのMemcacheに保存することで、パフォーマンスを向上させることを目的としています。これにより、頻繁にアクセスされるページのリクエストに対する応答時間を短縮し、App Engineのデータストアへの負荷を軽減します。

コミット

dashboard: store front page in memcache

R=golang-dev, dsymonds
CC=golang-dev
https://golang.org/cl/5503056

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/494e52fe1c07938e5127ef24e458b1f2744ac518

元コミット内容

commit 494e52fe1c07938e5127ef24e458b1f2744ac518
Author: Andrew Gerrand <adg@golang.org>
Date:   Wed Dec 21 14:57:46 2011 +1100

    dashboard: store front page in memcache
    
    R=golang-dev, dsymonds
    CC=golang-dev
    https://golang.org/cl/5503056
---
 misc/dashboard/app/build/handler.go | 12 ++++++++++++\n misc/dashboard/app/build/ui.go      | 39 +++++++++++++++++++++++++++++++++++--
 2 files changed, 49 insertions(+), 2 deletions(-)\n\ndiff --git a/misc/dashboard/app/build/handler.go b/misc/dashboard/app/build/handler.go
index facfeea814..576d7cb132 100644
--- a/misc/dashboard/app/build/handler.go
+++ b/misc/dashboard/app/build/handler.go
@@ -7,6 +7,7 @@ package build
 import (
 	"appengine"
 	"appengine/datastore"
+	"appengine/memcache"\n 	"crypto/hmac"
 	"fmt"
 	"http"
 	"os"
@@ -58,6 +59,7 @@ func commitHandler(r *http.Request) (interface{}, os.Error) {
 	if err := com.Valid(); err != nil {
 		return nil, fmt.Errorf("validating Commit: %v", err)
 	}\n+\tdefer invalidateCache(c)\n \ttx := func(c appengine.Context) os.Error {
 		return addCommit(c, com)
 	}
@@ -131,6 +133,7 @@ func tagHandler(r *http.Request) (interface{}, os.Error) {
 		return nil, err
 	}
 	c := appengine.NewContext(r)
+\tdefer invalidateCache(c)\n 	_, err := datastore.Put(c, t.Key(c), t)
 	return nil, err
 }
@@ -226,6 +229,7 @@ func resultHandler(r *http.Request) (interface{}, os.Error) {
 	if err := res.Valid(); err != nil {
 		return nil, fmt.Errorf("validating Result: %v", err)
 	}\n+\tdefer invalidateCache(c)\n 	// store the Log text if supplied
 	if len(res.Log) > 0 {
 		hash, err := PutLog(c, res.Log)
@@ -375,3 +379,11 @@ func logErr(w http.ResponseWriter, r *http.Request, err os.Error) {
 	w.WriteHeader(http.StatusInternalServerError)
 	fmt.Fprint(w, "Error: ", err)
 }\n+\n+// invalidateCache deletes the ui cache record from memcache.\n+func invalidateCache(c appengine.Context) {\n+\terr := memcache.Delete(c, uiCacheKey)\n+\tif err != nil && err != memcache.ErrCacheMiss {\n+\t\tc.Errorf("memcache.Delete(%q): %v", uiCacheKey, err)\n+\t}\n+}\ndiff --git a/misc/dashboard/app/build/ui.go b/misc/dashboard/app/build/ui.go
index 5070400d96..8a1cca320d 100644
--- a/misc/dashboard/app/build/ui.go
+++ b/misc/dashboard/app/build/ui.go
@@ -10,6 +10,8 @@ package build
 import (
 	"appengine"
 	"appengine/datastore"
+\t"appengine/memcache"\n+\t"bytes"\n 	"exp/template/html"
 	"http"
 	"os"
 	"strconv"
@@ -20,6 +22,11 @@ import (
 	"template"
 )
 
+const (\n+\tuiCacheKey    = "build-ui"\n+\tuiCacheExpiry = 10 * 60 // 10 minutes in seconds\n+)\n+\n func init() {
 \thttp.HandleFunc("/", uiHandler)
 \thtml.Escape(uiTemplate)
@@ -27,7 +34,6 @@ func init() {
 
 // uiHandler draws the build status page.
 func uiHandler(w http.ResponseWriter, r *http.Request) {
-\t// TODO(adg): put the HTML in memcache and invalidate on updates\n \tc := appengine.NewContext(r)
 
 	page, _ := strconv.Atoi(r.FormValue("page"))
 	if page == 0 {
@@ -35,6 +41,18 @@ func uiHandler(w http.ResponseWriter, r *http.Request) {
 		page = 0
 	}
 
+\t// Used cached version of front page, if available.\n+\tif page == 0 {\n+\t\tt, err := memcache.Get(c, uiCacheKey)\n+\t\tif err == nil {\n+\t\t\tw.Write(t.Value)\n+\t\t\treturn\n+\t\t}\n+\t\tif err != memcache.ErrCacheMiss {\n+\t\t\tc.Errorf("get ui cache: %v", err)\n+\t\t}\n+\t}\n+\n \tcommits, err := goCommits(c, page)
 \tif err != nil {
 \t\tlogErr(w, r, err)
 \t}
@@ -57,9 +75,26 @@ func uiHandler(w http.ResponseWriter, r *http.Request) {
 	\tp.HasPrev = true
 	}
 	data := &uiTemplateData{commits, builders, tipState, p}
-\tif err := uiTemplate.Execute(w, data); err != nil {\n+\n+\tvar buf bytes.Buffer\n+\tif err := uiTemplate.Execute(&buf, data); err != nil {\n \t\tlogErr(w, r, err)
+\t\treturn\n+\t}\n+\n+\t// Cache the front page.\n+\tif page == 0 {\n+\t\tt := &memcache.Item{\n+\t\t\tKey:        uiCacheKey,\n+\t\t\tValue:      buf.Bytes(),\n+\t\t\tExpiration: uiCacheExpiry,\n+\t\t}\n+\t\tif err := memcache.Set(c, t); err != nil {\n+\t\t\tc.Errorf("set ui cache: %v", err)\n+\t\t}\n \t}\n+\n+\tbuf.WriteTo(w)\n }\n \n type Pagination struct {

変更の背景

Go言語のダッシュボードは、Goプロジェクトのビルドステータスやコミット履歴を表示するウェブアプリケーションです。このアプリケーションはGoogle App Engine上で動作しており、データストアから情報を取得してHTMLページを動的に生成しています。

フロントページはユーザーが最も頻繁にアクセスするページの一つであり、その表示にはデータストアへの問い合わせやテンプレートのレンダリングといった処理が伴います。アクセスが増えるにつれて、これらの処理がサーバーの負荷となり、応答時間の遅延やApp Engineのリソース消費の増加につながる可能性があります。

このコミットの背景には、このようなパフォーマンス上の課題がありました。特に、フロントページの内容は頻繁に更新されるわけではないため、一度生成した内容をキャッシュすることで、その後のリクエストに対しては高速にサービスを提供できるという判断がありました。これにより、ユーザーエクスペリエンスの向上と、App Engineのリソース効率化が期待されます。

前提知識の解説

Google App Engine (GAE)

Google App Engineは、Googleが提供するPaaS(Platform as a Service)であり、ウェブアプリケーションやモバイルバックエンドを構築・ホストするためのプラットフォームです。開発者はインフラの管理を気にすることなく、アプリケーションのコードに集中できます。GAEは、スケーラビリティ、信頼性、セキュリティを自動的に提供します。

Google App EngineのMemcacheサービス

Memcacheは、Google App Engineが提供する分散型インメモリキャッシュサービスです。アプリケーションが頻繁にアクセスするデータを一時的に保存することで、データストアや他の永続ストレージへのアクセス回数を減らし、アプリケーションの応答速度を向上させることができます。Memcacheはキーと値のペアを保存し、高速な読み書きが可能です。データは揮発性であり、キャッシュの有効期限が切れたり、メモリが不足したりすると自動的に削除されます。

Go言語のappengineパッケージ

Go言語でGoogle App Engineアプリケーションを開発する際に使用される標準ライブラリです。このパッケージは、App Engineの各種サービス(データストア、Memcache、URLフェッチなど)へのアクセスを提供します。

  • appengine.Context: App Engineの各リクエストに関連付けられたコンテキストオブジェクトです。ログ記録、データストア操作、Memcache操作など、App Engineのサービスを利用する際にはこのコンテキストが必要です。
  • appengine/memcache: Memcacheサービスとやり取りするための機能を提供します。Get関数でキャッシュからデータを取得し、Set関数でデータをキャッシュに保存します。Delete関数でキャッシュからデータを削除します。
  • appengine/datastore: App EngineのNoSQLデータストアとやり取りするための機能を提供します。

Go言語のhttp.Handlerhttp.ResponseWriter

Go言語の標準ライブラリnet/httpパッケージは、HTTPサーバーとクライアントを構築するための強力な機能を提供します。

  • http.Handler: HTTPリクエストを処理するためのインターフェースです。ServeHTTP(w http.ResponseWriter, r *http.Request)メソッドを実装することで、HTTPリクエストを処理するロジックを定義します。
  • http.ResponseWriter: HTTPレスポンスをクライアントに書き込むためのインターフェースです。Writeメソッドでレスポンスボディを書き込み、WriteHeaderメソッドでHTTPステータスコードを設定します。

Go言語のdeferステートメント

deferステートメントは、関数がリターンする直前に実行される関数呼び出しをスケジュールします。これは、リソースのクリーンアップ(ファイルのクローズ、ロックの解放など)を確実に行うためによく使用されます。このコミットでは、キャッシュの無効化処理を確実に行うためにdeferが使用されています。

Go言語のbytes.Buffer

bytes.Bufferは、可変長のバイトシーケンスを扱うためのバッファです。io.Writerインターフェースを実装しているため、html/templateのようなテンプレートエンジンからの出力を直接受け取ることができます。このコミットでは、テンプレートのレンダリング結果を直接HTTPレスポンスライターに書き込むのではなく、一度bytes.Bufferに書き込んでからMemcacheに保存し、その後HTTPレスポンスライターに書き出すために使用されています。

Go言語のhtml/templateパッケージ

html/templateパッケージは、HTML出力の生成に使用されるテンプレートエンジンです。クロスサイトスクリプティング(XSS)攻撃を防ぐために、自動的にエスケープ処理を行います。

技術的詳細

このコミットは、Goダッシュボードのフロントページ表示におけるパフォーマンスボトルネックを解消するために、Google App EngineのMemcacheサービスを導入しています。

キャッシュ戦略:

  1. キャッシュの読み込み: uiHandler関数が呼び出された際、まずpageパラメータが0(つまりフロントページ)であるかを確認します。フロントページの場合、memcache.Get(c, uiCacheKey)を呼び出して、MemcacheからキャッシュされたHTMLコンテンツを取得しようとします。
  2. キャッシュヒット: キャッシュが存在し、エラーなく取得できた場合(err == nil)、そのキャッシュされたコンテンツ(t.Value)を直接http.ResponseWriterに書き込み、処理を終了します。これにより、データストアへのアクセスやテンプレートの再レンダリングがスキップされ、非常に高速な応答が可能になります。
  3. キャッシュミス: キャッシュが存在しない場合(memcache.ErrCacheMiss)や、その他のエラーが発生した場合は、通常通りデータストアからコミット情報などを取得し、テンプレートをレンダリングしてHTMLコンテンツを生成します。
  4. キャッシュへの書き込み: テンプレートのレンダリング結果は、直接http.ResponseWriterに書き込まれるのではなく、一度bytes.Bufferに書き込まれます。フロントページの場合(page == 0)、このbytes.Bufferの内容がmemcache.Set関数を使ってMemcacheに保存されます。キャッシュのキーはuiCacheKey ("build-ui")、有効期限はuiCacheExpiry (10分) に設定されています。
  5. キャッシュの無効化: commitHandler, tagHandler, resultHandlerといった、ダッシュボードのデータ(コミット、タグ、ビルド結果)を更新するハンドラ関数にdefer invalidateCache(c)が追加されています。これにより、データが更新されるたびに、フロントページのキャッシュがMemcacheから削除されます。これは、古い情報がユーザーに表示されるのを防ぐための重要なメカニズムです。invalidateCache関数はmemcache.Deleteを呼び出し、エラーが発生してもmemcache.ErrCacheMiss(キャッシュが存在しない場合のエラー)であれば無視します。

実装の詳細:

  • misc/dashboard/app/build/handler.goには、invalidateCache関数が追加され、データ更新系のハンドラ(commitHandler, tagHandler, resultHandler)にdefer invalidateCache(c)が挿入されています。これにより、データが変更された際に確実にキャッシュが無効化されます。
  • misc/dashboard/app/build/ui.goには、appengine/memcachebytesパッケージがインポートされています。
  • uiCacheKeyuiCacheExpiryという定数が定義され、キャッシュのキーと有効期限(10分)が設定されています。
  • uiHandler関数内で、page == 0の場合にMemcacheからの読み込みと書き込みのロジックが追加されています。
  • テンプレートの実行結果を一時的に保持するためにbytes.Bufferが導入され、その内容がMemcacheに保存されるようになりました。

このアプローチにより、フロントページへのリクエストの大部分はMemcacheから直接提供されるようになり、バックエンドの負荷が大幅に軽減され、応答速度が向上します。

コアとなるコードの変更箇所

このコミットでは、主に以下の2つのファイルが変更されています。

  1. misc/dashboard/app/build/handler.go:

    • appengine/memcacheパッケージがインポートされました。
    • commitHandlertagHandlerresultHandlerの各関数にdefer invalidateCache(c)が追加されました。
    • invalidateCacheという新しい関数が追加されました。この関数は、MemcacheからuiCacheKeyに対応するエントリを削除します。
  2. misc/dashboard/app/build/ui.go:

    • appengine/memcachebytesパッケージがインポートされました。
    • uiCacheKeyuiCacheExpiryという2つの定数が追加されました。uiCacheKeyはキャッシュのキーとして使用される文字列で、uiCacheExpiryはキャッシュの有効期限(10分)を秒単位で定義します。
    • uiHandler関数内で、page == 0(フロントページ)の場合にMemcacheからキャッシュされたコンテンツを読み込むロジックが追加されました。キャッシュが存在すればそれを返し、なければ通常のレンダリングに進みます。
    • テンプレートのレンダリング結果を直接http.ResponseWriterに書き込む代わりに、bytes.Bufferに書き込むように変更されました。
    • page == 0の場合、bytes.Bufferに書き込まれた内容をMemcacheに保存するロジックが追加されました。

コアとなるコードの解説

misc/dashboard/app/build/handler.go

import (
	"appengine"
	"appengine/datastore"
	"appengine/memcache" // 追加
	"crypto/hmac"
	"fmt"
	"http"
	"os"
)

// ... 既存のコード ...

func commitHandler(r *http.Request) (interface{}, os.Error) {
	// ... 既存のコード ...
	if err := com.Valid(); err != nil {
		return nil, fmt.Errorf("validating Commit: %v", err)
	}
	defer invalidateCache(c) // 追加: コミット追加後にキャッシュを無効化
	tx := func(c appengine.Context) os.Error {
		return addCommit(c, com)
	}
	// ... 既存のコード ...
}

func tagHandler(r *http.Request) (interface{}, os.Error) {
	// ... 既存のコード ...
	c := appengine.NewContext(r)
	defer invalidateCache(c) // 追加: タグ追加後にキャッシュを無効化
	_, err := datastore.Put(c, t.Key(c), t)
	return nil, err
}

func resultHandler(r *http.Request) (interface{}, os.Error) {
	// ... 既存のコード ...
	if err := res.Valid(); err != nil {
		return nil, fmt.Errorf("validating Result: %v", err)
	}
	defer invalidateCache(c) // 追加: ビルド結果追加後にキャッシュを無効化
	// ... 既存のコード ...
}

// invalidateCache deletes the ui cache record from memcache.
func invalidateCache(c appengine.Context) {
	err := memcache.Delete(c, uiCacheKey) // uiCacheKeyはui.goで定義
	if err != nil && err != memcache.ErrCacheMiss {
		c.Errorf("memcache.Delete(%q): %v", uiCacheKey, err)
	}
}
  • invalidateCache関数は、memcache.Deleteを呼び出して、uiCacheKeyで指定されたキャッシュエントリをMemcacheから削除します。
  • memcache.ErrCacheMissは、指定されたキーのキャッシュが存在しない場合に返されるエラーです。このエラーは、キャッシュを削除しようとしたが元々存在しなかったという正常なケースであるため、ログには出力されません。それ以外のエラーはログに出力されます。
  • defer invalidateCache(c)が各データ更新ハンドラに追加されたことで、これらの関数が正常終了するかエラーで終了するかにかかわらず、必ずキャッシュ無効化処理が実行されるようになります。これにより、データが更新された直後に古いキャッシュが提供されることを防ぎます。

misc/dashboard/app/build/ui.go

import (
	"appengine"
	"appengine/datastore"
	"appengine/memcache" // 追加
	"bytes"              // 追加
	"exp/template/html"
	"http"
	"os"
	"strconv"
	"template"
)

const (
	uiCacheKey    = "build-ui"        // フロントページのキャッシュキー
	uiCacheExpiry = 10 * 60 // 10 minutes in seconds // キャッシュの有効期限 (10分)
)

// ... 既存のコード ...

func uiHandler(w http.ResponseWriter, r *http.Request) {
	c := appengine.NewContext(r)

	page, _ := strconv.Atoi(r.FormValue("page"))
	if page == 0 {
		page = 0
	}

	// Used cached version of front page, if available.
	if page == 0 { // フロントページの場合のみキャッシュを試みる
		t, err := memcache.Get(c, uiCacheKey) // Memcacheからキャッシュを取得
		if err == nil { // キャッシュが存在する場合
			w.Write(t.Value) // キャッシュされた内容を直接レスポンスに書き込み
			return           // 処理を終了
		}
		if err != memcache.ErrCacheMiss { // キャッシュミス以外のエラーはログに出力
			c.Errorf("get ui cache: %v", err)
		}
	}

	// ... 既存のコミットとビルダーの取得ロジック ...

	data := &uiTemplateData{commits, builders, tipState, p}

	var buf bytes.Buffer // テンプレートのレンダリング結果を一時的に保持するバッファ
	if err := uiTemplate.Execute(&buf, data); err != nil { // バッファにレンダリング
		logErr(w, r, err)
		return
	}

	// Cache the front page.
	if page == 0 { // フロントページの場合のみキャッシュに保存
		t := &memcache.Item{
			Key:        uiCacheKey,
			Value:      buf.Bytes(), // バッファの内容をキャッシュ
			Expiration: uiCacheExpiry, // 有効期限を設定
		}
		if err := memcache.Set(c, t); err != nil { // Memcacheに保存
			c.Errorf("set ui cache: %v", err)
		}
	}

	buf.WriteTo(w) // バッファの内容を最終的にレスポンスに書き込み
}
  • uiCacheKeyuiCacheExpiryは、キャッシュの管理に使用される定数です。
  • uiHandlerの冒頭で、リクエストがフロントページ(page == 0)である場合にMemcacheからキャッシュを試みます。キャッシュが見つかれば、その内容をクライアントに直接返し、処理を終了します。
  • キャッシュが見つからなかった場合、またはフロントページ以外のリクエストの場合、通常のデータ取得とテンプレートレンダリングが行われます。
  • テンプレートのレンダリング結果は、bytes.Bufferに一時的に書き込まれます。これにより、レンダリングされたHTMLコンテンツをMemcacheに保存する前に捕捉できます。
  • レンダリング後、再度page == 0であれば、bytes.Bufferの内容がmemcache.SetによってMemcacheに保存されます。これにより、次回のフロントページリクエスト時にキャッシュが利用できるようになります。
  • 最後に、bytes.Bufferの内容がhttp.ResponseWriterに書き込まれ、クライアントにレスポンスが送信されます。

関連リンク

参考にした情報源リンク

  • 特になし (上記「関連リンク」に公式ドキュメントへのリンクを含んでいます)