Go Conference 2014 Autumnレポート

3セッションについてのまとめ~「App Engine for Golang Performance」「Golang @ISUCON」「mackerel-agent徹底解説」~

この記事を読むのに必要な時間:およそ 7.5 分

Golang @ISUCON(@y_matsuwitter氏)

このセッションでは,@y_matsuwitter氏からISUCON4にGo言語を使って出場した話がされました。なお,このセッションの資料は以下のリンクから閲覧できます。

写真2 @y_matsuwitter氏の発表の様子

写真2 @y_matsuwitter氏の発表の様子

ISUCONと出題される問題

ISUCONとは,⁠Iikanjini Speed Up CONtest」の略だそうで,8時間で与えられたWebアプリを出来る限り高速にするコンテストのことだそうです。ISUCONの予選の問題は,AWSのインスタンス1つでMySQLをDBとして使ったWebアプリが多く,ISUCON4では銀行のログインページが題材だったそうです。また,本戦の問題はサーバ3~4台で画像処理や動画の配信など,ある程度負荷の高い処理を含むWebアプリが多く,ISUCON4では動画広告の配信サーバが題材だったそうです。

高速なWebサーバのために

同氏は,高速なWebサーバを実現するためには,以下のようなリソースを出来るだけ使い切る必要があると述べていました。そして,そのうえで適切な処理にリソースを割り当てることが大切だと主張していました。

  • CPU(エンコードやデコード,画像処理)
  • メモリ(プロセス上のデータ量)
  • データ通信(帯域に制限がある中でのサーバ間,サーバ・クライアント間の総通信量)

そのため,言語の性能自体は問題にならないと同氏は主張していました。一方で,Go言語を使用するメリットとして,以下のような改善効率の高さを挙げていました。

  • 構文がシンプルなので,短時間のチーム開発向き
  • 並列・並行処理でのメモリの扱いが楽
  • デプロイが楽
  • 標準パッケージが実用的

取り組んだこと

Webアプリの高速化のために,プロセスキャッシュとタスクの分散処理を行ったそうで,それぞれ説明がされていました。

はじめに,プロセスキャッシュについて説明がされました。同氏は,プロセスキャッシュとは,データをプロセス上にすべて保持しておくことだと述べていました。そして,プロセスキャッシュを行うことで,I/O待ちを減らうことができると主張していました。また,ISUCONではデータの永続化が必須であるため,プロセスキャッシュから適当なタイミングでRedisやファイルに書き込む必要があると述べていました。

同氏は,プロセスキャッシュを実現するためには,複数のゴルーチンからアクセスされても整合性が取れることを保証する必要があると述べていました。複数のゴルーチンから1つの変数にアクセスすることで起きるレースコンディションを避けるには,チャネルを使ってデータをやりとりするか,きちんとロックを取る必要があると主張していました。そして,ロックをとるには以下のような標準パッケージであるsyncパッケージの機能を使うのが良いと述べていました。

  • sync.RWMutexReadWriteでロックレベルを分離したロック
  • sync/atomic:カウンタなどの処理を作るのに重宝するパッケージ
  • atomic.Value:Go言語の1.4から導入されたアトミックに任意のデータ型の値を扱うことができる

つぎに,タスクの分散処理について話されました。同氏は,タスクの分散処理を行う要件として以下の2つを挙げていました。

  • 処理に時間がかかるものを非同期かつ平行実行数を制限して処理したい
  • 画像・動画などは重複して処理したくない

同氏は,タスクの分散処理を行うためには,タスクキューを作って,そこからタスクを取得し分散して処理すると良いと述べていました。タスクキューを実現するには,キャパシティ付きのチャネルとゴルーチンを使うことで実現できるそうです。このタスクキューは,チャネルに重たいタスクを入れておき,ゴルーチンをワーカーとして動かしておいて,ゴルーチンからフェッチすることで,チャネルからタスクを取得して処理を行うというものでした。また,ゴルーチンの数を制限することで並行実行数を制限して非同期に処理できるとのことでした。

ISUCONでは,画像など処理に時間がかかるものを非同期で処理している際に,その画像を取得するリクエストが来た場合,そのリクエストを失敗させてしまうと減点されてしまうそうです。しかし,画像の取得時に処理するのは時間がもったいないと同氏は主張していました。そのため,サーバが1台の場合は,sync.Condをゴルーチン間で共有し,以下のように処理中のものを選択的に待つと良いと同氏は述べていました。

c := sync.NewCond(&sync.Mutex{})

go func() {
    fmt.Println("doing heavy task.")
    time.Sleep(5 * time.Second)
    c.Broadcast()
}()
c.L.Lock()
c.Wait()
fmt.Println("completed!!")

一方,複数台のサーバの場合はsync.Condを共有することができないため,同氏はy-matsuwitter/mcondというライブラリを作成したそうです。このライブラリは,Redisnet/httpパッケージを使って,複数台のサーバ間でsync.Condを扱えるようにしたものだそうです。ライブラリのインターフェースは,sync.Condと似た形にしており,重い処理をする側では,以下のように使えるそうです。

重い処理をする側

mc := mcond.NewMCond(mcond.MCondOption{})
key := "test"
mc.AddCond(key)
mc.AddHost("localhost:9012")
mc.Clear()

mc.AddProcessing(key)
go func() {
    fmt.Println("doing heavy task.")
    time.Sleep(5 * time.Second)
    mc.AddCompleted(key)
    mc.Broadcast(key)
}()

mc.WaitForAvailable(key)
fmt.Println("completed!!")
time.Sleep(time.Second)

また,処理した結果を使用する側では以下のように使えるそうです。

処理した結果を使用する側

mc := mcond.NewMCond(mcond.MCondOption{})
key := "test"
mc.AddCond(key)
mc.Start()
go func() {
    mc.WaitForAvailable(key)
    fmt.Println("this key available!! :" + key)
}()
time.Sleep(time.Minute)

同氏は,タスクをできるだけ一定のサーバに振り分けるために,コンシステントハッシュライブラリのstathat/consistentを利用すると良いと述べていました。ISUCON4では,帯域を使い潰すために,以下のように複数のWebDAVサーバに振り分けるのに使用したそうです。

const (
    webdavHost  = "http://10.0.0.1"
    webdavHost2 = "http://10.0.0.1"
)

var webdavRouter *consistent.Consistent
func init() {
    webdavRouter = consistent.New()
    webdavRouter.Add(webdavHost)
    webdavRouter.Add(webdavHost2)
}

func getWebdavHost(key string) string {
    server, _ := webdavRouter.Get(key)
    return server
}

同氏はセッションの最後に,もっとGo言語をプロダクションで使って欲しいと話していました。プロダクションで使う企業が増えてくれば,知見も増えてくるので嬉しいとのことでした。

著者プロフィール

上田拓也(うえだたくや)

KLab(株)所属。Go Conference 2014 Autumnスタッフ。

業務ではJavaScript,Luaなどを扱っている。

大学時代にGo言語に出会い,それ以来Go言語にのめり込む。

時間があると,Go言語の勉強会に参加している。

Go言語のマスコットのGopherの絵を描くのも好き。

Twitter:@tenntenn