なぜコスト最適化することになったか?
私たちは、株式会社ユーザベース スピーダ事業 Product Division SREチーム として Google Cloud を利用したシステムの構築運用を担っております。
弊社では年々サービスを運用し続けるにつれて、為替の影響も受けてGoogle Cloudの利用料金が増え続けている状況でした。
コスト最適化の必要性が日に日に増してきており、私たちのチームではGoogle Cloudのコスト最適化に取り組むことを決定しました。
以下にこの取り組みの中で、それぞれ対応したことを記載します。
Active Assist
コスト最適化。大事ですよね。Google CloudにはActive Assistに費用という項目があり、Compute EngineやCloud SQLのインスタンスタイプが最適でない場合、適切なインスタンスタイプになるように通知してくれたり、収集された過去と最近の使用状況の指標を分析し、最適なCUDの提案をしてくれます。どこから手をつけたらよいかわからない場合はまずはActive Assistantの費用とパフォーマンスを見ると良いでしょう。現状では以下の内容について提案をしてくれます。
- VM マシンタイプ Recommender
- 確約利用割引 Recommender
- アイドル状態の VM Recommender
- Cloud SQL オーバープロビジョニングされたインスタンス Recommender
ただし、このCloud SQLの通知は割とざっくりしていて全く使っていないインスタンスがあっても通知してくれなかったりするんですよね。
そこで今回は自分でしきい値を設定して、Cloud SQLでリソースが使用されていない場合にはSlack通知をするような仕組みを作っていきたいと思います。
まずはDatadogでGoogle Cloud Integrationをします。Terraformで実施する場合はこんな感じです。
// Service account should have compute.viewer, monitoring.viewer, cloudasset.viewer, and browser roles (the browser role is only required in the default project of the service account). resource "google_service_account" "datadog_integration" { account_id = "datadogintegration" display_name = "Datadog Integration" project = "gcp-project" } // Grant token creator role to the Datadog principal account. resource "google_service_account_iam_member" "sa_iam" { service_account_id = google_service_account.datadog_integration.name role = "roles/iam.serviceAccountTokenCreator" member = format("serviceAccount:%s", datadog_integration_gcp_sts.foo.delegate_account_email) } resource "datadog_integration_gcp_sts" "foo" { client_email = google_service_account.datadog_integration.email host_filters = ["filter_one", "filter_two"] automute = true is_cspm_enabled = false }
次にアラートの条件を設定します。CPUが十分に使用されておらず、メモリも必要以上に確保されているケース、かつインスタンスサイズが一定以上のもののみを検知したいので、今回は以下のような設定を試験的に入れてみました。
- CPU使用率が1週間以上10%以下を推移しているもの
- Memory使用率が1週間以上30%以下を推移しているもの
- Memoryのサイズが6GB以上のもの
以上の条件にすべて合致したインスタンスのみアラート状態となります。その設定はDatadogではcomposite monitorで実装しています。
# main alert resource "datadog_monitor" "cost_alert" { name = "Cost Alert" type = "composite" query = "${datadog_monitor.cpu_utilization.id} && ${datadog_monitor.memory_utilization.id} && ${datadog_monitor.memory_quota.id}" require_full_window = false notify_no_data = false timeout_h = 0 include_tags = false message = "" } # Alert if CPU utilisation does not exceed 10% for more than one month. resource "datadog_monitor" "cpu_utilization" { name = "CPU Utilization" type = "query alert" query = "max(last_1w):max:gcp.cloudsql.database.cpu.utilization{*} by {database_id} < 0.1" monitor_thresholds { critical = 0.1 } require_full_window = false notify_no_data = false timeout_h = 0 evaluation_delay = 660 include_tags = false message = "" } # Alert if Memory utilisation does not exceed 30% for more than one month. resource "datadog_monitor" "memory_utilization" { name = "Memory Utilization" type = "query alert" query = "max(last_1w):max:gcp.cloudsql.database.memory.quota{*} by {database_id} / avg:gcp.cloudsql.database.memory.total_usage{*} by {database_id} < 30" monitor_thresholds { critical = 30 } require_full_window = false notify_no_data = false timeout_h = 0 evaluation_delay = 660 include_tags = false message = "" } # Alert if Instances with 6GB of memory (to deter alerts on smaller instances). resource "datadog_monitor" "memory_quota" { name = "Memory Quota Over 6GB" type = "query alert" query = "max(last_1h):max:gcp.cloudsql.database.memory.quota{*} by {database_id} > 6144000000" monitor_thresholds { critical = 6144000000 } require_full_window = false notify_no_data = false timeout_h = 0 evaluation_delay = 660 include_tags = false message = "" }
自分たちの環境では100台以上のインスタンス中5台がアラートに合致しました。このモニタの良いところは都度SpreadSheetとにらめっこしたり、インスタンスを一台ずつ確認しておかなくてもアラートがSlackに通知され次第対応すれば良いということですね。
evaluationの期間についてはまだ試験的に1週間としていますが、1ヶ月等にしても良いかもなと考えています。
また、今回Datadogを使用しましたがGoogle Cloud純正のMonitoring,Alertの機能でも同じことが実装できそうですね。
Cloud Storage
ストレージサイズの削減
Cloud Storageで発生していた費用の内訳を調べたところ、大部分をデータストレージによる費用が占めていました。
そこで特にデータサイズの大きいバケットをリストアップして中のオブジェクトの確認を行ったところ、定期的に取得しているシステムのバックアップが数年間分残り続けていることが分かりました。ものによっては1回分のバックアップだけでも1TB以上あり、だいぶ無駄に容量を使ってしまっていたので、残しておく必要がある期間より前のバックアップの削除を実施しました。
その結果、全体で約100TBのストレージサイズを削減することができました。
また、今後も不要なバックアップが残り続けないように、一定期間経過したオブジェクトを自動で削除するライフサイクルの設定をしたり、バックアップを行っているジョブの中で古いファイルの削除の処理を入れるなどの対応をしました。
ストレージクラスの変更
Cloud Storageではオブジェクトごとに「Standard」「Nearline「Coldline」「Archive」という4つのストレージクラスを設定することができます。
オブジェクトごとに設定するだけでなく、バケットに対してデフォルトのストレージクラスを設定することも可能です。
以下がストレージクラスの比較表になります。
Standard | Nearline | Coldline | Archive | |
---|---|---|---|---|
可用性 | ・マルチリージョンとデュアルリージョンでは99.99%以上 ・リージョンでは 99.99% |
・マルチリージョンとデュアルリージョンでは99.95% ・リージョンでは99.9% |
・マルチリージョンとデュアルリージョンでは99.95% ・リージョンでは99.9% |
・マルチリージョンとデュアルリージョンでは99.95% ・リージョンでは99.9% |
最小保存期間 | なし | 30 日 | 90 日 | 365 日 |
クラス A オペレーション (1,000 オペレーションあたり) |
$0.005 | $0.01 | $0.02 | $0.05 |
クラス B オペレーション (1,000 オペレーションあたり) |
$0.0004 ~ | $0.001 | $0.01 | $0.05 |
検索料金 | $0/GB | $0.01/GB | $0.02/GB | $0.05/GB |
ストレージ料金 (GB 単位/月) |
$0.023 | $0.016 | $0.006 | $0.0025 |
※ 料金はロケーションがリージョン(asia-northeast1)の場合の2024/09/03時点の料金です
ストレージクラスには最小保存期間が設定されており、その日数が経過する前にオブジェクトの削除、置換、移動を行うと、早期削除料金が発生することがあります。
早期削除料金が発生した場合、オブジェクトが最小期間保存されていた場合と同様の課金がされてしまうので、ストレージクラスを選択するときは最小保存期間の間にオブジェクトの削除、置換、移動が行われるかを考慮する必要があります。
またストレージクラスが表の右側にいくほど、ストレージ料金が安くなる代わりに、検索料金やオペレーション料金が割高になっていきます。オブジェクトに対するアクセスパターンを予測して、ストレージ料金以外の料金も含めてどのストレージクラスが適切かを考えないと、結果的にStandard以上に料金が発生する可能性もあるので注意が必要です。
Autoclassという機能をバケットで有効化すると、各オブジェクトへの実際のアクセスパターンをもとに自動でストレージクラスが移行されるようになるため、オブジェクトへのアクセスパターンが予測できない場合のコスト削減に有用です。
以上を踏まえて、弊社では以下のようにストレージクラスの整理を行いました。
- バックアップのようにアクセス頻度が極端に少なく、長期保存が必要なデータはバックアップの保持期間に応じてNearline, Coldline, Archiveから選択
- 長期保存するものの、アクセスパターンが予測できないバケットはAutoclassを有効化
- それ以外はStandard
ロケーションの検討
Cloud Storageではバケット作成時に設定するロケーションによって、バケット内のオブジェクト データが存在する物理的な場所を制御することができます。
ロケーションには「リージョン」「デュアルリージョン」「マルチリージョン」があり、以下のような違いがあります。
リージョン | デュアルリージョン | マルチリージョン | |
---|---|---|---|
データの配置 | 特定のリージョン 例:asia-northeast1(東京) |
特定の二つのリージョン 例:asia1(東京・大阪) |
2 つ以上の地理的な場所を含む広い地理的なエリア 例:asia(アジア圏複数リージョン) |
冗長性 | アベイラビリティゾーン間のデータ冗長性(同期) | リージョン間のデータの冗長性(非同期) | リージョン間のデータの冗長性(非同期) |
パフォーマンス | 200 Gbps | 200 Gbps | 50 Gbps |
ストレージ料金 (GB 単位/月) |
$0.023 | $0.0506 | $0.026 |
レプリケーション料金(1 GB あたり) | なし | $0.08 | $0.08 |
※ 料金はストレージクラスがStandardで、ロケーションがそれぞれasia-northeast1、asia1、asiaの場合の2024/09/03時点の料金です
どのロケーションを選んだ場合でもデータの耐久性は99.999999999%(イレブンナイン)の年間耐久性が保証されています。しかし、ロケーションをデュアルリージョンやマルチリージョンにすることで、リージョンレベルの障害が発生した場合でもダウンタイムを0にすることができるようになるため、可用性を向上させることができます。
料金に注目すると、デュアルリージョンは他のロケーションに比べてストレージ料金が約2倍になります。また、デュアルリージョン、マルチリージョンの場合はデータのレプリケーション料金が追加で発生するのに注意が必要です。
ロケーションを決める際には求められる可用性、パフォーマンス、料金、データ保持ポリシーなどを考慮して検討することになります。
私たちの場合は複数のバケットがデュアルリージョンで作成されており、リージョンに変更することを検討しましたが、削減されるコストと、リージョンに変更したことで失われる可用性を比較した結果、従来通りデュアルリージョンのままにすることにしました。
結果
Cloud Storage全体の料金としては、40%ほど削減することができました。
Cloud Storageのコスト最適化の取り組みで特に効果が大きかったのは不要なバックアップの削除でした。皆さんも一度Cloud Storageに保管してあるデータを見直してみると、大幅にコスト削減できる余地が見つかるかもしれません。
Cloud SQL
コスト削減のため、Dev環境の停止を行なった
弊社の組織では、開発チームごとによって稼働時間が異なるものの概ね 8:00 ~ 19:00が業務時間となっていたため、それ以外の時間と休日はDev環境を利用していない状況となってました。
そのため、利用してない時間にCloud SQLを停止することでコスト削減を実施しようと考えました。単純計算として、停止時間が平日13時間(19:00~翌8:00)・土日48時間(終日)となり、計61時間を停止することを決めました。
実現方法として、以下の記事を参考にしました。
構成としては以下となっております。
基本的にはブログ記事の通りに構築することでこの仕組みを利用することができました。Cloud Schedulerで起動 or 停止の内容を含むリクエストをPub/Subに送信し、裏側のCloud Run内で起動 or 停止 の処理を実行するという流れとなっています。
多くのDBを一度に停止 or 起動をしようとした場合に、Cloud Runから Cloud SQLの起動するためのAPIを叩く処理でエラーが返ることが時々発生したため、自前でそこの処理にRetryを追加しました。
不要なリソースの削除
DBリソースの整理をしていく中で、以前開発はしてたが現在およびしばらくは開発しないDev環境のDBなどがいくつかでてきました。
DB内に含まれるデータ自体についても削除されても大丈夫ということが確認できたことと、基本的にTerraformによるコード管理がされており、起動したい場合は過去コードから起動を簡単にできると判断し、DB自体の削除を実施しました。
結果50%くらい削減することができた
上記の2つのを実施することで、Dev環境のCloud SQLを削減施策実施前から50%安く利用できるようになりました。
Dev環境であれば利用してない時間であったり、すでに使われてないものの整理をしていくだけで、簡単にコスト削減を実施することできました!
テスト環境の削除
テスト環境が起動しっぱなし問題
Compute Engine の料金を最適化できないかと考え調査していると、テスト環境がテスト終了後も残り続けていることを発見しました。
Pod数を確認してみると、Dev環境において約30%ほどのPodがテスト環境用のアプリケーションとなっており、このリソースを利用する時だけ稼働させるだけでもコスト最適化を実現できると考えました。
テスト環境を削除した
テスト環境を調べていると、直近開発はしていないアプリケーションだが、テスト環境だけは残っているものが多く存在していたため、ひとまず現状のテスト環境を全て削除する対応を実施しました。
その結果、CPUリソースは約30%ほど削除され、Memoryリソースは約20%ほど削除することができました。
しかし、このままではテスト実行をするたびに再度テスト環境が残り続けてしまうため、開発チームに対してCICDの中にテスト完了後テスト環境を削除する処理を追加するように依頼しました。
不要なテスト環境は削除していこうという啓蒙
単純な依頼だけでも良いのですが、削除漏れなども検知して今後不要なテスト環境が減りやすい仕組みを作っていきたいと思い、10日以上稼働しているテスト環境が存在していた場合通知するようにしました。
基本的に月曜日の朝にチームメンバーがSlackをみることが多いと思い、月曜日朝に通知する制御を実施しました。
通知にはPrometheusを利用しているため、以下のようなクエリで通知設定をしています。弊社ではテスト環境の場合 K8sのnamespaceにtestという文字列を入れているため以下のようなクエリで起動時間をみるようにしています。
max by(namespace, cluster) ((time() - kube_pod_start_time{namespace=~".*test.*"}) > 864000) and on() (day_of_week() == 1 and hour() == 0)
and 以降の on() (day_of_week() == 1 and hour() == 0)
という設定は、月曜日の朝にのみ通知を受け取りたかったため、その制御としてのクエリとなっています。
結果
結果として、テスト環境のリソースを削除することでNodeの数をいくつか削除することができました。今回のように一つ一つのリソース量は大したことないが、数が多いものに関しては組織全体として皆で取り組んでいくことで組織としてのコストへの意識を高めつつ、対応を進めることができたと感じています。
BigQuery
BigQueryのコスト削減について調べていくとストレージ課金モデルによって金額が大きく変わるという記事を見つけました。どうやらStorageにはPHYSICALとLOGICALというものがあり、PHYSICALはLOGICALよりもStorage費用が二倍高いものの圧縮が効かせられるようです。圧縮率によってどちらのモデルを選択したほうが良いか決まるので、まずは圧縮率を事前に調査してみました。
LOGICAL | PHYSICAL | |
---|---|---|
Active Storage費用(USD / GB month) | 0.02 | 0.04 |
Long term Storage費用(USD / GB month) | 0.01 | 0.02 |
データの圧縮 | 無し | 有り |
Time travel Storageに対する費用 | 無し | 有り |
圧縮率は公式のクエリを叩けば自動で算出されます。
DECLARE active_logical_gib_price FLOAT64 DEFAULT 0.02; DECLARE long_term_logical_gib_price FLOAT64 DEFAULT 0.01; DECLARE active_physical_gib_price FLOAT64 DEFAULT 0.04; DECLARE long_term_physical_gib_price FLOAT64 DEFAULT 0.02; WITH storage_sizes AS ( SELECT table_schema AS dataset_name, -- Logical SUM(IF(deleted=false, active_logical_bytes, 0)) / power(1024, 3) AS active_logical_gib, SUM(IF(deleted=false, long_term_logical_bytes, 0)) / power(1024, 3) AS long_term_logical_gib, -- Physical SUM(active_physical_bytes) / power(1024, 3) AS active_physical_gib, SUM(active_physical_bytes - time_travel_physical_bytes) / power(1024, 3) AS active_no_tt_physical_gib, SUM(long_term_physical_bytes) / power(1024, 3) AS long_term_physical_gib, -- Restorable previously deleted physical SUM(time_travel_physical_bytes) / power(1024, 3) AS time_travel_physical_gib, SUM(fail_safe_physical_bytes) / power(1024, 3) AS fail_safe_physical_gib, FROM `region-asia-northeast1`.INFORMATION_SCHEMA.TABLE_STORAGE_BY_PROJECT WHERE total_physical_bytes + fail_safe_physical_bytes > 0 -- Base the forecast on base tables only for highest precision results AND table_type = 'BASE TABLE' GROUP BY 1 ) SELECT dataset_name, -- Logical ROUND(active_logical_gib, 2) AS active_logical_gib, ROUND(long_term_logical_gib, 2) AS long_term_logical_gib, -- Physical ROUND(active_physical_gib, 2) AS active_physical_gib, ROUND(long_term_physical_gib, 2) AS long_term_physical_gib, ROUND(time_travel_physical_gib, 2) AS time_travel_physical_gib, ROUND(fail_safe_physical_gib, 2) AS fail_safe_physical_gib, -- Compression ratio ROUND(SAFE_DIVIDE(active_logical_gib, active_no_tt_physical_gib), 2) AS active_compression_ratio, ROUND(SAFE_DIVIDE(long_term_logical_gib, long_term_physical_gib), 2) AS long_term_compression_ratio, -- Forecast costs logical ROUND(active_logical_gib * active_logical_gib_price, 2) AS forecast_active_logical_cost, ROUND(long_term_logical_gib * long_term_logical_gib_price, 2) AS forecast_long_term_logical_cost, -- Forecast costs physical ROUND((active_no_tt_physical_gib + time_travel_physical_gib + fail_safe_physical_gib) * active_physical_gib_price, 2) AS forecast_active_physical_cost, ROUND(long_term_physical_gib * long_term_physical_gib_price, 2) AS forecast_long_term_physical_cost, -- Forecast costs total ROUND(((active_logical_gib * active_logical_gib_price) + (long_term_logical_gib * long_term_logical_gib_price)) - (((active_no_tt_physical_gib + time_travel_physical_gib + fail_safe_physical_gib) * active_physical_gib_price) + (long_term_physical_gib * long_term_physical_gib_price)), 2) AS forecast_total_cost_difference FROM storage_sizes ORDER BY (forecast_active_logical_cost + forecast_active_physical_cost) DESC;
クエリを実行した結果はこちらです。
注目すべき点は active_compression_ratio
とlong_term_compression_ratio
で、この値が2倍以上であればphysical storageというデータを圧縮することで費用を下げることができます。
どちらのdatasetも2を超えているのでPhysical Storageに変更したほうが良いですね。
dataset_name | active_logical_gib | long_term_logical_gib | active_physical_gib | long_term_physical_gib | time_travel_physical_gib | fail_safe_physical_gib | active_compression_ratio | long_term_compression_ratio | forecast_active_logical_cost | forecast_long_term_logical_cost | forecast_active_physical_cost | forecast_long_term_physical_cost | forecast_total_cost_difference | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
sample1 | 136954.5 | 10816.83 | 15260.71 | 901.21 | 0 | 0 | 8.97 | 12 | 2739.09 | 108.17 | 610.43 | 18.02 | 2218.81 | |
sample2 | 892.72 | 10231.32 | 20.15 | 227.28 | 0 | 0 | 44.3 | 45.02 | 17.85 | 102.31 | 0.81 | 4.55 | 114.82 |
Physical Storageに変更するには以下のコマンドで実施できます。
※Physical StorageからLogical Storageに戻すこともできますが、一旦変更すると14日間は元に戻せないので注意が必要です。
bq update -d --storage_billing_model=PHYSICAL <project-id>:<dataset-name>
すべてのdatasetに調査と料金モデルの変更を行ったところ、費用が50%も安くなりました。大量のデータをBigQueryに格納している方には是非おすすめの施策となっております。
CUDの購入
Google CloudにはCUD(確定利用割引)という仕組みがあり、一定期間決められたリソースを使用することのコミットメントによって料金の割引を得ることができます。
今回のコスト最適化の取り組みでは以下のリソースベースのCUDを新しく購入しました。
Compute Engine
Compute EngineのCUDの購入はマシンタイプを選択して、vCPUとメモリのリソース量を指定して購入することになります。
まずはCUDの購入の前にインスタンスサイズの適正化や、マシンタイプのN1からN2Dへの変更などを行いました。
その後、従来購入していたCUDに加えて、コミットメント量よりも多く使っている分のN2DのCUDを追加で購入しました。
Bigtable
BigtableのCUDはノードのコンピューティングリソースの1時間あたりの費用に対してコミットメントする形になります。
弊社ではBigtableを5ノード分使用しており、そのうち確実に今後も利用し続けることが見込まれる4ノード分のCUDを購入しました。
まとめ
今回、円安による費用が上昇したことを受けて、コスト最適化に取り組んでみました。
現状の費用分析から効率の良い部分への対応を進めてきたのですが、実際に調査してみると ”なんでこんなにかかってるんだっけ?” や “これって必要だっけ?” といった知らないクラウドサービスの課金形態や不必要なリソースがかなりありました。
結果として18%近くのGoogle Cloud費用を削減することができたのですが、その大部分は不要なリソースの整理などの地道な作業が多かったです。
不要に費用が膨らむことを減らすためにも、定期的に費用分析をすることはとても重要だと学びました。