BigQuery 連携クエリ・外部データセットが使う Spanner トランザクションについて

ゼロバンク・デザインファクトリー株式会社(ZDF)で、データ基盤の運用・構築を担当しているキョウです。

## 背景

2024年4月の[Google Cloud Next '24 Las Vegas Recap](https://cloudonair.withgoogle.com/events/gc-updates?talk=optional1-2024)で言及されて以来、10月3日に、BigQuery external dataset を使って Spanner のデータセットをリンクする機能が Public Preview になったと[リリースノート](https://cloud.google.com/bigquery/docs/release-notes##October_03_2024)で発表されました。

生成AIほどの注目を集めていないかもしれませんが、みんなの銀行のデータ基盤にとっては、嬉しいリリースになりそうです。

みんなの銀行開業に伴い、データ基盤システムが稼働開始しました。多様なデータソースから、リアルタイム、毎時、毎日といった頻度でデータ連携処理を行っています。その中で、やむを得ず、一部データについては、Spanner から BigQuery への日次連携に以下の方法を採用しました。

![Spanner_GCS_BigQuery.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3128410/158459eb-b794-3ac5-2334-c84b4042232f.png)


自ら実装した連携処理のため、ライブラリアップデート対応などの一般的な保守タスクに加え、Google Cloud Storage(GCS)へのエクスポート時に稀に発生する一時エラーへの対応にも頭を悩ませてきました。[^1]
グリーンフィールドプロジェクトへの憧れは尽きませんが、現実には、既存システムであるブラウンフィールドプロジェクト[^2]に柔軟性をもたらすことこそが、最大のミッションだと認識しています。
「利用するコンポーネントは少ない方が良い」という前提のもと、Spanner から BigQuery へのデータ連携方法として、以下の二つの仕組みを検討することにしました。
いずれの方法も、BigQuery のクエリを実行するだけで Spanner のデータを参照できます。
それぞれを深堀して得られた気づきを共有させていただきます。

### 1. Federated queries(連携クエリ)

> 連携クエリによって、クエリ ステートメントを Spanner または Cloud SQL データベースに送信し、結果を一時テーブルとして取得できます。連携クエリは、BigQuery Connection API を使用して Spanner または Cloud SQL との接続を確立します。クエリで `EXTERNAL_QUERY` 関数を使用して、外部データベースの SQL 言語を使用してクエリ ステートメントを外部データベースに送信します。結果は GoogleSQL データ型に変換されます。

と[連携クエリの概要のドキュメント](https://cloud.google.com/bigquery/docs/federated-queries-intro?hl=ja)に記載されています。さらに、Spanner との接続を作成する際に、`"useDataBoost": true` を使うことで、既存の Spanner トランザクションへの影響少なくデータアクセスできます。

### 2. External datasets(外部データセット)

BigQuery external dataset は、外部データソースと BigQuery をデータセットレベルで接続する機能です。これにより、Spanner から BigQuery にデータを移動することなく、GoogleSQL を使用して Spanner データベースのトランザクションデータにアクセスできます。[^3]

また、Spanner データを参照する際に、デフォルトで Data Boost が利用されています。

以上の両者とも、データ連携に必要なコンポーネントは、データソースである Spanner と連携先の BigQuery のみになります。このシンプルな構成は魅力的です。また、Spanner の既存トランザクションへの影響が少ない点も評価できます。

連携クエリ、外部データセットの作成・使い方について、参考資料のリンク先のドキュメントで記載されているため、ここでは割愛します。
これからは、連携クエリと外部データセットを比較しながら、両者が利用している Spanner トランザクションについて、少し深く述べたいと思います。 

## Spanner トランザクション

Data Boost に関するドキュメントを漁る中で、[Data Boost 使用状況をモニタリングする](https://cloud.google.com/spanner/docs/databoost/databoost-monitor?hl=ja)が調査の一つ切り口となりました。プリンシパル別・BigQuery ジョブ ID 別で使用状況を確認していく中で、Spanner 監視ログに記録されているログエントリーの内容が気になり始めました。

```text
protoPayload.authenticationInfo.principalEmail="{Google Account}"
resource.type="spanner_instance"
resource.labels.location="{Region}"
resource.labels.instance_id="{Spanner Instance ID}"
timestamp>="{BigQuery Job Start time}" timestamp<="{BigQuery Job End time}"
```

例えば、上の Cloud Logging クエリ[^4]で抽出したログエントリーでは、`protoPayload.requestMetadata.callerSuppliedUserAgent`[^5]フィールドが、連携クエリも、外部データセットも同じ値（以下）になっていることが判明しました。

> BQ_ExternalQuery/bigquery.federation_{VERSION} grpc-c++/{VERSION} grpc-c/{VERSION} (linux; chttp2)
> BQ_ExternalQuery/dremel_service_{VERSION} grpc-c++/{VERSION} grpc-c/{VERSION} (linux; chttp2)

Spanner側のログから見ると、連携クエリと外部データセットのリクエストは同じ仕組みが利用されているのではないかと思えました。[^6]

### Spanner Read-only Transaction

ここから本題。
監視ログエントリーの`protoPayload.methodName`フィールドを絞り込むと、以下のリストが得られました。括弧内は、同じログエントリーで確認できたリクエストタイプです。

- google.spanner.v1.Spanner.`CreateSession` (`CreateSessionRequest`)
- google.spanner.v1.Spanner.`ExecuteSql` (`ExecuteSqlRequest`)
- google.spanner.v1.Spanner.**`PartitionQuery` (`PartitionQueryRequest`)**
- google.spanner.v1.Spanner.`ExecuteStreamingSql` (`ExecuteSqlRequest`)

これらの内、`PartitionQuery`と`PartitionQueryRequest`は、Spanner のトランザクション種類などの情報を確認する手がかりになりました。

まず、`PartitionQuery`に関して、ドキュメントには以下の記述があります。

> Creates a set of partition tokens that can be used to execute a query operation in parallel. Each of the returned partition tokens can be used by ExecuteStreamingSql to specify a subset of the query result to read. 

並行してクエリが実行できるように、パーティショントークンセットを作成します。そのパーティショントークンは`ExecuteStreamingSql`で読み取りのクエリ結果のサブセットを特定するために使えます。
ログエントリー上でも、`PartitionQuery`ログの後に、複数回の`ExecuteStreamingSql`ログが確認でき、全てが Data Boost を利用していることもわかりました。

そして、最も重要な情報が`PartitionQueryRequest`にあります。

```json
"request": {
  "@type": "type.googleapis.com/google.spanner.v1.partitionqueryrequest",
  "session": "projects/{project_id}/instances/{instance_id}/databases/{database_id}/sessions/{session_id}",
  "transaction": {
    "begin": {
      "readonly": {
        "readtimestamp": "2024-10-02t00:47:46.689z"
      }
    }
  },
  "sql": "{query}"
}
```

transaction → begin → readonly → readtimestamp の順でたどり着いた[ドキュメント](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#readonly)では、以下の記述があります。

> Executes all reads at the given timestamp. Unlike other modes, reads at a specific timestamp are repeatable; the same read at the same timestamp always returns the same data. 

ドキュメントに記載されている「同じタイムスタンプであれば、同じデータが返ってきます」という一文は魅力的ですが、ログエントリーに記録される`readtimestamp`は BigQuery ジョブで指定できず、あくまでも BigQuery と Spanner やり取りの内部処理で使用される値とのことです。残念ながら、エンドユーザーである私たちが簡単に検証することはできません。

しかし、この **readtimestamp** を実行された BigQuery ジョブの情報と紐づけることで、連携処理の可観測性を向上させるために活用できると考えられます。今後の設計において、この点を積極的に検討していきたいと思います。


## まとめ

データ連携に関して考慮すべき点は多岐に渡り、本記事で触れられたのはそのほんの一部に過ぎません。しかし、監視ログを起点にし、連携クエリと外部データセットを比較検討することで、連携クエリ・外部データセット仕組みの違い、また、Spanner のトランザクションについて、これまでよりも深く理解することができました。

本記事が、読者の皆様にとって少しでも参考になれば幸いです。


## 参考資料

- [[Cloud Spanner] TransactionOptions](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.TransactionOptions)
- [[Cloud Spanner] Data Boost の概要](https://cloud.google.com/spanner/docs/databoost/databoost-overview?hl=ja)
- [Cloud Spanner における各種トランザクションの使い分け](https://zenn.dev/google_cloud_jp/articles/15d34df66becfe)
- [[BigQuery] Create Spanner external datasets](https://cloud.google.com/bigquery/docs/spanner-external-datasets)
- [[BigQuery] Introduction to federated queries](https://cloud.google.com/bigquery/docs/federated-queries-intro)
- [[BigQuery] Spanner federated queries](https://cloud.google.com/bigquery/docs/spanner-federated-queries)



[^1]: 必ずしも GCS サービス自身の一時エラーではありません。GCSへアクセスする際に、IAM認証なども含まれています。そして、発生したエラーはPython クライアントライブラリの[内部リトライ対象](https://github.com/googleapis/python-storage/blob/main/google/cloud/storage/retry.py#L28)であるかを正しく認識しないと、冪等性が損ねる実装をしてしまうこともあります。

[^2]: 「[データエンジニアリングの基礎](https://www.oreilly.co.jp/books/9784814400652/)」という本では、まっさらな状態から始まるプロジェクトを「**グリーンフィールド**」、既存のアーキテクチャを再設計するプロジェクトを「**ブラウンフィールド**」と呼んでいます。

[^3]: Public Preview のため、[Create Spanner external datasets](https://cloud.google.com/bigquery/docs/spanner-external-datasets)の冒頭の文書を参照して翻訳してみました。データ移動することなくと記載されていますが、BigQuery は、すべてのクエリ結果を永続テーブルまたは一時テーブルに保存します([ドキュメント](https://cloud.google.com/bigquery/docs/writing-results?hl=ja))。

[^4]: クエリ中の`Google Account`は BigQuery ジョブを実行したユーザーになります。

[^5]: リクエスト元となる情報、例えば、ユーザーがブラウザーで BigQuery クエリを実行した場合、ユーザーの「ブラウザ・PC情報」がこのフィールドで確認できます。今回 Spanner の場合は、リクエスト元となる BigQuery 側の情報が含まれていると理解しています。

[^6]: 2024/10/25 時点の情報を基づく話で、BigQuery のリリースによる、変更されるかもしれません。