こんにちは。メルカリ Accounting Productsチーム Software Engineerのayanekoです。
この記事は、Mercari Advent Calendar 2023 の20日目の記事です。
私たちAccounting Productsチームは会計システムの開発、運用をしています。会計データを扱うという特性上、以下にあげる理由から大量のデータを保持しており、多額の費用がかかっていました。
- 会計データは法律上一定期間の保持が必要であること
- 一時ファイルやログファイルなども含めて保守的にすべてのデータを保存していたこと
そこで、FinOps観点でCloud Storage(以下GCS)やCloud Spanner(以下Spanner)のリソース最適化のPJを始めました。リソース最適化とは、必要なリソースはしっかりと保存し、更新され古くなったデータは必要な期間のみ保存してデータの総量から余剰分を取り除けるようにする取り組みのことです。
この投稿では、その一環として行ったGCSのリソース最適化の取り組みで得た知見についてご紹介したいと思います。
利用環境
本題に入る前に、私たちが普段利用している環境について少し触れておきたいと思います。
- Dev環境
- 開発環境
- QA環境
- テスト環境(ステージング環境の扱いに近い)
- Prod環境
- 本番環境
システムに変更を加える際は、Dev環境、QA環境の順に検証し、最終的にProd環境へ適用します。
また、GCPのリソースはほぼすべてTerraformで管理しています。
以上のことを踏まえて本題に入りたいと思います。
オブジェクトのバージョニングを有効にするときは適切なライフサイクルを設定する
今回リソース最適化をしたいバケットは最初からオブジェクトのバージョニングが有効の状態でしたが、ライフサイクルの設定がされておらず大量のオブジェクトが保存され、多額の費用がかかっていました。
バケットのバージョニングを有効にするとライブオブジェクトバージョンを置換または削除するたびに非現行オブジェクトバージョンが保持されるようになるため、非現行オブジェクトバージョンをどの程度保持するかをライフサイクルにより管理することが重要になってきます。
そこで、特定の日数が経過後に非現行バージョンのオブジェクトを削除するライフサイクルの設定をすることで、本当に保持しなければならないオブジェクトのみが残るようにしました。
オブジェクトを削除するときは量やタイミングに注意する
ライフサイクルの設定を適用し大きなコスト削減につながると喜んだのもつかの間、この対応の直後に大きな問題が発生しました。これにはオブジェクト削除の量やタイミングが関係していることがわかりました。
ライフサイクルにより一度にPB単位のオブジェクトが削除されることとなったのですが、それが引き金となって同バケットのDeleteObjectやRewriteObject.FromがUnavailableを返すようになるという問題が発生しました。
社内の有識者とともにいろいろ調査を尽くし結果的に1週間後に問題は解消しましたが、この経験からあまりにも大量のオブジェクトを一度に削除することは今後は避けるべきという教訓を得ました。
さらに、一時的にコストが跳ね上がっていたことに気が付きました。
削除されたオブジェクトが保存されていたバケットのストレージクラスがArchiveストレージであったために、多くのオブジェクトに対して早期削除料金がかかっていることがわかりました。
各ストレージクラスには最小保存期間が設定されており、Archiveストレージの場合は365日です。
最小保存期間が経過していないオブジェクトに対して削除、置換、移動をした場合は早期削除料金がかかってしまうのです。
(上記の内容は2023年11月時点のもので、将来ストレージクラスの種類や最小保存期間が変更になる可能性があります)
一時的にコストがかかってしまうことは仕方ないとしても事前に予測することは可能であったため、そこまで考えが至らなかったことは反省すべき点でした。
Rewrite時のストレージクラスの違いによる影響を考慮する
バケットから別のバケットへRewriteが行われる場合には、両者のストレージクラスの違いに注意したほうが良いということがわかりました。
会計システムの一部でAirflowを使ってデータをExportしている処理があり、その中の一時ファイル用のバケットとデータの保存先のバケットを別々にする対応をしました。
QA環境での実行では問題がなかったのですが、Prod環境での実行で一時ファイル用のバケットからデータの保存先のバケットへRewriteが行われている箇所で処理が失敗していました。
このとき一時ファイル用のバケットがStandardストレージ、データの保存先のバケットがArchiveストレージであり、両者のストレージクラスが異なっている状態でした。
また、問題なく動いたQA環境とProd環境の違いとして、扱うデータ量がProd環境の方がかなり多いという点があげられます。
そこで一時ファイル用のバケットとデータの保存先のバケットのストレージクラスを、両者とも同じStandardストレージにしました。
そうするすることでProd環境でも問題なく処理が完了することがわかりました。
Cloud Storage JSON APIのRewrite methodのリファレンスに記載されている注意点として、Rewrite元とRewrite先のバケットのロケーションとストレージクラスが同じ場合は1回のリクエストでRewriteが完了するとの記載があります。
このことから、ロケーションやストレージクラスの違いがRewriteの処理に影響するということが推測できます。
バケットは種類ごとに分けて管理する
今回のリソース最適化の対象のバケットには、いくつもの異なる保持ポリシーのオブジェクトが一緒くたに保存されていたことも最適化までの道のりを困難にした要因の一つでした。
たとえば「オブジェクトを削除するときは量やタイミングに注意する」で発生した問題のさなかにも、削除対象外のオブジェクトにもかかわらず同じバケットにあるというだけで影響を受けてしまうということがありました。
本来オブジェクトの種類によって選択すべきストレージクラスや設定すべきライフサイクルは異なるため、保存期間やアクセスの頻度などを考慮しバケットを分けたほうが扱いやすいです。
たとえば保存期間が2年であり頻繁にアクセスすることがないオブジェクトの場合は、1日経過後にストレージクラスをArchiveストレージにするライフサイクルと、2年経過したオブジェクトを削除するライフサイクルをバケットに設定します。また保存期間が1日のオブジェクトの場合は1日経過したオブジェクトを削除するライフサイクルをバケットに設定します。そのため両者のバケットは別の方が扱いやすいです。
デフォルトストレージクラスはStandardにし、他のストレージクラスへの変更は基本的にはライフサイクルで行う構成は、Merpay社員かつGoogle Developers Expertでもある@sinmetalさんからのアドバイスと、今回の取り組みを通しての私自身の見解としても、この方法が理に適っていると実感しています。
オブジェクトをバケットにアップロードすると、明示的に設定しない限りそのオブジェクトにはバケットのデフォルトのストレージ クラスが割り当てられます。
オブジェクトのアップロード後にオブジェクトのストレージクラスを変更したい場合は、ライフサイクルによる変更や、オブジェクトの書き換えによる変更などの方法があります。
従ってデフォルトストレージクラスがArchiveストレージの場合オブジェクトがアップロードされると即座にArchiveストレージになるため、たとえば以下のような難点があります。
- システム修正後の動作確認でシステムからExportされたオブジェクトの中身を見たい場合にオペレーション料金が高い
- 誤って不要なオブジェクトをバケットに保存してしまい削除をしたい場合に早期削除料金がかる
このようなコスト面での難点を回避するため、Standardストレージ以外のストレージクラスの設定は基本的にライフサイクルで行っています。
(上記の内容は2023年11月時点のもので、将来ストレージクラスの種類やオペレーション料金が変更になる可能性があります)
バケットを目的ごとに分けた後は、バケットごとにラベルを設定すると請求を確認する際にもバケットごとに把握することが可能になります。
ラベルはKeyValue形式で、メルカリではbucket={$bucket-name}
の形式でラベルを設定しています。
ラベルを設定することで、たとえば早期削除料金が発生しているバケットを容易に特定できるようになります。
ポリシーに基づき運用をする
目的に合わせてバケットの作成やライフサイクルの設定をするにあたり、どのデータをどのくらいの期間保持する必要があるのかという基準を定めたドキュメントであるデータの保持ポリシーを作成しました。
私たちは会計データを扱うため、そのデータが会計帳簿保存の対象となるデータかどうかの判断が必要になってきます。
その判断をするにあたり、内部監査、経理、外部監査法人と協議しながらポリシーを作成しました。
たとえばSpannerの特定の日のバックアップは何年保存する必要がある、それ以外は何年保存する必要がある、というように、データの種類ごとに保存すべき期間を定めていきます。
このような基準に沿った運用ができるようバケットの作成やライフサイクルの設定をしていきます。
このポリシーを作成する際にバケット内にあるオブジェクトを一覧化するために活用した機能として、Storage Insights のインベントリ レポートというものがあります。
Storage Insights のインベントリ レポートにはオブジェクトのストレージクラスなどのオブジェクトに関するメタデータ情報が含まれています。
今回はこのインベントリレポートをBigQueryに取り込みました。
ライフサイクルの設定だけでカバーできない不要なオブジェクトの削除の際には、削除対象のオブジェクトをクエリにて抽出し、その情報を元にスクリプトでオブジェクトを削除しました。
おわりに
リソース最適化前から最適化後を比較すると、おおよそ54%ものコストを削減することができました。
この取り組みを始めた時点ではGCSに関しての知識が不足していたこともあり多くの問題に直面しましたが、問題を1つ1つ解決していく中でGCSやその周辺に関する知識を深めることができ、得るものが大きかったと感じています。
またリソースを目的ごとに最適な状態で管理することの大切さを実感し、そのコストのインパクトの大きさをひしひしと感じられた取り組みでもありました。
今後は今回のリソース最適化の取り組みの対象外だった部分も含めてコストを削減できる余地がないかどうか、継続的に見直しを行っていきたいと思います。
Accounting Productsチームでは、メルカリのミッション・バリューに共感できるSoftware Engineerを募集しています。一緒に働ける仲間をお待ちしております!
採用情報
明日の記事はpakioさんです。引き続きお楽しみください。