この記事はユニファAdvent Calendar 2023の18日目の記事です。 adventar.org
こんにちは。 プロダクトエンジニアリング部の船曵です。 布団の中から出たくない季節になってきました。
起床時に暖かい毛布にくるまったまま子供に「おふとんから脱出しよう」とまったく説得力のない声かけをする日々が続いています。
RDSをモニタリングしていると直近のリリース後からWriteIOPSの増加傾向にあり、 バルクインサートで節約可能か簡単な実験を試みました。
今回、書き込みの多いテーブルは目星がついていて、バッチの定期実行である程度まとまったデータを登録するため、 バルクインサートを使えそうだなと思った。。。のが発端なのですが、 バルクインサートIO減りそう、くらいのイメージしかなかったので実際に実験してみました。
RDSのWriteIOPSについて
IOPSは、1秒当たりにディスクが処理できる入出力操作の数を示します。 AWS RDSでは、このIOPSの性能は使用するストレージの容量に依存します。
たとえば、汎用SSDのgp2ストレージでは、1GBあたり3IOPSが提供され、20GBのストレージでは最低100IOPSが保証され、バースト時には最大3000IOPSまで増加します。
今回の実験にはあまり関係ないですが、バーストできる時間も定義されていて、20GBなら1862秒(約30分)、使い切ると54,000秒(約15時間)はバーストできなくなります。
実験
準備
RDSは実験用に最小限の性能のインスタンスを準備しました。
パラメータ | 値 |
---|---|
DBバージョン | Postgresql 15.4 |
インスタンス | t3.micro |
ストレージタイプ | 汎用SSD gp2 |
ストレージ容量 | 20GB |
ここにsample_recordsというシンプルなテーブルを準備します。 データの準備、実行はrailsを使いました。 rails consoleでマイグレーションしてテーブル作成します。
rails generate model SampleRecord name:string data:string rails db:migrate
実験方法
10万件のデータを用いて、通常のインサートとバルクインサートを比較します。
通常のインサート
100000.times do |i| SampleRecord.create(name: "Record #{i}", data: "Data #{i}") end
バルクインサート
# 1000件ずつまとめて100回インサートする 100.times do |i| records = [] 1000.times do |j| records << {name: "Record #{i * j}", data: "Some data #{i * j}", created_at: Time.current, updated_at: Time.current} end SampleRecord.insert_all(records) end
実験結果
最初の矩形になっているエリアがインサートが叩き出したWriteIOPS、その後の小さい三角の一山がバルクインサートのWriteIOPSとなります。 シングルインサートはインスタンスの限界値の100IOPS付近にしばらく張り付いた状態が続いています。対して、バルクインサートは一瞬20IOPSちょっとに上がったのみで、他の平時で発生しているIOとあまり区別がつきません。 バルクインサートによってWriteIOPSを減らせる、という明確な結果が出力されました。
統計情報を比較します。
SELECT * from pg_stat_database where datname = 'demo_development';
Key | Value1 | Value2 |
---|---|---|
xact_commit | 100250 | 126 |
blks_read | 1629 | 1285 |
blks_hit | 449242 | 708328 |
tup_returned | 89232 | 8630 |
tup_fetched | 11668 | 994 |
tup_inserted | 100000 | 100000 |
IOの書き込みに影響を与える要素
chatGPTさんによると
- xact_commit: トランザクションのコミット数。これが多いほど、ディスクへの書き込みが多くなります。
- blks_read と blks_hit: ディスクから読み出されるブロック数とメモリからヒットするブロック数。これらのバランスがIO性能に直接影響します。
- tup_returned と tup_fetched: 返されるタプル(行)の数。これが多いほど、読み書きの負荷が増加します。
とのことでした。 統計情報比較結果からもコミットの量などIOに関係する要素の減少を確認できたので、バルクインサートはIO節約に効果的と言えそうです。
また、グラフからはインサートの処理時間がかかっているように見えます。正確に計測しそびれてしまったのですが、実際に10万レコードのインサートに10分くらいの時間を要しました。バルクインサートは1分程度でした。 これはインスタンスの性能が控えめな分、10万回と100回のオーバーヘッドの差が顕著に可視化された結果とも言えそうですが、逆にバルクインサートのコストの低さを改めて実感できる結果になったかなと感じています。
終わりに
実験結果からバルクインサートによってIOを節約できるという知識を深めることができました。 とはいえ、節約効果が高いからといって必ずしもバルクインサートが適切かというと、それはテーブル設計や実装によって変わります。
例えばトランザクションが必要なケースだと必ずしもバルクインサートが最適解ではないし、どんなインデックスが設計されているかなどによっても変わってきます。そもそもまとまってデータが飛んでくるわけではなく随時書き込みが発生する場合はバルクインサートを使えないので、書き込みの得意なKVSが良いかも?など解決方法は状況によって変わると思います。
日々の運用において、使用しているインフラの性能を理解し、最適な実装を検討することの重要性を再認識しつつ、なんとなくそうかな?と思ったことを実際に試すことで知識として根付くという学びを得ました。
それでは皆さん良いお年をお迎えください。
参考
docs.aws.amazon.com docs.aws.amazon.com www.postgresql.jp
ユニファでは、一緒に働いてくれる仲間を募集しています!
詳細は、以下募集要項からご確認ください。