🕌

GCPの料金削減をしたい

2024/03/19に公開

はじめに

Google Cloud Platform(GCP)はその柔軟性、スケーラビリティ、強力なクラウドサービスのラインナップで、企業や開発者から高い支持を受けています。しかし、少しの油断で予想外の高額請求に直面することもあります。多くの人がGCPの料金に関して、何百万円もの損失を経験した話を聞いたことがあるでしょう。

今回はVoicyで行ったコスト削減の取り組みについて紹介します。

削減の流れ

  • 分析と方針の決定
  • BigQueryの使用料金内訳確認←今回の記事はここまで
  • BigQueryのコスト削減
  • 定期実行頻度の見直し
  • コストのアラートと上限設定
  • 結果
  • 今後の改善に向けて

分析と方針の決定

まず、現在のGCPの使用状況を徹底的に分析しました。どのサービスにどれだけの費用がかかっているか、使用率はどれくらいか、そして不要なリソースがないか等を確認しました。この分析を基に、コスト削減のための具体的な方針を決定しました。

こちらが実際に使用している内訳です。

画像の説明:

青:BigQuery
橙:Cloud Composer
黄:Cloud Storage
その他:…
BigQueryの使用料金が目立って高いことが分かります。全体の約77%がBigQueryに関連するコストです。

これにはいくつかの理由がありますが、主なものは以下の通りです。

  1. Voicyには数百万の会員がおり、非会員のWebユーザーも含めると、アクセスログの数が多くなります。
  2. アプリの各行動に対して詳細にログを取得しているため、一般的なWebサイト分析と比べて1ユーザーあたりのレコード数が数倍になります。
  3. 社内分析やパーソナリティ向けのデータ提供など、定期的に実行される処理が多いです。
  4. SQLの積極的な利用を社内で推奨した結果、長期にわたり高額なSQLクエリが作成されてしまっています。

BigQueryの使用料金内訳確認

BigQueryが主要なコスト要因であることが分かったため、詳細な分析を進めました。Cloud Functions、Cloud Scheduler、Pub/Subを使用して、実際に利用されたリソースをSQL単位で記録しました。

実装コード
Cloud Schedulerを用いて、以下のコードを毎日1回実行し、前日に使用したリソースを把握します。GCPの料金については、公式ドキュメントを参照してください。

main.py
from google.cloud import bigquery
from google.api_core.exceptions import NotFound
import pytz
from datetime import datetime, timedelta, timezone

def hello_world(event, context):
    client = bigquery.Client()
    yesterday = datetime.now(timezone.utc) - timedelta(days=1)
    yesterday_start = datetime(yesterday.year, yesterday.month, yesterday.day, 0, 0, 0, 0, timezone.utc)
    yesterday_end = datetime(yesterday.year, yesterday.month, yesterday.day, 23, 59, 59, 999999, timezone.utc)

    def list_jobs(client):
        return client.list_jobs(max_results=1000000, min_creation_time=yesterday_start, max_creation_time=yesterday_end, all_users=True, state_filter="done")

    records = list_jobs(client)

    def build_records(jobs):
        ja_timezone = pytz.timezone('Asia/Tokyo')
        RATE = 6.25 * 148 #現在のドル円為替を入れてください
        BYTES2TiB = 1099511627776
        mylist = []
        for job in jobs:
            try:
                # クエリジョブであるかどうかをチェック
                if hasattr(job, 'total_bytes_billed') and job.total_bytes_billed is not None and job.total_bytes_billed > 0:
                    alls = [
                        job.user_email,
                        job.job_id,
                        job.total_bytes_billed,
                        job.total_bytes_billed * RATE / BYTES2TiB, #料金設定に応じて設定してください
                        job.created,
                        job.query if hasattr(job, 'query') else 'N/A',
                        job.project
                    ]
                    mylist.append(alls)
            except Exception as e:
                print(f"Error processing job {getattr(job, 'job_id', 'Unknown Job ID')}: {e}")
        return mylist

    df = build_records(records)

    schema = [
        bigquery.SchemaField("user_email", "STRING", mode="NULLABLE"),
        bigquery.SchemaField("jobid", "STRING", mode="NULLABLE"),
        bigquery.SchemaField("total_byte", "INT64", mode="NULLABLE"),
        bigquery.SchemaField("prices", "FLOAT64", mode="NULLABLE"),
        bigquery.SchemaField("time", "TIMESTAMP", mode="NULLABLE"),
        bigquery.SchemaField("query", "STRING", mode="NULLABLE"),
        bigquery.SchemaField("project_id", "STRING", mode="NULLABLE"),
    ]
    dt_string = yesterday_start.strftime("%Y%m%d")
    table_id = "project-id.dataset-name.bigquery_cost_" + dt_string

    try:
        table = client.get_table(table_id)
    except NotFound:
        table = bigquery.Table(table_id, schema=schema)
        table = client.create_table(table)
        print(f"Created table {table_id}")

    # データをバッチで挿入する
    batch_size = 500
    for i in range(0, len(df), batch_size):
        batch = df[i:i+batch_size]
        errors = client.insert_rows(table, batch)
        if errors:
            print(f"Data insertion errors: {errors}")
        else:
            print(f"Batch {i // batch_size + 1} inserted successfully.")
requirements.txt
bigquery
Credentials
datetime
slackweb

実行結果として得られるテーブルは、次のようになります。

このテーブルを基にした分析から、以下のような内訳が明らかになりました。

  • 定期実行処理:80%
  • データチームが想定していなかった長期間のSQL実行:10%
  • その他の通常BIツール利用:10%

一部の長期実行SQLでは、1回あたり数千円のコストが発生していることも分かりました。
この問題の原因は、データの民主化を推進する際にMetabaseというBIツールを導入したことにあります。Metabaseでは、ユーザーごとに実行を追跡できるものの、すべての実行が単一のアカウントに紐づいているため、料金をBIツールの利用ユーザー単位で制限することができませんでした。また、料金の監視を全体的にしか行っていなかったため、この問題が発生しました。

料金の内訳を把握できたため、次の記事では具体的な削減方法について紹介します。

Voicyテックブログ

Discussion