case-kの備忘録

日々の備忘録です。データ分析とか基盤系に興味あります。

データ基盤におけるBigQuery Flex Slots導入メリットとオンデマンド自動切り替えの必要性

本記事はZOZOテクノロジーズ #1 Advent Calendar 2020 - Qiita 22日目の記事です。
Flex Slotsの概要や導入のメリット、データ基盤における活用用途をご紹介できればと思います。
また、Flex Slotsの購入が失敗した際にオンデマンドに自動で切り替える必要性についても合わせてご紹介できればと思います。

Flex Slotsの概要と導入メリット

BigQueryの料金モデルにはオンデマンド料金と定額料金の2種類あります。
オンデマンドはクエリでスキャンしたデータに対して課金されるモデルで、定額料金は事前に専用のクエリ処理容量を購入します。

オンデマンドは従量課金です。コストメリットがある一方、パフォーマンス面で不安定になります。
Googleが管理するBigQueryのスロットが枯渇すると2000スロット以上使えなくなるような制限があります。重たい処理でマシンパワーが必要ときスロットが使えないことで集計処理が遅延します。また不用意に重たいクエリを投げて課金されてしまうこともあるんじゃないかと思います。

定額プランは安定的なパフォーマンス提供する一方、柔軟性がなくコストメリットがありませんでした。
年間契約と月次契約しかなく、使われた分だけ課金するオンデマンドと比べてBigQueryのコストが高くついてしまいました。
常に高いパフォーマンスは必要なくデータマートなどパフォーマンスが求められるタイミングでのみ使いたかったためです。

Flex Slotsが登場したことでこれまで1ヶ月が購入の最小単位でしたが、60秒単位でBigQueryのスロットが購入できるようになりました。
データ基盤における活用用途としては重い処理の前にスロットを購入し、処理が終わったらスロットを破棄するような使い方があります。費用としては500 スロット1分あたり$0.33です(1時間$20)
cloud.google.com
tech.plaid.co.jp

構成要素について

定額プランの構成要素である、「コミットメント」、「リザベーション」、「アサイメント」について簡単にご紹介できればと思います。

  • コミットメント

コミットメントはBigQueryのコンピューティング容量の購入です。スロットを購入し割り当てることでBigQueryのパフォーマンスを高めることができます。

  • リザベーション(予約)

リザベーションを使うことで組織にあったスロットの割り当てが可能です。
例えば月次契約で2000スロット購入して、重い処理の前にFlex Slotsで8000スロット購入したとします。
その場合10000スロットをリザベーションし、重たいデータ集計を行うプロジェクトに割り当てることが可能です。

アサイメントではどのプロジェクトに、どの程度スロットを割り振るか決めます。組織内のどのプロジェクトにどのくらいスロットを割り当てるか定義することができます。
f:id:casekblog:20201220221046p:plain
cloud.google.com

ワークフローの概要

先ほど書いたように重たい処理の前(データマートの集計など)にFlex Slotsを購入し、処理が終わったら購入したスロットを破棄するようにしています。
大まかに次のようなワークフローを定義してます。

+bigquery_flex_slots_up: # flex slots buying

+bigquery_datamart_job: # datamart task 

+bigquery_flex_slots_down: # flex slots removement

オンデマンド自動切り替えの必要性

Flex Slotsですが1~2ヶ月に1度購入に失敗します。
購入に失敗するとワークフローが止まってしまうので、購入失敗時は自動でオンデマンドに切り替えるようにして運用してます。

ワークフロー(スロットの購入)

スロットが必要な重い処理の前にスロット数をあげてます。月次契約で2000スロット購入してます。Flex Slotsで7000スロット購入(コミットメント)し、9000スロットを予約(リザベーション)してます。具体的には次のようなdigファイルを作ってます。コンテナイメージをPULLしてタスクを実行してます。
(実行してるタスクの詳細は次章で書きます)

+bigquery_flex_slots_up:
  +bigquery_flex_slots_reserve_assignment:
    _retry: 3
    _env:
      GCP_CREDENTIAL: ${secret:gcp.credential}
    _export:
      docker:
        image: ${docker_cloudsdk.image}
        pull_always: ${docker_cloudsdk.pull_always}
    sh>: tasks/bigquery_flex_slots_reserve_assignment.sh
  +bigquery_flex_slots_commitment:
    _retry: 3
    _env:
      GCP_CREDENTIAL: ${secret:gcp.credential}
    _export:
      docker:
        image: ${docker_cloudsdk.image}
        pull_always: ${docker_cloudsdk.pull_always}
    sh>: tasks/bigquery_flex_slots_commitment.sh 7000
  +bigquery_flex_slots_verification:
    _retry: 3
    _env:
      GCP_CREDENTIAL: ${secret:gcp.credential}
    _export:
      docker:
        image: ${docker_cloudsdk.image}
        pull_always: ${docker_cloudsdk.pull_always}
    sh>: tasks/bigquery_flex_slots_verification.sh
  +bigquery_flex_slots_reservation:
    _retry: 3
    _env:
      GCP_CREDENTIAL: ${secret:gcp.credential}
    _export:
      docker:
        image: ${docker_cloudsdk.image}
        pull_always: ${docker_cloudsdk.pull_always}
    sh>: tasks/bigquery_flex_slots_reservation.sh batch 9000

