Mercari Engineering Blog

We're the software engineers behind Mercari. Check out our blog to see the tech that powers our marketplace.

Cloud Scheduler の cron ジョブで Cloud Spanner のノード数を変更する方法

この記事は MERPAY TECH OPENNESS MONTH の8日目の記事です。

こんにちは。株式会社メルペイ SRE の tkuchiki です。

メルペイでは Cloud Spanner(以降、Spanner) をメインのデータベースとして利用していますが、Spanner に限らず、バッチ処理などで一定時間 DB の負荷が増加する際に一時的にリソースを増やしたいと思うことがあるのではないでしょうか。Spanner は水平スケーリング可能な分散リレーショナルデータベースであるため、ダウンタイムなしでリソース(ノード)を増減させることができるので、一時的に負荷が増加するワークロードにも簡単に対応できます。
本記事では、Cloud Scheduler を活用して Spanner のノード数を変更する方法を 2つ紹介します。

(2019年5月29日現在の情報をもとに執筆しているため、今後 GCP のサービスや記事中に記載しているコマンドの仕様は変更される可能性がありますのでご注意ください)

1. Cloud Scheduler で Spanner のノード数を変更する

Cloud Schedulerは GCP のフルマネージド cron ジョブスケジューラです。 HTTP/S エンドポイント、Cloud Pub/Sub(以降、Pub/Sub)トピック、App Engine アプリケーションのいずれかのターゲットにジョブを送信することができます。
GCP の各種サービスには REST API が用意されているので、HTTP/S エンドポイントを使って API を叩いてみます。

Service Account の設定

まず、Cloud Scheduler が Spanner の REST API を実行できるように、Service Account を作成します。
作成した Service Account にspanner.instances.updatecloudscheduler.jobs.run permission を持ったカスタム role を作成して付与します。

$ gcloud --project YOUR_PROJECT iam service-accounts create spanner-operator --display-name="Spanner Operator"
$ gcloud --project YOUR_PROJECT iam roles create spannerOperator --permissions spanner.instances.update,cloudscheduler.jobs.run
$ gcloud projects add-iam-policy-binding YOUR_PROJECT \
  --member serviceAccount:spanner-operator@YOUR_PROJECT.iam.gserviceaccount.com \
  --role projects/YOUR_PROJECT/roles/spannerOperator

$ gcloud --project YOUR_PROJECT iam service-accounts set-iam-policy spanner-operator@YOUR_PROJECT.iam.gserviceaccount.com  policy.json 

cron ジョブの設定

次に cron ジョブの設定です。 Spanner のノード数を変更するには、projects.instances.patch APIを叩く必要があります。 PATCH メソッドで、https://spanner.googleapis.com/v1/projects/YOUR_PROJECT/instances/YOUR_INSTANCE というエンドポイントにリクエストを送信すれば OK です。 パラメータや OAuth のスコープの詳細についてはドキュメントを読んでいただくとして、コマンドにすると以下のようになります。

$ cat << EOS > increase_nodes.json
{
  "fieldMask": "nodeCount",
  "instance": {
    "name":        "projects/YOUR_PROJECT/instances/YOUR_INSTANCE",
    "nodeCount":   2
  }
}
EOS

$ gcloud --project YOUR_PROJECT beta scheduler jobs create http increase_spanner_nodes  \
       --schedule "0 0 * * *" --time-zone "Asia/Tokyo" \
       --uri="https://spanner.googleapis.com/v1/projects/YOUR_PROJECT/instances/YOUR_INSTANCE" \
       --oauth-service-account-email=spanner-operator@YOUR_PROJECT.iam.gserviceaccount.com    \
       --message-body-from-file=increase_nodes.json \
       --headers=Content-Type=application/json \
       --oauth-token-scope=https://www.googleapis.com/auth/cloud-platform

gcloud beta scheduler jobs create http コマンドには --http-method=HTTP_METHOD というオプションがありますが、執筆時に対応している HTTP メソッドは DELETE、GET、HEAD、POST、PUT となっていたため、GCP Console 上で手動で PATCH メソッドに変更する必要がありました。

