Go Conference 2014 Autumnでは、
App Engine for Golang Performance(@sinmetal氏)
このセッションでは、

Goolge App Engine for Golang (GAE/G)
まずはじめに、
- すべてGoogleが提供するインフラの上で動くため、
インフラのことを心配する必要がない - オートスケーリングというリクエストに合わせて、
自動的にインスタンスが増える機能がある - Webアプリを作るためのプラットフォームの提供
また同氏は、
一方、
- Go言語のバージョンアップへの追従が遅い
(セッション発表当時は Go 1. 2) GOMAXPROCS
が1
であるため、ゴルーチンが複数のコアで動かない - 独自にツールをインストールしたり、
カスタマイズすることができない - ベータ版であるためプロダクトで使いづらい
Managed VMs
同氏は、
- ローカルファイルにアクセスできる
- Dockerfileを書き換えることでカスタマイズできる
- コア数を指定することができるので、
GOMAXPROCS
が変更できる
しかし、
- デプロイが非常に遅い
(5分くらいかかる) - auto-scalingの最小値が
1
であるため、常時インスタンスが起動し続ける
Managed VMsについては、
パフォーマンスの計測
今回のパフォーマンス計測では、Hello, Wold
を返すような簡単なWebアプリをデプロイし、
- Javaで書かれたものは、
スピンアップに時間がかかる - Go言語で書かれたものは、
Javaで書かれたものと比べて、 半分のインスタンス数で処理することが可能
そして、
- Go言語で書かれたものは、
インスタンス1つで十分処理できた - Javaで書かれたものは、
503エラーを返していたため、 うまく計測できなかった
同氏は、
Golang @ISUCON(@y_matsuwitter氏)
このセッションでは、

ISUCONと出題される問題
ISUCONとは、
高速なWebサーバのために
同氏は、
- CPU
(エンコードやデコード、 画像処理) - メモリ
(プロセス上のデータ量) - データ通信
(帯域に制限がある中でのサーバ間、 サーバ・ クライアント間の総通信量)
そのため、
- 構文がシンプルなので、
短時間のチーム開発向き - 並列・
並行処理でのメモリの扱いが楽 - デプロイが楽
- 標準パッケージが実用的
取り組んだこと
Webアプリの高速化のために、
はじめに、
同氏は、sync
パッケージの機能を使うのが良いと述べていました。
sync.
:RWMutex Read
とWrite
でロックレベルを分離したロックsync/
:カウンタなどの処理を作るのに重宝するパッケージatomic atomic.
:Go言語の1.Value 4から導入されたアトミックに任意のデータ型の値を扱うことができる
つぎに、
- 処理に時間がかかるものを非同期かつ平行実行数を制限して処理したい
- 画像・
動画などは重複して処理したくない
同氏は、
ISUCONでは、sync.
をゴルーチン間で共有し、
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.
を共有することができないため、y-matsuwitter/
というライブラリを作成したそうです。このライブラリは、net/
パッケージを使って、sync.
を扱えるようにしたものだそうです。ライブラリのインターフェースは、sync.
と似た形にしており、
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/
を利用すると良いと述べていました。ISUCON4では、
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
}
同氏はセッションの最後に、
mackerel-agent徹底解説(@songmu氏)
このセッションでは、