ワークフロー(スロットの破棄)

重たい処理が終わったらスロットを下げてます。予約した9000スロットから月次契約の2000スロットに戻してます。

+bigquery_flex_slots_down:
  +bigquery_flex_slots_reservation:
    _retry: 3
    _env:
      GCP_CREDENTIAL: ${secret:gcp.credential}
    _export:
      docker:
        image: ${docker_cloudsdk.image}
        pull_always: ${docker_cloudsdk.pull_always}
    sh>: tasks/bigquery_flex_slots_reservation.sh batch 2000
  +bigquery_flex_slots_removement:
    _retry: 3
    _env:
      GCP_CREDENTIAL: ${secret:gcp.credential}
    _export:
      docker:
        image: ${docker_cloudsdk.image}
        pull_always: ${docker_cloudsdk.pull_always}
    sh>: tasks/bigquery_flex_slots_removement.sh
  +bigquery_flex_slots_reserve_assignment:
    _retry: 3
    _env:
      GCP_CREDENTIAL: ${secret:gcp.credential}
    _export:
      docker:
        image: ${docker_cloudsdk.image}
        pull_always: ${docker_cloudsdk.pull_always}
    sh>: tasks/bigquery_flex_slots_reserve_assignment.sh

ワークフロータスク詳細

Digdagワークフローで実行しているタスクの詳細についてご紹介できればと思います。

スロット購入時のタスク

  • bigquery_flex_slots_reserve_assignment.sh

アサイメントを行いスロットを割り当てるプロジェクトを決めます。Digdagから環境変数に入ったサービスアカウントキーを渡してコンテナ内で認証を行っています。

#!/bin/bash
admin_project_id=$ADMIN_PROJECT_ID

# authorization
echo $GCP_CREDENTIAL > GCP_CREDENTIAL.json
gcloud auth activate-service-account --key-file=GCP_CREDENTIAL.json
export BIGQUERYRC=/root/.bigqueryrc

# reservation_assignment
assignment=$(bash tasks/bigquery_flex_slots_assignment_status.sh)
if [ -z "$assignment" ]; then
  bq mk --reservation_assignment --project_id=${admin_project_id} --assignee_id=<project_id> --location=US --assignee_type=PROJECT  --job_type=QUERY --reservation_id=${admin_project_id}:US.batch
  bash tasks/bigquery_flex_slots_alert_notice.sh 2
fi
  • bigquery_flex_slots_assignment_status.sh

アサイメントの状態を取得してます。アサイメントは1度登録すればいいのですがオンデマンド自動切り替え時にアサイメントを削除する必要があります。

#!/bin/bash
admin_project_id=$ADMIN_PROJECT_ID

# authorization
echo $GCP_CREDENTIAL > GCP_CREDENTIAL.json
gcloud auth activate-service-account --key-file=GCP_CREDENTIAL.json
export BIGQUERYRC=/root/.bigqueryrc

# flex slots status
bq ls --project_id=${admin_project_id} --location=US --reservation_assignment  | sed 's/No reservation assignments found.//g'
  • bigquery_flex_slots_commitment.sh

Flex Slotsの購入を行います。購入したスロットはリザベーションすることでアサイメント対象のプロジェクトに割り当てることができます。

#!/bin/bash
admin_project_id=$ADMIN_PROJECT_ID
slots=$1

# authorization
echo $GCP_CREDENTIAL > GCP_CREDENTIAL.json
gcloud auth activate-service-account --key-file=GCP_CREDENTIAL.json
export BIGQUERYRC=/root/.bigqueryrc

# commitment
bq mk --project_id=${admin_project_id} --location=US --capacity_commitment --plan=FLEX --slots=${slots}
  • bigquery_flex_slots_verification.sh

Flex Slotsが正常に購入できたか確認してます。購入できなかった場合stateはPENDINGとなっています。

                     name                      slotCount   plan   renewalPlan    state    commitmentEndTime  
 -------------------------------------------- ----------- ------ ------------- --------- ------------------- 
  <admin-project>:US.<id>  7000        FLEX                 PENDING     

購入できなかった場合はオンデマンドに切り替えます。オンデマンドに切り替えるためにアサイメントを削除し、お金がかからないようPENDING状態のスロットを削除します。

#!/bin/bash
admin_project_id=$ADMIN_PROJECT_ID

