Firestoreの単一documentの読み込み速度の検証
結論
単一documentを連続で読み込むと、約 300/s 程度の速度は出せそう。
読み込み速度
前回は書き込み速度を測定しましたが、今回は読み込み速度を調べました。
環境
- Google Compute Engine、e2-medium (2 vCPU、4GB メモリ)
- Ubuntu 20.04 LTS
- Docker 19.03.8
- Golang 1.14.9
- cloud.google.com/go/firestore 1.3.0
検証
単一のdocumentを、1秒毎に指定したrateの数だけ取得し、全ての取得処理が完了するまでの時間を測定します。これを指定した回数(今回は60回)繰り返しました。使用したコードは以下です。(createまわりは前回の使い回し)
| package main | |
| import ( | |
| "context" | |
| "flag" | |
| "fmt" | |
| "log" | |
| "os" | |
| "strconv" | |
| "sync" | |
| "time" | |
| "cloud.google.com/go/firestore" | |
| "google.golang.org/api/iterator" | |
| ) | |
| var ( | |
| total = flag.Int("total", 0, "total count") | |
| rate = flag.Int("rate", 0, "rate") | |
| duration = flag.Int("duration", 0, "duration") | |
| execName = flag.String("exec", "read", "exec") | |
| sub = flag.Bool("sub", false, "read sub") | |
| ) | |
| func main() { | |
| flag.Parse() | |
| ctx := context.Background() | |
| client, err := firestore.NewClient(ctx, os.Getenv("PROJECT_ID")) | |
| if err != nil { | |
| log.Fatal(err) | |
| } | |
| data := generateDocumentData(client) | |
| if *execName == "create" { | |
| exec(ctx, client, data, create) | |
| } | |
| if *execName == "read" { | |
| read(ctx, client.Collection("articles")) | |
| } | |
| } | |
| func panicIf(err error) { | |
| if err != nil { | |
| panic(err) | |
| } | |
| } | |
| func generateDocumentData(client *firestore.Client) []map[*firestore.DocumentRef]map[string]interface{} { | |
| limit := 120 | |
| totalCount := *total | |
| count := totalCount/limit + 1 | |
| data := make([]map[*firestore.DocumentRef]map[string]interface{}, 0, count) | |
| col := client.Collection("articles") | |
| for j := 0; j < count; j++ { | |
| min := limit * j | |
| max := limit * (j + 1) | |
| if max > totalCount { | |
| max = totalCount | |
| } | |
| if min == max { | |
| break | |
| } | |
| results := make(map[*firestore.DocumentRef]map[string]interface{}, limit) | |
| for i := min; i < max; i++ { | |
| doc := col.NewDoc() | |
| results[doc] = map[string]interface{}{ | |
| "title": "title_" + strconv.FormatInt(int64(i), 10), | |
| "tags": []string{"tag_1", "tag_2", "tag_3"}, | |
| "author_id": "author_id_" + strconv.FormatInt(int64(i), 10), | |
| "view_count": i, | |
| "is_published": true, | |
| "text": "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789", | |
| } | |
| subCol := doc.Collection("sections") | |
| for k := 0; k < 3; k++ { | |
| doc := subCol.NewDoc() | |
| results[doc] = map[string]interface{}{ | |
| "sort_order": k, | |
| "text": "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789", | |
| } | |
| } | |
| } | |
| data = append(data, results) | |
| } | |
| return data | |
| } | |
| func exec(ctx context.Context, client *firestore.Client, data []map[*firestore.DocumentRef]map[string]interface{}, | |
| execFunc func(ctx context.Context, client *firestore.Client, articles map[*firestore.DocumentRef]map[string]interface{})) { | |
| fmt.Println("start") | |
| start := time.Now() | |
| multi := 3 | |
| limit := make(chan struct{}, multi) | |
| wg := &sync.WaitGroup{} | |
| for _, articles := range data { | |
| limit <- struct{}{} | |
| wg.Add(1) | |
| go func(articles map[*firestore.DocumentRef]map[string]interface{}) { | |
| execFunc(ctx, client, articles) | |
| <-limit | |
| wg.Done() | |
| }(articles) | |
| } | |
| wg.Wait() | |
| fmt.Println(time.Now().Sub(start)) | |
| fmt.Println("end") | |
| } | |
| func create(ctx context.Context, client *firestore.Client, articles map[*firestore.DocumentRef]map[string]interface{}) { | |
| batch := client.Batch() | |
| for doc, article := range articles { | |
| batch.Create(doc, article) | |
| } | |
| _, err := batch.Commit(ctx) | |
| panicIf(err) | |
| } | |
| func read(ctx context.Context, col *firestore.CollectionRef) { | |
| iter := col.Documents(ctx) | |
| snap, err := iter.Next() | |
| panicIf(err) | |
| id := snap.Ref.ID | |
| duration := *duration | |
| rate := *rate | |
| start := time.Now() | |
| fmt.Println("start") | |
| wgAll := &sync.WaitGroup{} | |
| for j := 0; j < duration; j++ { | |
| wgAll.Add(1) | |
| go func() { | |
| start := time.Now() | |
| wg := &sync.WaitGroup{} | |
| for i := 0; i < rate; i++ { | |
| wg.Add(1) | |
| go func() { | |
| _, err := col.Doc(id).Get(ctx) | |
| panicIf(err) | |
| // log.Printf("%+v", snap.Data()) | |
| if *sub { | |
| iter := col.Doc(id).Collection("sections").Documents(ctx) | |
| for { | |
| _, err := iter.Next() | |
| if err == iterator.Done { | |
| break | |
| } | |
| panicIf(err) | |
| // log.Printf("%+v", snap.Data()) | |
| } | |
| } | |
| wg.Done() | |
| }() | |
| } | |
| wg.Wait() | |
| fmt.Println(time.Now().Sub(start)) | |
| wgAll.Done() | |
| }() | |
| time.Sleep(1 * time.Second) | |
| } | |
| wgAll.Wait() | |
| fmt.Println("end") | |
| fmt.Println(time.Now().Sub(start)) | |
| } |
結果は以下になります。

300/sまではギリギリ捌ききれていそうです。500/sで急に遅くなっているのは1秒間に捌ききれずにどんどん詰まっていったためと思われます。
追加の検証で、単一documentの取得後にdocumentに紐づくsub collection配下のdocument(今回は3つ)を全取得するような処理を試してみました。結果は以下になります。

100/s はほぼ0.4sで完了していましたが、1回だけ1s越えがありました。それ以外はほぼ変わらない結果になりました。
まとめ
単一アプリケーションからgoroutineで試してもこれくらいの速度が出るなら、懸念していたほど遅くはならなそうなので安心しました。