🔧

Spannerのデータ移行を支えるツール

toshi06072023/01/13に公開

概要

プロダクトでデータベースにGoogle CloudのCloud Spannerを利用する際、日々の運用の中で単発のDMLでは難しい一定量以上のデータを移行するタイミングがあると思います。Google Cloud公式ページで紹介されているSpanner のインポートとエクスポートについてが多くのユースケースをカバーしているものの、何らかの制約で利用できない場合もあります。本記事では、そういうときに便利だったツールとユースケースを3つ紹介します。

1. 少量のデータ移行

ユースケースと前提

既存Spannerテーブルの一部を別プロジェクトのSpannerテーブルに移行するユースケースです。Cloud Dataflowを利用してエクスポート・インポートを行うことが適切な場合が多いかもしれません。しかし、今回は次の要件があったためCloud Dataflowの利用は見送りました。

  • データ量が60万件程度
  • データ移行中にデータ移行前のテーブルにデータが数十件追加・更新される見込み。追従する必要がある
  • データ移行中の差分を抽出して反映したいが、時間ベース(CreatedAtやUpdatedAtなど)で差分を抽出できない

実際の手法

このユースケースで利用したのが以下の2つのCLIです。

spanner-dumpは、指定したSpannerデータベースのデータをINSERT文としてダンプするためのCLIツールです。つぎのように対象テーブルをオプションで絞り込めます。

$ go install github.com/cloudspannerecosystem/spanner-dump@latest
$ spanner-dump -p ${PROJECT} -i ${INSTANCE} -d ${DATABASE} --no-ddl --tables ${TABLES} > inserts.sql

INSERT文はデフォルトで100レコードずつダンプされ、--bulk-sizeオプションで変更可能です。ダンプに要した時間は10秒未満でした。

ダンプしたデータ(複数行のINSERT文)は、spanner-cliを利用して実行します。

$ go install github.com/cloudspannerecosystem/spanner-cli@latest
$ spanner-cli -p ${PROJECT} -i ${INSTANCE} -d ${DATABASE} < inserts.sql

PU 3000、INSERT文を分割せず直列に実行するのに要した時間は40分程度でした。PUを上げると、CPU利用率を低減できるだけでなく、実行対象のINSERT文を変更せずにスループットを向上させることができます。パフォーマンスチューニングに十分な時間をかけられず、一時的なコスト増を許容できる場合は簡易チューニング手段として試してみる価値があるはずです。

このユースケースでは、spanner-cliでのINSERT文実行中に移行元テーブルで追加・更新されたレコードをダンプ同士の比較で抽出し、移行後のテーブルに同期しました。本題ではないため、詳細は割愛します。

非本番環境での大量テストデータを削除するのには、cloudspannerecosystem/spanner-truncateが便利です。削除操作のミューテーション上限(40,000)を超える場合でもよしなにハンドリングしてくれます。

$ go install github.com/cloudspannerecosystem/spanner-truncate@latest
$ spanner-truncate -p ${PROJECT} -i ${INSTANCE} -d ${DATABASE} -t ${TABLES}

2. 別RDBMSからのデータ移行

ユースケースと前提

Cloud SQL for PostgreSQLからSpannerにデータベースを移行するユースケースです。公式ドキュメントでは、cloudspannerecosystem/harbourbridgeを利用する方法が紹介されています。しかし、PostgreSQLからSpannerへのスキーママッピングの不明瞭さ、マッピング定義の大変さ、配置するプロジェクトの制約、Cloud SQLに直接接続時のエラー追跡の困難さなどから利用を見送りました。

実際の手法

このユースケースで利用したのがCloud Dataflowです。

Google Cloudが準備するCloud Storage Text to Cloud Spannerというバッチテンプレートがサポートする範囲内のデータ型をもつテーブルのデータを移行する限りでは、それを利用するのが便利でした。COPYpsqlで実行してデータをCSVエクスポートし、Cloud Dataflowでインポートできます。

しかし、利用時点でサポートしていないデータ型がある場合は利用できません。データベース移行時、ARRAYNUMERIC型をサポートしていなかったため、他の方法を検討する必要がありました。そこで、mercari/DataflowTemplateというCloud Dataflowのフレックステンプレートを利用することにしました。

このテンプレートは、sources(データ移行元)にJDBCを、sinks(データ移行先)にSpannerを指定することができます。

Cloud SQL for PostgreSQLからSpannerにデータを移行する際は、つぎのような設定ファイルを書きます。

