[インデックス 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.Handler
とhttp.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サービスを導入しています。
キャッシュ戦略:
- キャッシュの読み込み:
uiHandler
関数が呼び出された際、まずpage
パラメータが0
(つまりフロントページ)であるかを確認します。フロントページの場合、memcache.Get(c, uiCacheKey)
を呼び出して、MemcacheからキャッシュされたHTMLコンテンツを取得しようとします。 - キャッシュヒット: キャッシュが存在し、エラーなく取得できた場合(
err == nil
)、そのキャッシュされたコンテンツ(t.Value
)を直接http.ResponseWriter
に書き込み、処理を終了します。これにより、データストアへのアクセスやテンプレートの再レンダリングがスキップされ、非常に高速な応答が可能になります。 - キャッシュミス: キャッシュが存在しない場合(
memcache.ErrCacheMiss
)や、その他のエラーが発生した場合は、通常通りデータストアからコミット情報などを取得し、テンプレートをレンダリングしてHTMLコンテンツを生成します。 - キャッシュへの書き込み: テンプレートのレンダリング結果は、直接
http.ResponseWriter
に書き込まれるのではなく、一度bytes.Buffer
に書き込まれます。フロントページの場合(page == 0
)、このbytes.Buffer
の内容がmemcache.Set
関数を使ってMemcacheに保存されます。キャッシュのキーはuiCacheKey
("build-ui")、有効期限はuiCacheExpiry
(10分) に設定されています。 - キャッシュの無効化:
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/memcache
とbytes
パッケージがインポートされています。uiCacheKey
とuiCacheExpiry
という定数が定義され、キャッシュのキーと有効期限(10分)が設定されています。uiHandler
関数内で、page == 0
の場合にMemcacheからの読み込みと書き込みのロジックが追加されています。- テンプレートの実行結果を一時的に保持するために
bytes.Buffer
が導入され、その内容がMemcacheに保存されるようになりました。
このアプローチにより、フロントページへのリクエストの大部分はMemcacheから直接提供されるようになり、バックエンドの負荷が大幅に軽減され、応答速度が向上します。
コアとなるコードの変更箇所
このコミットでは、主に以下の2つのファイルが変更されています。
-
misc/dashboard/app/build/handler.go
:appengine/memcache
パッケージがインポートされました。commitHandler
、tagHandler
、resultHandler
の各関数にdefer invalidateCache(c)
が追加されました。invalidateCache
という新しい関数が追加されました。この関数は、MemcacheからuiCacheKey
に対応するエントリを削除します。
-
misc/dashboard/app/build/ui.go
:appengine/memcache
とbytes
パッケージがインポートされました。uiCacheKey
とuiCacheExpiry
という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) // バッファの内容を最終的にレスポンスに書き込み
}
uiCacheKey
とuiCacheExpiry
は、キャッシュの管理に使用される定数です。uiHandler
の冒頭で、リクエストがフロントページ(page == 0
)である場合にMemcacheからキャッシュを試みます。キャッシュが見つかれば、その内容をクライアントに直接返し、処理を終了します。- キャッシュが見つからなかった場合、またはフロントページ以外のリクエストの場合、通常のデータ取得とテンプレートレンダリングが行われます。
- テンプレートのレンダリング結果は、
bytes.Buffer
に一時的に書き込まれます。これにより、レンダリングされたHTMLコンテンツをMemcacheに保存する前に捕捉できます。 - レンダリング後、再度
page == 0
であれば、bytes.Buffer
の内容がmemcache.Set
によってMemcacheに保存されます。これにより、次回のフロントページリクエスト時にキャッシュが利用できるようになります。 - 最後に、
bytes.Buffer
の内容がhttp.ResponseWriter
に書き込まれ、クライアントにレスポンスが送信されます。
関連リンク
- Google App Engine Documentation
- Google App Engine Memcache for Go
- Go言語
net/http
パッケージ - Go言語
html/template
パッケージ - Go言語
bytes
パッケージ - Go言語
defer
ステートメント
参考にした情報源リンク
- 特になし (上記「関連リンク」に公式ドキュメントへのリンクを含んでいます)