ノード数を増加させる cron ジョブは設定できましたので、次はノード数を減少させる cron ジョブの設定です。

$ cat << EOS > decrease_nodes.json
{
  "fieldMask": "nodeCount",
  "instance": {
    "name":        "projects/YOUR_PROJECT/instances/YOUR_INSTANCE",
    "nodeCount":   1
  }
}
EOS

$ gcloud --project YOUR_PROJECT beta scheduler jobs create http decrease_spanner_nodes  \
       --schedule "0 1 * * *" --time-zone "Asia/Tokyo" \
       --uri="https://spanner.googleapis.com/v1/projects/YOUR_PROJECT/instances/YOUR_INSTANCE" \
       --oauth-service-account-email=spanner-operator@YOUR_PROJECT.iam.gserviceaccount.com    \
       --message-body-from-file=decrease_nodes.json \
       --headers=Content-Type=application/json \
       --oauth-token-scope=https://www.googleapis.com/auth/cloud-platform

こちらも GCP Console 上で HTTP メソッドを PATCH に変更します。 以上の設定で、00:00 JST に Spanner のノード数を 2 に変更し、01:00 JST に Spanner のノード数を 1 に変更できるようになりました。 この方法は、プログラムを書かず Cloud Scheduler の設定だけで Spanner のノード数を変更できるため簡単に導入できます。

本項では、単純な 1 API 呼び出しでの絶対値によるノード数の増減方法を紹介しました。しかし、現在のノード数を起点に 3台追加したい、1.5倍にしたい、といった相対値で設定したいこともあるのではないかと思います。単純な 1 API 呼び出ししかできない Cloud Scheduler の HTTP/S エンドポイントでは、ある HTTP リクエストの結果を使ってさらに HTTP リクエストを送ることはできないので、現在のインスタンス数を取得して、それに 3台 追加/削除する、といった相対値での設定ができません。 次項では、Cloud Scheduler と Pub/Sub、 Cloud Functions を組み合わせて Spanner ノード数を変更する方法を紹介します。

2. Cloud Scheduler + Cloud Pub/Sub + Cloud Functions で Spanner のノード数を変更する

Cloud Functions はイベント駆動型のサーバレスコンピューティングサービスです。 Cloud Functions はプログラムを実行できるので、相対値でのノード数を変更するプログラムを書いてデプロイしてみます。

Pub/Sub トピック作成

Cloud Functions で Pub/Sub のメッセージをサブスクライブするので Pub/Sub トピックを作成します。

$ gcloud --project YOUR_PROJECT pubsub topics create spanner-topic

Cloud Functions で実行する関数の実装

以下のコマンドを実行して Go Modules の設定をします。

$ export GO111MODULE=on
$ go mod init
$ go get cloud.google.com/go/spanner/admin/instance/apiv1

準備ができたら Go のプログラムを書きます。

package function

import (
    "context"
    "encoding/json"
    "log"
    "time"

    spannerClient "cloud.google.com/go/spanner/admin/instance/apiv1"
    "google.golang.org/genproto/googleapis/spanner/admin/instance/v1"
    "google.golang.org/genproto/protobuf/field_mask"
)

type updateParams struct {
    NodeCount int32 `json:"node_count"`
}

type pubsubMessage struct {
    Data []byte
}

var instanceName = "projects/YOUR_PROJECT/instances/YOUR_INSTANCE"

func SetSpannerNodes(ctx context.Context, m pubsubMessage) error {
    _, cancel := context.WithTimeout(ctx, time.Second*10)
    defer cancel()

    var uparams updateParams
    err := json.Unmarshal(m.Data, &uparams)
    if err != nil {
        return err
    }

    client, err := spannerClient.NewInstanceAdminClient(ctx)
    if err != nil {
        log.Print(err)
        return err
    }

    getReq := &instance.GetInstanceRequest{
        Name: instanceName,
    }

    i, err := client.GetInstance(ctx, getReq)
    if err != nil {
        log.Print(err)
        return err
    }

    reqInstance := &instance.Instance{
        Name:      instanceName,
        NodeCount: i.NodeCount + uparams.NodeCount,
    }

    fieldMask := &field_mask.FieldMask{
        Paths: []string{"node_count"},
    }

    uireq := &instance.UpdateInstanceRequest{
        Instance:  reqInstance,
        FieldMask: fieldMask,
    }

    op, err := client.UpdateInstance(ctx, uireq)
    if err != nil {
        log.Print(err)
        return err
    }

    _, err = op.Wait(ctx)
    if err != nil {
        log.Print(err)
        return err
    }

    return nil
}