Mackerel
Mackerelとは、
Mackerelのアーキテクチャは、
mackerel-agent
mackerel-agentとは、
同氏は、
- シングルバイナリでコンパイルされるので、
セットアップが容易 - マルチプラットフォーム対応が比較的容易
- 常駐プロセスを書くことに向いている
- フットプリントが小さいため、
監視対象のサーバーのパフォーマンスに影響を及ぼさない - 書いていて楽しい
mackerel-agentソースコード解説
mackerel-agentの実装について、
- シグナルハンドリング
- データ送受信制御
- グレースフルシャットダウン
ディレクトリ構成
@songmu氏は、
- command:メインの処理
- mackerel:Mackerel APIへの投稿処理
- agent:メトリクス収集処理を束ねている
- metrics:各種メトリクス収集処理単位
- その他
main
関数
まず、main
関数
func main() {
conf, printVersion := resolveConfig()
// (snip)
logger.Infof("Starting mackerel-agent version:%s, rev:%s", version.VERSION, version.GITCOMMIT)
// (snip)
if err := start(conf); err != nil {
exit(1, conf)
}
}
main
関数では、start
関数に処理を渡しています。
start
関数
start
関数では、
pid
ファイルの作成- ホスト情報・
投稿先情報の設定 - シグナルハンドラの設定
command.
関数に処理を渡すRun
同氏は、start
関数では、
c := make(chan os.Signal, 1)
termChan := make(chan chan int) // メインの処理との情報のやりとり
signal.Notify(c, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGHUP)
go func() { // シグナルハンドリング用のgoroutine
for sig := range c {
if sig == syscall.SIGHUP { // sighupを受け取ったらhost情報を読み込み直す
command.UpdateHostSpecs(conf, api, host)
} else { // 他のシグナルを受け取ったら処理の終了を待ってexit
exitChan := make(chan int)
termChan <- exitChan // チャンネルにチャンネルを渡す
go func() { // 但し処理に時間が掛かるようなら強制的に終了
time.Sleep(MAX_TERMINATING_INTERVAL * time.Second)
exitChan <- 1
}()
exitCode := <-exitChan // 渡したチャンネルからexitCodeの返却を待つ
exit(exitCode, conf)
}
}
}()
command.Run(conf, api, host, termChan) // メインの処理
まず、c
を作成し、signal.
で受け取るシグナルを設定しています。このとき、syscall.
ではなくos.
を使用しないとWindowsで正しく動かないとのことでした。
そして、syscall.
を受け取った場合は、termChan
というint
のチャネルのチャネルchan chan int
)termChan
に渡したチャネルを介して、
Run
関数
command.Run
関数では、Agent
オブジェクトを作成して、loop
関数に処理を渡しているそうです
func Run(conf *config.Config, api *mackerel.API, host *mackerel.Host, termChan chan chan int) {
logger.Infof("Start: apibase = %s, hostName = %s, hostId = %s", conf.Apibase, host.Name, host.Id)
ag := &agent.Agent{
MetricsGenerators: metricsGenerators(conf),
PluginGenerators: pluginGenerators(conf),
}
ag.InitPluginGenerators(api)
loop(ag, conf, api, host, termChan)
}
loop
関数の概要
loop
関数ではメインの処理行っているそうで、
- キューからメトリクスを投稿する処理
- ホスト情報を定期更新する処理
- メトリクスを取得してキューに入れる処理
このセッションでは、
なお、queueState
)
type queueState int
const (
queueStateFirst queueState = iota // 初期状態
queueStateDefault // 通常
queueStateQueued // キューが溜まっている場合
queueStateTerminated // シグナルを受け取っている場合
)
メトリクスの投稿処理(初期化処理)
はじめに、postQueue
のサイズは6時間程度のメトリクスを保持できる程度の大きな値を指定しているそうです。キューの状態はqState
で管理され、queueStateFirst
)
// 十分なバッファを溜め込めるように
postQueue := make(chan []*mackerel.CreatingMetricsValue, conf.Connection.Post_Metrics_Buffer_Size)
go func() {
postDelaySeconds := delayByHost(host)
qState := queueStateFirst // キューの状態
exitChan := make(chan int) // 処理を抜ける際にexitCodeを送るチャンネル
メトリクスの投稿処理(終了割り込みに対する処理)
終了割り込みに対する処理について、
for {
select {
case exitChan = <-termChan: // シグナルを受け取ったら割り込まれる
if len(postQueue) <= 0 {
exitChan <- 0 // キューにデータが残ってなかったら抜ける
} else { // 残っている場合はキューの状態を更新して処理続行
qState = queueStateTerminated
}
case values := <-postQueue:
シグナルを受け取ると、termChan
から終了コードを受け取るためのチャネルが送られてきます。このとき、queueStateTerminated
にします。
メトリクスの投稿処理(バルク送信)
1分に1回キューにメトリクスが送られてくるため、
case values := <-postQueue:
if len(postQueue) > 0 {
// 送り過ぎないように最大2件
logger.Debugf("Merging datapoints with next queued ones")
nextValues := <-postQueue
values = append(values, nextValues...)
}
メトリクスの投稿処理(送信タイミングの調整)
キューの状態に応じて、queueStateDefault
)
// 状態に応じてdelayさせるタイミングを調整
delaySeconds := 0
switch qState {
case queueStateTerminated:
delaySeconds = 1
case queueStateFirst:
// nop
case queueStateQueued:
delaySeconds = conf.Connection.Post_Metrics_Dequeue_Delay_Seconds
default:
// 通常状態で00秒にAPIへのサーバーがアクセスすると困るので投稿時間をhashingしている
elapsedSeconds := time.Now().Second() % int(config.PostMetricsInterval.Seconds())
if postDelaySeconds > elapsedSeconds {
delaySeconds = postDelaySeconds - elapsedSeconds
}
}
メトリクスの投稿処理(スリープ処理)
メトリクスの投稿を遅らせるのには、time.
を使用しているそうです。このとき、select
でtermChan
からも受信しています。
sleepCh := make(chan struct{})
go func() {
time.Sleep(delaySeconds * time.Second)
sleepCh <- struct{}{}
}()
sleepLoop:
for {
select {
case <-sleepCh:
break sleepLoop
case exitChan = <-termChan:
qState = queueStateTerminated
break sleepLoop
}
}
メトリクスの投稿処理(投稿処理)
実際にメトリクスを投稿する部分は以下のとおりになっているそうです。この部分には、
tries := conf.Connection.Post_Metrics_Retry_Max
for {
err := api.PostMetricsValues(values)
if err == nil {
logger.Debugf("Posting metrics succeeded.")
break
}
tries -= 1
if tries <= 0 {
logger.Errorf("Give up retrying to post metrics.")
break
}
time.Sleep(conf.Connection.Post_Metrics_Retry_Delay_Seconds * time.Second)
}
メトリクスの投稿処理(終了処理)
同氏は、
// シグナル受信状態でキューにデータが残ってなかったらexit
if qState == queueStateTerminated && len(postQueue) <= 0 {
exitChan <- 0
}
同氏はセッションの最後に、