Cloud Spanner を導入してハマったアレコレ

アソビュー! Advent Calendar 2023 の15日目です。

エンジニアの村松です。みなさん進捗いかがでしょうか。私はまだ子供4人分のクリスマスプレゼントが決まっておりません🎅

さて、アソビューでは以前よりシステムの刷新プロジェクトに取り組んでいます。新規システムは、これまでと同様、様々なレジャー施設や体験事業者様のプランを取り扱い、多くの人にアクセスされるシステムになりえるため、高可用性とスケーラビリティの観点からデータベースには Cloud Spanner を採用しています。

Spanner はアソビューにとって新しい要素技術ということもあり、いろいろとハマりどころがありました。今回は、私たちがハマったアレコレの一部を紹介します。

また、本アドベントカレンダーの4日目でも Spanner に関する記事を書いていますのでこちらもぜひご覧ください!

tech.asoview.co.jp

前提

新規システムのアプリケーションは Java + Spring Boot を利用して開発しており、ローカルでは Docker の Spanner エミュレータを利用しています。

また、データベースのマイグレーションツールには Flyway を利用しています。関連するライブラリのバージョンは次の通りです。(執筆時点)

  • google-cloud-spanner-jdbc (2.11.5)
  • org.flywaydb.flyway (9.21.1)
  • flyway-gcp-spanner (9.21.1-beta)

それでは、私たちがハマったアレコレの一部を紹介します。

アプリケーションが起動しない

事象

ローカルでいくつか機能開発を進めて開発環境にデプロイしてみたところ、アプリケーションが起動しない事象が発生しました。調査はそこそこ難航したのですが、Readiness Probe が失敗してコンテナが起動できない状況であることが分かりました。

アプリケーションと Spanner の間は gRPC で通信しています。この gRPC コネクションの初期化には非常に時間がかかるようです。そのため、初回の Probe では Spanner への初回のヘルスチェックが通らず、Probe が落ちてしまっているようでした。

対策

Spring の起動プロセスの中で gRPC コネクションを初期化するようにしました。具体的には、Spring の ApplicationStartedEvent イベントをハンドリングし、その中で Spanner に接続して SELECT 1 を投げるようにしました。

@Configuration
public class Config {
    @EventListener
    public void handle(ApplicationStartedEvent event) {
        new JdbcTemplate(dataSource).execute("SELECT 1");
    }
    // ...(略)
}

ApplicationStartedEvent イベントは Liveness Probe や Readiness Probe が始まる前に発行されます。Spring の起動プロセス内で Spanner に初回接続して gRPC コネクションが初期化されるため、これで Probe が落ちる問題は回避できました。

データベースマイグレーションが完了しない

事象

普段はローカルの Spanner エミュレータを利用して開発しており、ローカルの Spanner は Flyway でマイグレーションしています。ここまでは特に問題なかったのですが、開発環境の Spanner を Flyway でマイグレーションしようとしたところ、途中までは正常にテーブルやインデックスが作成されるものの、ある時点から DDL の実行が進まなくなる事象が発生しました。

こちらの調査もわりと難航したのですが、Flyway だけでなく DB クライアントツールからの実行でも事象が再現したため、サーバー側に原因があることが予想されました。調査を進めたところ、Spanner が DDL 実行をスロットリングしていることが分かりました。以下、公式ドキュメントから抜粋です。

短時間に多数のスキーマ更新を実行した場合、Spanner はキューに格納されたスキーマ更新の処理を throttle する可能性があります。これは、Spanner がスキーマのバージョンを保存するためのスペースの量を制限するためです。保持期間内に古いスキーマ バージョンが多すぎる場合は、スキーマの更新が抑制されることがあります。

スキーマの更新  |  Cloud Spanner  |  Google Cloud

新規システムの初期構築で大量の DDL を実行したがために発生した事象でした。

対策

DDL をバッチ実行するようにしました。具体的には、DDL の前後に START BATCH DDLRUN BATCH のステートメントを追加しました。

START BATCH DDL;

CREATE TABLE ...
CREATE INDEX ...
...(略)

RUN BATCH;

ただ、ここでもう1点問題が。

DDL のバッチ実行は、jdbc レベルではサポートされているようでしたが、現時点の Flyway では正常に動作しませんでした。正確には、flyway_schema_history テーブルは作られて SUCCESS のレコードは登録されているものの、肝心のテーブルが作成されない状況でした。(注: 少なくとも Community Edition では動作を確認できませんでした。Team Edition には Batch サポートがあるようなのですがこちらは試すことができず...。)

そこで、flyway-core の MigrationExecutor インタフェースを実装して、BATCH DDL のステートメントを含む DDL を直接実行するようにしました🐼

public class MyMigrationExecutor implements MigrationExecutor {
    // ...(略)
    @Override
    public void execute(Context context) throws SQLException {
        Connection connection = context.getConnection();
        try (Statement statement = connection.createStatement();) {
            String ddl = new String(Files.readAllBytes(Paths.get(file.toURI())));
            for (String sqlStatement : ddl.split(";")) {
                // remove comments
                String sql = sqlStatement.replaceAll("-- .*", "").trim();
                if (sql.isEmpty()) {
                    continue;
                }
                statement.execute(sql);
            }
        } catch (Exception e) {
            throw new SQLException(e);
        }
    }
}

この Executor は MigrationResolver インタフェースの実装クラスで指定します。

public class MyMigrationResolver implements MigrationResolver {
    @Override
    public Collection<ResolvedMigration> resolveMigrations(Context context) {
        // ...(略)
        return new ResolvedMigrationImpl(
                info.getLeft(),
                info.getRight(),
                filePath,
                null,
                null,
                CoreMigrationType.CUSTOM,
                ClassUtils.getLocationOnDisk(getClass()),
                new MyMigrationExecutor(f) // ★
        );
    }
}

さらに、この Resolver クラスは Flyway のプロパティで指定します。(以下は build.gradle の例)

flyway {
    url =  'jdbc:cloudspanner://...'
    locations = '...'
    resolvers = ['MyMigrationResolver'] // ★
    skipDefaultResolvers = true // ★
}

これで Flyway でも DDL のバッチ実行ができるようになりました。

まとめ

Cloud Spanner における私たちがハマったアレコレの一部を紹介しました。Spanner に関してはまだまだ未知の部分があり、今後も開発/運用していく中でノウハウを身に付けていければと思っています。

アソビューでは「生きるに、遊びを。」をミッションに、一緒に働くメンバーを募集しています!ご興味がありましたらお気軽にご応募いただければと思います!

www.asoview.com