簡単に説明すると、以下のようなことをしています。

  • サブスクライブした Pub/Sub メッセージから JSON データを取り出す
  • 現在の Spanner ノード数を取得して JSON ペイロードの NodeCount と加算してノード数を変更する
    • NodeCount を負の整数にすればノード数を減少できる
      • これにより、一つの function で Spanner ノード数の追加・削除を行う

Service Account の role を設定

デプロイする前に、Cloud Functions の中で Instance 情報を取得する API を叩いているので Service Account に role を追加します。

$ gcloud --project YOUR_PROJECT iam roles update spannerOperator --add-permissions spanner.instances.get

デプロイと cron ジョブの設定

以下のコマンドで Cloud Functions をデプロイします。

$ gcloud --project YOUR_PROJECT functions deploy SetSpannerNodes \
       --runtime go111 --trigger-topic spanner-topic --region asia-northeast1 --memory 128MB \
       --service-account spanner-operator@YOUR_PROJECT.iam.gserviceaccount.com

Cloud Scheduler の設定をします。

$ gcloud --project YOUR_PROJECT beta scheduler jobs create pubsub increase_spanner_nodes_cloud_functions  \
       --schedule "0 0 * * *" --time-zone "Asia/Tokyo" \
       --topic=spanner-topic \
       --message-body='{"node_count": 2}'

$ gcloud --project YOUR_PROJECT beta scheduler jobs create pubsub decrease_spanner_nodes_cloud_functions  \
       --schedule "0 1 * * *" --time-zone "Asia/Tokyo" \
       --topic=spanner-topic \
       --message-body='{"node_count": -2}'

以上の設定で、00:00 JST に現在の Spanner ノード数を 2 増やして、01:00 JST に現在の Spanner ノード数を 2 減らせるようになりました。

考察

Cloud Scheduler のドキュメントを読むと以下のような記載があります。

どの時点をとっても同じジョブの複数インスタンスが同時に実行されないようにしてください。また、Cloud Scheduler は、「少なくとも 1 回」を基本に処理を行うよう設計されています。つまり、ジョブがスケジュールされると、Cloud Scheduler はそのジョブのリクエストを少なくとも 1 回は送信します。まれに、同じジョブの複数のインスタンスがリクエストされる可能性があります。このためリクエスト ハンドラはべき等である必要があります。またコードを記述する際は、このような状態が発生した場合に有害な副作用が発生しないようにする必要があります。

Cloud Scheduler + Pub/Sub + Cloud Functions の組み合わせは柔軟な設定が可能である一方で、べき等性を担保する必要があるため前述したプログラムに一工夫加える必要があります。ノード数の追加がべき等でない分にはノードが増えすぎるだけなので費用の問題以外は発生しないと思われますが、ノード数の減少は致命的な問題になりかねません。
Cloud Scheduler 単体での Spanner ノード数の増減は、絶対値による単純な増減しかできないものの、プログラムを書かなくても導入でき、べき等性も担保されているという利点がありそうです。

まとめ

Cloud Scheduler を活用して Spanner のノード数を変更する方法を 2 つ紹介しました。
Cloud Scheduler から GCP の API を定期的に実行するワークロードは、Spanner 以外でも活用できる場面があると思いますので参考にしていただけますと幸いです。

MERPAY TECH OPENNESS MONTH の 9日目の記事はメルペイソリューションチームの @orfeon による「メルペイにおけるDataflow Templateの活用」です。お楽しみに!

参考文献