The Go Blog
ゼロからGoへ: 24時間でGoogleのホームページに公開
はじめに
この記事は、Googleの検索チームのソフトウェアエンジニアであるライナルド・アギアが執筆しました。彼は、初めてのGoプログラムを開発し、数百万人のユーザーに向けて公開するまでの経験を、わずか1日で達成したことを共有しています!
最近、私はささやかではあるものの、非常に注目度の高い「20%プロジェクト」に参加する機会を得ました。それは、2011年の感謝祭Google Doodleです。このDoodleは、頭、翼、羽根、脚の異なるスタイルをランダムに組み合わせて生成される七面鳥を特徴としています。ユーザーは、七面鳥の異なる部分をクリックしてカスタマイズできます。このインタラクティブ性は、JavaScript、CSS、そしてもちろんHTMLの組み合わせによってブラウザで実装されており、七面鳥をその場で作成します。
ユーザーがパーソナライズされた七面鳥を作成すると、Google+に投稿することで友人や家族と共有できます。「共有」ボタン(ここには表示されていません)をクリックすると、ユーザーのGoogle+ストリームに七面鳥のスナップショットを含む投稿が作成されます。このスナップショットは、ユーザーが作成した七面鳥と一致する単一の画像です。
七面鳥の8つのパーツ(頭、脚のペア、異なる羽根など)それぞれに13の選択肢があるため、生成可能なスナップショット画像は8億通り以上にもなります。これらすべてを事前に計算することは明らかに不可能です。代わりに、スナップショットはその場で生成する必要があります。この問題と、即座のスケーラビリティと高い可用性の必要性を組み合わせると、プラットフォームの選択は明らかです。Google App Engineです!
次に決定する必要があったのは、どのApp Engineランタイムを使用するかでした。画像操作タスクはCPUバウンドであるため、この場合はパフォーマンスが決定要因となります。
情報に基づいた意思決定を行うために、テストを実行しました。新しいPython 2.7ランタイム(Cベースの画像ライブラリであるPILを提供)とGoランタイム用に、いくつかの同等のデモアプリを素早く準備しました。各アプリは、いくつかの小さな画像で構成される画像を生成し、その画像をJPEGとしてエンコードし、JPEGデータをHTTPレスポンスとして送信します。Python 2.7アプリは中央値で65ミリ秒のレイテンシでリクエストを処理しましたが、Goアプリはわずか32ミリ秒の中央値レイテンシで実行されました。
したがって、この問題は実験的なGoランタイムを試す絶好の機会であると思われました。
Goの経験は以前になく、タイムラインは厳しく、2日間で本番環境に対応する必要がありました。これは威圧的でしたが、Goを異なる、見落とされがちな角度、つまり開発速度からテストする機会と捉えました。Goの経験がない人がどれくらいの速さでGoを習得し、パフォーマンスとスケーラビリティのあるものを作成できるでしょうか?
設計
アプローチは、七面鳥の状態をURLにエンコードし、スナップショットをその場で描画してエンコードすることでした。
すべてのDoodleのベースは背景です
有効なリクエストURLは次のようになります: http://google-turkey.appspot.com/thumb/20332620][http://google-turkey.appspot.com/thumb/20332620
「/thumb/」に続く英数字文字列は、この画像が示すように、各レイアウト要素のどの選択肢を描画するかを(16進数で)示します。
プログラムのリクエストハンドラは、URLを解析して各コンポーネントにどの要素が選択されているかを判断し、背景画像の上に適切な画像を描画し、結果をJPEGとして提供します。
エラーが発生した場合、デフォルトの画像が提供されます。ユーザーはエラーページを見ることはないため、エラーページを提供する意味はありません。ブラウザはほぼ確実にこのURLを画像タグにロードしています。
実装
パッケージスコープで、七面鳥の要素、対応する画像の場所、背景画像のどこに描画すべきかを記述するデータ構造を宣言します。
var (
// dirs maps each layout element to its location on disk.
dirs = map[string]string{
"h": "img/heads",
"b": "img/eyes_beak",
"i": "img/index_feathers",
"m": "img/middle_feathers",
"r": "img/ring_feathers",
"p": "img/pinky_feathers",
"f": "img/feet",
"w": "img/wing",
}
// urlMap maps each URL character position to
// its corresponding layout element.
urlMap = [...]string{"b", "h", "i", "m", "r", "p", "f", "w"}
// layoutMap maps each layout element to its position
// on the background image.
layoutMap = map[string]image.Rectangle{
"h": {image.Pt(109, 50), image.Pt(166, 152)},
"i": {image.Pt(136, 21), image.Pt(180, 131)},
"m": {image.Pt(159, 7), image.Pt(201, 126)},
"r": {image.Pt(188, 20), image.Pt(230, 125)},
"p": {image.Pt(216, 48), image.Pt(258, 134)},
"f": {image.Pt(155, 176), image.Pt(243, 213)},
"w": {image.Pt(169, 118), image.Pt(250, 197)},
"b": {image.Pt(105, 104), image.Pt(145, 148)},
}
)
上記の点のジオメトリは、画像内の各レイアウト要素の実際の場所とサイズを測定することによって計算されました。
リクエストごとにディスクから画像をロードするのは無駄な繰り返しになるため、最初のリクエストを受信した際に、106枚すべての画像(13 * 8要素 + 1背景 + 1デフォルト)をグローバル変数にロードします。
var (
// elements maps each layout element to its images.
elements = make(map[string][]*image.RGBA)
// backgroundImage contains the background image data.
backgroundImage *image.RGBA
// defaultImage is the image that is served if an error occurs.
defaultImage *image.RGBA
// loadOnce is used to call the load function only on the first request.
loadOnce sync.Once
)
// load reads the various PNG images from disk and stores them in their
// corresponding global variables.
func load() {
defaultImage = loadPNG(defaultImageFile)
backgroundImage = loadPNG(backgroundImageFile)
for dirKey, dir := range dirs {
paths, err := filepath.Glob(dir + "/*.png")
if err != nil {
panic(err)
}
for _, p := range paths {
elements[dirKey] = append(elements[dirKey], loadPNG(p))
}
}
}
リクエストは単純な順序で処理されます
-
リクエストURLを解析し、パス内の各文字の10進数値をデコードします。
-
最終画像のベースとして背景画像のコピーを作成します。
-
layoutMapを使用して、各画像要素を背景画像のどこに描画すべきかを決定します。
-
画像をJPEGとしてエンコードします。
-
JPEGをHTTPレスポンスライターに直接書き込むことで、画像をユーザーに返します。
エラーが発生した場合は、defaultImageをユーザーに提供し、App Engineダッシュボードにエラーをログ記録して後で分析します。
以下に、説明コメント付きのリクエストハンドラのコードを示します。
func handler(w http.ResponseWriter, r *http.Request) {
// Defer a function to recover from any panics.
// When recovering from a panic, log the error condition to
// the App Engine dashboard and send the default image to the user.
defer func() {
if err := recover(); err != nil {
c := appengine.NewContext(r)
c.Errorf("%s", err)
c.Errorf("%s", "Traceback: %s", r.RawURL)
if defaultImage != nil {
w.Header().Set("Content-type", "image/jpeg")
jpeg.Encode(w, defaultImage, &imageQuality)
}
}
}()
// Load images from disk on the first request.
loadOnce.Do(load)
// Make a copy of the background to draw into.
bgRect := backgroundImage.Bounds()
m := image.NewRGBA(bgRect.Dx(), bgRect.Dy())
draw.Draw(m, m.Bounds(), backgroundImage, image.ZP, draw.Over)
// Process each character of the request string.
code := strings.ToLower(r.URL.Path[len(prefix):])
for i, p := range code {
// Decode hex character p in place.
if p < 'a' {
// it's a digit
p = p - '0'
} else {
// it's a letter
p = p - 'a' + 10
}
t := urlMap[i] // element type by index
em := elements[t] // element images by type
if p >= len(em) {
panic(fmt.Sprintf("element index out of range %s: "+
"%d >= %d", t, p, len(em)))
}
// Draw the element to m,
// using the layoutMap to specify its position.
draw.Draw(m, layoutMap[t], em[p], image.ZP, draw.Over)
}
// Encode JPEG image and write it as the response.
w.Header().Set("Content-type", "image/jpeg")
w.Header().Set("Cache-control", "public, max-age=259200")
jpeg.Encode(w, m, &imageQuality)
}
簡潔にするため、これらのコードリストからいくつかのヘルパー関数を省略しました。詳細については、ソースコードを参照してください。
パフォーマンス
このグラフは、App Engineダッシュボードから直接取得したもので、公開時の平均リクエストレイテンシを示しています。ご覧のとおり、負荷がかかっている状況でも60msを超えることはなく、中央値レイテンシは32ミリ秒です。リクエストハンドラがその場で画像操作とエンコードを行っていることを考えると、これは非常に高速です。
結論
Goの構文は直感的で、シンプルで、クリーンだと感じました。過去に多くのインタプリタ型言語を扱ってきましたが、Goは静的型付けでコンパイル型言語であるにもかかわらず、このアプリを記述する感覚は、まるで動的なインタプリタ型言語を扱っているようでした。
SDKに付属の開発サーバーは、変更後すぐにプログラムを再コンパイルするため、インタプリタ型言語と同じくらい高速に反復作業ができました。セットアップも非常に簡単で、開発環境のセットアップには1分もかかりませんでした。
Goの優れたドキュメントも、この作業を迅速に進めるのに役立ちました。ドキュメントはソースコードから生成されるため、各関数のドキュメントは関連するソースコードに直接リンクされています。これにより、開発者は特定の関数が何をするかを非常に迅速に理解できるだけでなく、パッケージの実装を深く掘り下げることを促し、良いスタイルと慣習を学ぶのを容易にします。
このアプリケーションを記述するにあたり、私はわずか3つのリソースを使用しました: App EngineのHello World Goの例、Goパッケージのドキュメント、そしてDrawパッケージを紹介するブログ記事です。開発サーバーと言語自体によって可能になった高速な反復のおかげで、私は24時間以内に言語を習得し、超高速で本番環境に対応したDoodleジェネレーターを構築することができました。
完全なアプリのソースコード(画像を含む)は、Google Codeプロジェクトでダウンロードできます。
DoodleをデザインしてくれたGuillermo RealとRyan Germickに感謝します。
次の記事: GoでStatHatを構築する
前の記事: Goプログラミング言語、2周年を迎える
ブログインデックス