大量の個人情報を生成してMongoDBに効率的に挿入する #golang #MongoDB #Faker
はじめに
MongoDBテクニカルサポートの山森です。
先日、MongoDBのパフォーマンスチューニングに関わる機会がありました。クエリの速度が9割方改善したのを目の当たりにし、パフォーマンスチューニングの奥深さを感じました。
そこで、効率的なクエリの書き方やインデックスについて、実機を触りながら知見を深めるサンドボックスの環境が欲しくなりました。
パフォーマンスチューニングやインデックスの効果を検証するために、データ数としては数十万件は欲しいなと思いましたが、好みのサンプルデータ(※1)が見つけられなかったので、自分で生成することにしました。
今回は、サンドボックスの下準備として、個人情報のフェイクデータを大量(数十万件~)に生成してMongoDBに挿入してみた。という内容です。
実装するまでの紆余曲折を時系列で書いていますが、手っ取り早く解決策だけを知りたい方は、目次から「解決策」以降をご覧ください。
※1…MongoDBのAtlasのサンプルデータだと最大で10万件(sample_training内のgrades)でした。
言語の選定
MongoDBでは各言語の公式ドライバが提供されています。
今回はGO言語を使用することにしました。選定理由は以前から気になっていたからです。
普段からプログラミングされる方はお好きな言語を使うのが良いかと思います。
チュートリアル
今回の記事ではあまり重要でないので、このへんを参考にしたよ。というのを上げていきます。
プログラミングは年単位でご無沙汰でしたし、初めて触る言語でもあったので、A Tour of Goや初心者向けの解説記事に助けられました。
Goの初心者が見ると幸せになれる場所 #golang #Go - Qiita
フェイクデータの生成
フェイクデータの生成と言えばFakerです。GO言語でもFakerのパッケージが提供されています。
複数あるようですが、今回はこちらを使用しました。
まずは、個人情報を1件生成して、出力するプログラムを書いてみました。出力する項目は以下の通りです。
- ユーザー名
- パスワード
- 年齢(18歳~100歳の間のランダムな数値)
- 性別(男性 or 女性)
- ファーストネーム(名前)
- ラストネーム(苗字)
- 年収(200万円~5000万円のランダムな数値)
Fakerではいろいろな項目に対応していますので、興味のある方はドキュメントにあるExampleを見てみてください。今回は英語で出力していますが、ほかの言語(もちろん日本語も対応)でも出力可能です。
package main import ( "fmt" "github.com/go-faker/faker/v4" "github.com/go-faker/faker/v4/pkg/options" ) type PersonalData struct { Username string `custom:"username"` Password string `custom:"password"` Age int `custom:"boundary_start=18,boundary_end=100"` Gender string `custom:"oneof:female, male"` Firstname string `custom:"first_name"` Lastname string `custom:"last_name"` Income int `custom:"boundary_start=200,boundary_end=5000"` } func main() { var personaldata PersonalData err := faker.FakeData(&personaldata, options.WithTagName("custom")) if err != nil { panic(err) } fmt.Println(personaldata) }
実行した環境はWindows 11上のWSL(Debian GNU/Linux 12.5)です。
以下が実行結果です。実行するたびにランダムなデータが1件生成されているのが分かります。
kayou@DESKTOP-EUK2PH4:~/go-faker-mongodb/make-data-by-faker$ go run fake_sample.go {fEJULSH dRbLiYAFCxTntarBMJTjBBeDRbcyGxBqJgQOYlkfhTMflbjMGV 96 female Bettie Weissnat 549} kayou@DESKTOP-EUK2PH4:~/go-faker-mongodb/make-data-by-faker$ go run fake_sample.go {NKfSZxs cDiBPiKmVkGPWdhxZsHewIalDHPZrKygYHvaKjWCSTEUMaFYGQ 44 male Nyasia Frami 1141} kayou@DESKTOP-EUK2PH4:~/go-faker-mongodb/make-data-by-faker$ go run fake_sample.go {CfFmibZ dOdOXKxgGMFGuYTyUqjIHInDOQdHMsoYuaaVKCbAcfKPxBGTVT 67 male Melba Wolf 561}
これを繰り返せば、数十万件のフェイクデータを生成できそうです。for文を追加してみました。
package main import ( "fmt" "github.com/go-faker/faker/v4" "github.com/go-faker/faker/v4/pkg/options" ) type PersonalData struct { Username string `custom:"username"` Password string `custom:"password"` Age int `custom:"boundary_start=18,boundary_end=100"` Gender string `custom:"oneof:female, male"` Firstname string `custom:"first_name"` Lastname string `custom:"last_name"` Income int `custom:"boundary_start=200,boundary_end=5000"` } func main() { for i := 0; i < 10; i++ { var personaldata PersonalData err := faker.FakeData(&personaldata, options.WithTagName("custom")) if err != nil { panic(err) } fmt.Println(personaldata) } }
10回繰り返す場合の出力結果です。良い感じです。
kayou@DESKTOP-EUK2PH4:~/go-faker-mongodb/make-data-by-faker$ go run fake_sample.go {LSWguOM hLUxlTZchcZiZZlBajWQYgWocqebgjSjEmupHUsxCZRjxFmQgV 58 male Lura Schmidt 1706} {GoVdGFY wdDpnedFNgxmMJuiVRcbZMllxEDIYNGHXkFbCNwCDHyGLlukRY 69 male Johanna Hessel 1129} {RplUKqc fROqgCXbQCjPcfyAlVOWiemIhoJuuyGneNvYkWsucWdgSBIGcU 95 male Logan Homenick 1115} {ymtjqVL amFRrTeUYiFRWJSlPYFBoItxkMxkdhkeLVrGTgImpvIBZOLoww 55 female Eveline Hirthe 4426} {TrsEQeA qhqXqGsTCRcMHPVWnItlNSKPToQrZenBAgtAEIICpdOdZfPWZj 83 female Ida Green 1258} {NgwymBO ETJPhqBrTvHqWoVfUNKOMBPeYSwQhKbprnKVwvNQamhHQHsBBg 30 male Sienna Gusikowski 4115} {gKKCwlE ZekMjNqSIkOlYqASIeRFVMDklHBcLTNyrZvrjevLFRNaNEkWQA 98 male Lizzie Stokes 4733} {YHZEdjT JYcTgEKJdCvyCkPbTORVmZhlxGLsucrJHNmtfbCYuFsHdBRlhT 95 male Amalia Reichert 2198} {HEXKeFt OCaCuOGVkyKqwNTwUZqmVjniBviBNjDhFQPKLnwMXkRixfkGwx 51 male Marta Schamberger 480} {oYVPRgX xVxEcRAXCOuenNhNQMuKjHvoTYTZhRvSYrDqNBGOdteXRPTVXv 55 female Arielle Hegmann 2540}
MongoDBにデータを挿入するコードを追加します。接続先のMongoDB(コード内ではrep1sv.mongo.lab)は、WSLが乗っているWindows PCと同じネットワークセグメントにあるMongoDB Server(6.0.14)です。ブログ内では登場しませんが、他に2台のセカンダリがおり、3台のレプリカセットです。
package main import ( "context" "fmt" "github.com/go-faker/faker/v4" "github.com/go-faker/faker/v4/pkg/options" "go.mongodb.org/mongo-driver/mongo" mongoOptions "go.mongodb.org/mongo-driver/mongo/options" ) type PersonalData struct { Username string `custom:"username"` Password string `custom:"password"` Age int `custom:"boundary_start=18,boundary_end=100"` Gender string `custom:"oneof:female, male"` Firstname string `custom:"first_name"` Lastname string `custom:"last_name"` Income int `custom:"boundary_start=200,boundary_end=5000"` } func main() { for i := 0; i < 10; i++ { var personaldata PersonalData err := faker.FakeData(&personaldata, options.WithTagName("custom")) if err != nil { panic(err) } fmt.Println(personaldata) // MongoDB接続設定 uri := "mongodb://rep1sv.mongo.lab:27017/" DatabaseName := "test" CollectionName := "testcoll" client, err := mongo.Connect(context.TODO(), mongoOptions.Client().ApplyURI(uri)) if err != nil { panic(err) } // コレクションとデータベースの指定 coll := client.Database(DatabaseName).Collection(CollectionName) result, err := coll.InsertOne(context.TODO(), personaldata) if err != nil { panic(err) } fmt.Println(result.InsertedID) // } }
実行してみました。思った通りに動いています。
kayou@DESKTOP-EUK2PH4:~/go-faker-mongodb/make-data-by-faker$ go run fake_sample.go {ekBEckJ aLQhGTTYWKkDJqBBCyAGASpBisSkuVnsqHIENCKIneUoQxFxUK 26 male Manuel Fahey 2182} ObjectID("660225d588b90bfd9a524653") {SmbBcdn fjrwbyOuDUFPFwZWUkQKUpvJOwAstDfqdJHxBsSGlcXeScvfil 66 female Ruthe Hermiston 3870} ObjectID("660225d588b90bfd9a524655") {BUYmMsj HWvkZhSuujolZPixLArdaFYIVqQMwTISagtfdPJQHPZEpvBKeq 68 female Aurelie Moen 2328} ObjectID("660225d588b90bfd9a524657") {MDQbflZ gjSdSlvviahSOJcaveyYetNTWHPWQtdVabgMqAXsTgFdYLnaSG 58 male Brennan Ullrich 3608} ObjectID("660225d588b90bfd9a524659") {groUghY CIYecBwwKLvsoVxeRwPgZyGGnFBDGtsGjKSAaBSEXxYcqZTYZv 33 male Britney Dickens 2930} ObjectID("660225d588b90bfd9a52465b") {foLkXqw fxMPkBGgsEwTqCSbahPZsMwWwcTmaFXOeAadmBfyOlebyRXcbF 62 male Bethany Durgan 468} ObjectID("660225d588b90bfd9a52465d") {RfmquVA OQoHBgRGZuOKIryOmXFjmieYbuMsuBIRbMxNbyytVnZxHDJAJR 55 female Vernon Feest 2075} ObjectID("660225d588b90bfd9a52465f") {vQsafxr mijoOlNVJfEcKCKunChOPsNetwqPeESPQPNUCVCGGfGDuSMTwS 76 female Porter Borer 2526} ObjectID("660225d588b90bfd9a524661") {KXCsjpn wqBeSPYuUknAgQMkoPBOwbdMODfsWUOBNjDfcRjJEECZpCxEoP 82 male Assunta Murphy 3667} ObjectID("660225d588b90bfd9a524663") {FpOZjWo YEaCEVxAdnjEsCRtctfNlDUpSHfOuSsstyFrrIAAZpMITjnaJY 60 male Dovie Wuckert 2127} ObjectID("660225d588b90bfd9a524665")
あとはfor文で繰り返す回数を増やせば目的達成ですね。
…お察しの良い方は、この辺から「ばっかもん!」と思うかもしれません。だいたい思った通りのことが起きます。
(´゚д゚`)< さすがに1万件は時間がかかるなあ…コーヒーでも飲んで待ってよっと
panic: connection() error occurred during connection handshake: dial tcp 192.168.50.119:27017: i/o timeout goroutine 1 [running]: main.main() /home/kayou/go-faker-mongodb/make-data-by-faker/fake_sample.go:48 +0x2df exit status 2 real 3m16.508s user 1m17.444s sys 0m56.345s
(;´゚д゚`)< 3分もかかって異常終了しとる…
時間計測にはtimeコマンドを使用しました。「time go run main.go」で実行しています。何回かやり直してみましたが、全て3分台で異常終了してしまいました。
パラメータチューニング
(;´゚д゚`)< データ挿入も3000件しか終わってない…どうして…エラー見るとi/o timeoutって書いてあるし、処理が長すぎてセッションがタイムアウトしているのか?
Go Driverのタイムアウトやセッション、MongoDB Server側のパラメータを見直してみたけど改善しませんでした。
解決には至りませんでしたが、これはこれで勉強になったので、調べたドキュメント一覧を載せておきます。
MongoDB Server側でのコネクションの最大数の設定
コネクションプール
Go Driverのタイムアウト設定
- https://pkg.go.dev/go.mongodb.org/mongo-driver@v1.10.3/mongo/options#ClientOptions.SetConnectTimeout
- https://support.mongodb.com/article/000018542
- https://support.mongodb.com/article/000020717
※support.mongodb.comのknowladgeはサインアップしないと見られません。ご了承ください。
ulimitの推奨設定
MongoDB Server側でmongodのセッションが最大で1万件以上張られているのを見つけました。ファイルディスクリプタの上限に達しているかと思い、こちらのパラメータを推奨の値に変更しました。
oplogのサイズ変更
oplogのバッファがあふれたエラー(↓参照)が出ていたため、oplogが足りないことが原因かと思いサイズアップしました。
{ "t": { "$date": "2024-03-21T15:53:25.865+09:00" }, "s": "I", "c": "REPL", "id": 21239, "ctx": "ReplBatcher", "msg": "Oplog buffer has been drained", "attr": { "term": 9 } }
(´゚д゚`) <これだけ色々いじってもダメ…ももも、もしかしてプログラムが悪い?
解決策
プログラミングはご無沙汰だったため、chatGPTを先生にしながらプログラムを書き直すことにしました。書き直したコードがこちらです。
package main import ( "context" "fmt" "github.com/go-faker/faker/v4" "github.com/go-faker/faker/v4/pkg/options" "go.mongodb.org/mongo-driver/mongo" mongoOptions "go.mongodb.org/mongo-driver/mongo/options" ) type Person struct { Username string `custom:"username"` Password string `custom:"password"` Age int `custom:"boundary_start=18,boundary_end=100"` Gender string `custom:"oneof:female, male"` Firstname string `custom:"first_name"` Lastname string `custom:"last_name"` Income int `custom:"boundary_start=200,boundary_end=5000"` } func main() { // MongoDB接続設定 uri := "mongodb://rep1sv.mongo.lab:27017/" DatabaseName := "test" CollectionName := "testcoll" client, err := mongo.Connect(context.TODO(), mongoOptions.Client().ApplyURI(uri)) if err != nil { panic(err) } // コレクションとデータベースの指定 coll := client.Database(DatabaseName).Collection(CollectionName) // fakeデータの作成 var people []interface{} for i := 0; i < 100000; i++ { var person Person err := faker.FakeData(&person, options.WithTagName("custom")) if err != nil { panic(err) } people = append(people, person) } result, err := coll.InsertMany(context.TODO(), people) if err != nil { panic(err) } for _, id := range result.InsertedIDs { fmt.Println(id) } }
元のコードと比較して大きく変更した点は2つです。
- interface型のスライスを宣言し、fakerで作ったデータをappendでそちらに格納した
- データの挿入時に使用する関数をInsertOneからInsertManyにした
恥ずかしながら、chatGPT先生に提案してもらった書き方です。どちらも「そんな書き方が出来るのか!」と感心しました。
50万件のデータ挿入が26秒で終わりました。速い!
kayou@DESKTOP-EUK2PH4:~/go-faker-mongodb/make-data-by-faker$ time go run main.go … ObjectID("66025d6667752c8ba1863905") ObjectID("66025d6667752c8ba1863906") real 0m26.860s user 0m17.716s sys 0m4.857s
MongoDB内のデータを確認してみると、確かに50万件挿入されていることが分かります。
プログラム実行時に、MongoDB Server側でmongodのプロセスのセッション数が最大で1万件以上あるのも気になっていましたが、プログラム改修後は3件程度増加するだけでした。少ないセッション数でデータを流す方が効率が良いことが分かりました。
おわりに
私自身がインフラ&運用畑で生まれ育ったせいか、エラーメッセージを見たときにサーバーサイドのパラメータやGo Driverのパラメータで解決しようとしたことが、今回は裏目に出てしまいました。
思い返してみると、insertManyというMongoDBのクエリが存在すること自体は知っていたので、大量のデータを挿入したい段階でこちらを使っていれば、ここまで酷いプログラムにはならなかったのかもしれません。効率の良いプログラムを書くことの大切さを学びました。
GO言語におけるスライスとinterface型の合わせ技についても知ることが出来て良かったです。MongoDBのようにスキーマレス(個人的にこの表現はあまり好きではありませんが…)なデータを扱うのであれば、かなり相性が良いのではないかと感じました。
蛇足:アイキャッチ画像について
富山にある「おおざわの石仏の森・八百羅漢」です。五百羅漢のほか、実業家の方が知り合いを石像にしまくり、飾ったという、富山県の中でもかなりの珍スポットです。800体近い石像はなかなかに迫力があります。アイキャッチの写真は2018年に自身で撮影したものです。