MongoDBベンチマーク 1インスタンスで8万insert/秒達成

この記事ではAWS ec2上のCentOS7にインストールしたGo1.7+mgoと、MongoDB3.2を使っています。

MongoDBは、遅い遅いと言われているとともに、シャーディングが複雑な割には性能が出ないとも言われています。しかし、AWS ec2でベンチマークしてみた結果、そこそこの性能が出せました。クラウドによってスケールアップが容易になったため、シャーディング無しの1インスタンスでも、大抵のサイトでは十分に活用できるのものと思います。

##ベンチマーク条件

クライアント側はGo1.7+mgo、サーバ側はMongoDB3.2として、それぞれにec2のインスタンスを割り当てて、ネットワーク越しに1KBのドキュメントを20万回insertして、ベンチマークしました。

クライアント側のコードを示します。

```go:mongo_bench.go
package main

import (
  "fmt"
  "strings"
  "os"
  "strconv"
  "time"
  mgo "gopkg.in/mgo.v2"
)

const lenDummydata = 970   // create 1Kbyte-size bson

type InsertDoc struct {
  D string `bson:"d"`
}

func main() {
  // confirm arg
  maxSessions := 100  // arg #1
  maxGoroutines := 10000  // arg #2, shold be a multiple of maxSessions
  numInserts := 200000  // arg #3, shold be a multiple of maxGoroutines
  if len(os.Args) >= 2 {
    maxSessions, _ = strconv.Atoi(os.Args[1])
  }
  if len(os.Args) >= 3 {
    maxGoroutines , _ = strconv.Atoi(os.Args[2])
  }
  if len(os.Args) >= 4 {
    numInserts, _ = strconv.Atoi(os.Args[3])
  }
  fmt.Println("maxSessions :=", maxSessions, "maxGoroutines :=", maxGoroutines, "numInserts :=", numInserts)

  // clear mongodb
  session, err := mgo.Dial("mongodb://172.31.18.129/")
  if err != nil {
    panic(err)
  }
  COL := session.DB("test").C("COL")
  COL.DropCollection()
  // create InsertDoc
  var doc InsertDoc
  doc.D = strings.Repeat("0", lenDummydata)
  // start timer
  time_start := time.Now()

  // multiplex mgo sessions
  for i := 0; i < maxSessions; i++ {
    go func() {
      session, err := mgo.Dial("mongodb://172.31.18.129/")
      if err != nil {
        panic(err)
      }
      COL := session.DB("test").C("COL")
      // multiplex goroutines
      for j := 0; j < maxGoroutines / maxSessions; j++ {
        go func() {
          for k:= 0; k < numInserts / maxGoroutines; k++ {
            // insert document
            err := COL.Insert(&doc)
            if err != nil {
              panic(err)
            }
          }
        } ()
      }
    } ()
  }
  // wait all docs inserted
  n := 0
  for n < numInserts {
    n, _ = COL.Count()
    time.Sleep(10 * time.Millisecond)
  }
  // print result
  fmt.Println(n, "docs inserted")    
  duration := time.Now().Sub(time_start).Seconds()*1000
  fmt.Println("TOTAL:", int(duration), "msec", int(float64(numInserts) * 1000 /duration), "docs/s")
}
```

`$ go run mongo_bench.go DBセッション数 goroutine数 insert回数`でベンチマークを実行します。後ろの引数は省略可能です。

goroutineの完了待ち合わせには、sync.WaitGroupを使うのが常套手段ですが、単純にMongoDB側の書き込み数を監視した方が、若干速くなるようです。こちらの方が非同期書き込みっぽいですね。

MongoDB側は、デフォルトindexのみで、journalingはenableです。他で試してみたところ、indexを増やすと、若干（数%）書き込みが遅くなるようです。またsnappyによる圧縮により、30%程度まで実データが圧縮されているようで、ストレージ性能の制約が出にくくなっていると思います。insert文字列を乱数生成することで影響を減らすことはできますが、乱数生成のオーバーヘッドを考慮して、今回はそこまではやりませんでした。

##結果と考察

以下に結果を表にまとめます。
AWS ec2のインスタンスタイプ、DBセッション数、goroutine数を変えて、様々なベンチマーク結果を比較しています。

秒間insert数の平均値
（d/gは、d:DBセッション数、g:goroutine数）
（インスタンスタイプはクライアント〜DBの双方で同一）

|インスタンスタイプ|コア数|1/1|1/100|100/100|100/10000|
|:--|--:|--:|--:|--:|--:|
|t2.small|1|2,014|24,547|9,917|10,293|
|t2.medium|2|2,716|23,114|21,986|18,665|
|t2.xlarge|4|3,671|18,623|59,356|33,101|
|t2.2xlarge|8|3,727|21,828|84,428|58,290|
|m4.4xlarge|16|4,629|19,986|74,102|69,072

DBセッションを貼る本数、goroutineの多重数によって、結果が微妙に異なります。特に、DBセッション数=1のままでは、マルチコア環境でもMongoDB側が1コアしか利用されないため、性能がスケールしないことに注意が必要です。

DBセッション=goroutine数=100とすると、比較的良好な結果が出るようです。t2.2xlargeで8万insert/秒を達成しました。

一方、お金をかけてm4.4xlargeも試してみましたが、さらに速くはなりませんでした。t2.2xlargeとt2.xlargeの比率も倍増していませんので、CPU以外のボトルネックに到達したのかもしれません。類似の前提条件で10万insert/秒以上を出せる方がいれば報告してほしいものです。