Goブログ

ゼロからGoへ:24時間でGoogleホームページに公開

レイナルド・アギア
2011年12月13日

はじめに

この記事は、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の経験がない人がどれくらい早く習得し、パフォーマンスとスケーラビリティを備えた何かを構築できるでしょうか?

設計

アプローチは、七面鳥の状態を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ダッシュボードから直接取得したもので、起動時の平均リクエストレイテンシを示しています。ご覧のとおり、負荷がかかっても60ミリ秒を超えることはなく、中央値レイテンシは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周年を迎える
ブログインデックス