{
  "sources": [
    {
      "name": "jdbc",
      "module": "jdbc",
      "parameters": {
        "query": "SELECT * FROM mytable",
        "url": "jdbc:postgresql://google/mydatabase?cloudSqlInstance=myproject:us-central1:myinstance&socketFactory=com.google.cloud.sql.postgres.SocketFactory",
        "driver": "org.postgresql.Driver",
        "user": "myuser",
        "password": "mypassword"
      }
    }
  ],
  "sinks": [
    {
      "name": "spanner",
      "module": "spanner",
      "input": "jdbc",
      "parameters": {
        "projectId": "myproject",
        "instanceId": "myinstance",
        "databaseId": "mydatabase",
        "table": "mytable",
        "createTable": false,
        "keyFields": ["Field1", "Field2"]
      },
      "priority": "LOW"
    }
  ]
}

jdbcSecret Managerのリソース名を指定することもできます。

また、フィールド名変更やIDの生成・補完を行いたい場合は、次のようにsourcesに書くクエリで調整できます。

SELECT id AS PostgresId, xxx_id as XxxId, gen_random_uuid() AS YyyId FROM public.zzzs

実行にあたっては、Google Cloudリソースを準備した上でフレックステンプレートのビルドも必要です。

$ git clone git@github.com:mercari/DataflowTemplate.git
$ cd DataflowTemplate
$ mvn clean package -DskipTests -Dimage=asia-northeast1-docker.pkg.dev/path/to/mercari-flex-template
$ gcloud dataflow flex-template build gs://path/to/flex-template \
  --image "asia-northeast1-docker.pkg.dev/path/to/mercari-flex-template" \
  --sdk-language "JAVA"

# 上記のsourcesとsinksを含む設定ファイル
$ gsutil cp config.json gs://{path/to/config.json}

$ gcloud dataflow flex-template run migrate-to-spanner \
  --region asia-northeast1 \
  --service-account-email="postgres-migrator@xxxx.iam.gserviceaccount.com" \
  --staging-location gs://path/to/tmp \
  --subnetwork=regions/asia-northeast1/subnetworks/xxx \
  --template-file-gcs-location=gs://path/to/flex-template \
  --parameters=config=gs://path/to/config.json

検証時に一部動作しなかった部分も即座にサポートしていただき、無事データ移行を完了できました。公式のテンプレートではサポートされていないユースケースでも、mercari/DataflowTemplateではサポートされていることもあるはずです。

3. 変換を伴うデータ移行

ユースケースと前提

Firebase Authenticationのユーザーデータを独自の認証サービスのSpannerに移行するなど、アプリケーションに実装されたバリデーションやビジネスロジックを介してデータ移行したいユースケースです。

実際の手法

このユースケースでは、アプリケーションの資産を再利用したかったため、CLIを書きました。

CLIはデータ移行専用に一から実装するのではなく、対象のデータを持つサービスの設計に相乗りしました。対象サービスは通常サーバーとして運用していますが、serveというコマンドをurfave/cliを利用して実装したGo製CLIでもあります。そのため、今回のようなデータ移行や運用オペレーションをコマンドやサブコマンドとして実装できるようになっています。

データ移行ロジックは、普段アプリケーションで利用しているものを流用しました。データ量を考慮して過度なチューニングをせず、golang.org/x/sync/errgroupとurfave/cliのFlagを活用して実行の並列度を調整できるようになっています。

# 雰囲気を伝えるための擬似コードです
func (u *Usecase) Migrate(ctx ctx context.Context, parallel int) error {
    eg, ectx := errgroup.WithContext(ctx)
    // parallelはCLI実行時にIntFlagで渡します
    eg.SetLimit(parallel)

LOOP:
    for {
        user, err := getuser()
        if err != nil {
            return err
        }
        if user == nil {
            break
        }

        select {
        case <-ectx.Done():
            break LOOP
        default:
            eg.Go(func() error {
                if err := x.migrateUser(ctx, user); err != nil {
                    return err
                }
                return nil
            })
        }
    }
    if err := eg.Wait(); err != nil {
        return err
    }
    return nil
}

負荷を見ながら非本番環境でSpannerのPUと並列度を調整し、データ移行を完了できました。

まとめ

本記事では、Google Cloudの公式ページで紹介されたデータ移行方法では難しいユースケースと、そのときに便利なツールを紹介しました。紹介したツールを開発・メンテしてくださっているみなさまに深く感謝します。

Ubie テックブログ

Ubie株式会社のテックブログです。 採用情報:https://recruit.ubie.life/engineer

Discussion

ログインするとコメントできます