database/sqlのrowsのメモリ量を調査する
今回はGoの「database/sql」を使って、結果が大きいクエリを発行したときにヒープのメモリ量を見ていきたいと思います。
検証理由
多くの場合LimitとOffsetを指定するのですが、大量の結果が返されるときに rows がどのくらいメモリを使っているのか知りたくなりました。
rows.Next と rows.Scan を利用するので、 rows に結果が一度に入りヒープのメモリ量が増えることはないだろうという前提での検証です。準備
MySqlにUUIDが入っているカラムと、テキトウな文字を入れるカラムを用意して、そこに30,000件のデータを登録しておきます。
mysql> select count(*) from dummy;
+----------+
| count(*) |
+----------+
| 30000 |
+----------+
*************************** 30000. row ***************************
id: fffedc81-f3f0-4de3-86b5-3b029f502e6a
content: Voluptatem sit accusantium perferendis aut consequatur. Aut perferendis voluptatem sit accusantium consequatur. Voluptatem sit accusantium aut perferendis consequatur. Voluptatem aut perferendis consequatur sit accusantium.
内容はこんな感じになります。
次に、検証用のコードを書きます。
今回は、簡単にヒープに割り当てられたメモリ量を表示しています。
package query
import (
"database/sql"
"fmt"
"log"
"runtime"
_ "github.com/go-sql-driver/mysql"
)
var mem runtime.MemStats
func Query() {
db, err := sql.Open("mysql", "user:password@tcp(localhost:49152)/foo")
if err != nil {
panic(err)
}
db.SetMaxOpenConns(20)
db.SetMaxIdleConns(20)
var (
id string
content string
)
rows, err := db.Query("SELECT id, content FROM dummy")
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
err := rows.Scan(&id, &content)
if err != nil {
log.Fatal(err)
}
}
// runtime.GC()
runtime.ReadMemStats(&mem)
fmt.Println(mem.Alloc, mem.TotalAlloc)
err = rows.Err()
if err != nil {
log.Fatal(err)
}
}
実際に動かしてメモリを計測する
それでは、sql部分にLimitを追加して検証していきたいと思います。
左から、ヒープに割り当てられたメモリ量、ヒープに割り当てられたメモリの総量(減らない)、OSから取得したメモリ量です。
Limit: 10
376048 376048 71650320
376256 376256 71912464
376544 376544 71650320
Limit: 10000
1202960 4504840 73679880
1235760 4505128 73942024
1134624 4504952 73483272
Limit: 20000
1510472 8630744 74007560
1460512 8629504 74269704
1462568 8632184 74269704
Limit: 30000
1823432 12766528 74269704
1945400 12764736 74269704
1959984 12764528 74269704
Limitを上げると順調にヒープが育っていってます。
次に、上記コードのGC部分のコメントを外して実行します。(検証用にGCをコールしています)
Limit: 10
223784 382464 71715600
226216 384672 72239888
226264 385360 72239888
Limit: 10000
224800 4503592 74269704
224472 4504720 74269704
226152 4504200 74269704
Limit: 20000
226848 8632624 74269704
229000 8633136 74007560
224312 8628920 74007560
Limit: 30000
228488 12765656 74269704
226488 12764600 74269704
224776 12764296 74007560
今度はGCによって削除されているのがわかります。
次に、ループ中のメモリを調べたいので、少し強引ですが rows.Next のループ毎にGCを呼んでみます。
毎回GCを呼ぶのでかなり遅くなることが予想されます。
for rows.Next() {
err := rows.Scan(&id, &content)
if err != nil {
log.Fatal(err)
}
runtime.GC()
runtime.ReadMemStats(&mem)
fmt.Println(mem.Alloc, mem.TotalAlloc, mem.Sys)
}
Limit: 10
228624 399640 74007560
228640 401680 74007560
228632 403752 74007560
Limit: 10000
250304 22738464 74269704
250304 22741000 74269704
250400 22743632 74269704
Limit: 20000
251448 45285408 74335240
251448 45287880 74335240
251448 45290352 74335240
Limit: 30000
253800 67179272 74269704
253800 67181200 74269704
253800 67183336 74269704
手元の環境だと30000レコードで9秒かかりました。
こちらもヒープのメモリ量は一定になりましたが、GCが多発すると遅くなることが実感できます。
まとめ
簡易的な検証でしたが、以下のような予想ができます。
- 一度に大量の行をクエリすると、database/sqlのrowsのヒープは増えていく
- GCが実行されると削除されるが、GCが過剰に実行されると遅くなる
この記事を書いた人
-
2008年にアーティスへ入社。
システムエンジニアとして、SI案件のシステム開発に携わる。
その後、事業開発部の立ち上げから自社サービスの開発、保守をメインに従事。
ドメイン駆動設計(DDD)を中心にドメインを重視しながら、保守可能なソフトウェア開発を探求している。
この執筆者の最新記事
関連記事
最新記事
FOLLOW US
最新の情報をお届けします
- facebookでフォロー
- Twitterでフォロー
- Feedlyでフォロー