# authorization
echo $GCP_CREDENTIAL > GCP_CREDENTIAL.json
gcloud auth activate-service-account --key-file=GCP_CREDENTIAL.json
export BIGQUERYRC=/root/.bigqueryrc

# verification
flex_slots_status=$(bq ls --capacity_commitment --location US --format prettyjson --project_id=${admin_project_id} | jq 'map(select(.["plan"] | startswith("FLEX"))) | .[] | .state | split("/") | .[0]'| sed 's/"//g')
if [ $flex_slots_status = "PENDING" ]; then
  # change plan from flex slots to ondemand
  bash tasks/bigquery_flex_slots_removement.sh
  bash tasks/bigquery_flex_slots_remove_assignment.sh
  bash tasks/bigquery_flex_slots_alert_notice.sh 1
fi
  • bigquery_flex_slots_remove_assignment.sh

定額からオンデマンドに切り替えるためにアサイメントを削除します。アサイメントが削除されるとオンデマンドに切り替わります。

#!/bin/bash
admin_project_id=$ADMIN_PROJECT_ID

# authorization
echo $GCP_CREDENTIAL > GCP_CREDENTIAL.json
gcloud auth activate-service-account --key-file=GCP_CREDENTIAL.json
export BIGQUERYRC=/root/.bigqueryrc

# removement
# --format prettyjsonオプションを使うと値が変わる
# bq ls --project_id=<admin-project>--location=US --reservation_assignment
assignment=$(bq ls --project_id=${admin_project_id} --location=US --reservation_assignment --format prettyjson | jq 'map(select(.["assignee"] | startswith("projects/<project_id>"))) | .[] | .name'| sed 's/projects//g' | sed 's/locations/:/g' | sed 's/reservations/./g' | sed 's/assignments/./g'  | sed 's/\///g' | sed 's/"//g')
bq rm --project_id=${admin_project_id} --location=US --reservation_assignment $assignment
  • bigquery_flex_slots_reservation.sh

リザベーション処理です。重い処理を行う際月次契約2000スロット+ Flex Slotsで購入した7000スロットを足して9000スロットをリザベーションしてます。
重い処理が終わったタイミングで2000スロットに戻してます。

#!/bin/bash
admin_project_id=$ADMIN_PROJECT_ID
reservation=$1
assignment_project_slot=$2

# authorization
echo $GCP_CREDENTIAL > GCP_CREDENTIAL.json
gcloud auth activate-service-account --key-file=GCP_CREDENTIAL.json
export BIGQUERYRC=/root/.bigqueryrc

# reservation
assignment=$(bash tasks/bigquery_flex_slots_assignment_status.sh)
if [ -n "$assignment" ]; then
  bq update --project_id=${admin_project_id} --location=US --slots=${assignment_project_slot} --reservation ${reservation}
fi

スロット破棄時のタスク

  • bigquery_flex_slots_reservation.sh

リザベーション処理です。重い処理が終わったタイミングで2000スロットに戻してます。

#!/bin/bash
admin_project_id=$ADMIN_PROJECT_ID
reservation=$1
assignment_project_slot=$2

# authorization
echo $GCP_CREDENTIAL > GCP_CREDENTIAL.json
gcloud auth activate-service-account --key-file=GCP_CREDENTIAL.json
export BIGQUERYRC=/root/.bigqueryrc

# reservation
assignment=$(bash tasks/bigquery_flex_slots_assignment_status.sh)
if [ -n "$assignment" ]; then
  bq update --project_id=${admin_project_id} --location=US --slots=${assignment_project_slot} --reservation ${reservation}
fi
  • bigquery_flex_slots_removement.sh

購入したFlex Slotsを破棄しています。破棄することでFlex Slots購入分の課金がとまります。

#!/bin/bash
admin_project_id=$ADMIN_PROJECT_ID

# authorization
echo $GCP_CREDENTIAL > GCP_CREDENTIAL.json
gcloud auth activate-service-account --key-file=GCP_CREDENTIAL.json
export BIGQUERYRC=/root/.bigqueryrc

# removement
assignment=$(bash tasks/bigquery_flex_slots_assignment_status.sh)
if [ -n "$assignment" ]; then
  capacity_commitment_id=$(bq ls --capacity_commitment --location US --format prettyjson --project_id=${admin_project_id} | jq 'map(select(.["plan"] | startswith("FLEX"))) | .[] | .name | split("/") | .[5]'| sed 's/"//g')
  bq rm --project_id=${admin_project_id} --location=US --capacity_commitment ${admin_project_id}:US.${capacity_commitment_id}
fi

まとめ

Flex Slotsを導入することでBigQueryの使い方によってはコストメリットが得られ、データ集計の遅延がなくなるんじゃないかと思います。
Flex Slotsが購入できない場合もあるので、オンデマンドへ自動切り替えできるようワークフローを作る必要があります。