この記事ではAWS ec2上のCentOS7にインストールしたGo1.7+mgoと、MongoDB3.2を使っています。
MongoDBは、遅い遅いと言われているとともに、シャーディングが複雑な割には性能が出ないとも言われています。しかし、AWS ec2でベンチマークしてみた結果、そこそこの性能が出せました。クラウドによってスケールアップが容易になったため、シャーディング無しの1インスタンスでも、大抵のサイトでは十分に活用できるのものと思います。
##ベンチマーク条件
クライアント側はGo1.7+mgo、サーバ側はMongoDB3.2として、それぞれにec2のインスタンスを割り当てて、ネットワーク越しに1KBのドキュメントを20万回insertして、ベンチマークしました。
クライアント側のコードを示します。
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/秒以上を出せる方がいれば報告してほしいものです。