はじめに
Datastream + Dataflow + Spanner の組み合わせで初めて本番想定のデータ移行を回したとき、1日のうちに3つの罠を踏みました。
- IAM: Dataflow 管理者ロールだけ付けて満足し、ワーカーがコケる
-
パス不一致: ジョブは
Runningのまま、なのにデータが入らず2時間放置 - 非同期完了待ち: Spanner のインデックス作成が完了する前に Dataflow を投げて詰まる
それぞれ別レイヤの問題ですが、共通する怖さがあります。
エラーにならない罠が一番怖い
ジョブステータスは緑、ログにも ERROR は出ない、なのに何も進んでいない。この記事は、これから移行を控える人が「2時間溶かす前」に起動前チェックリスト3点を回せるようにすることを目的にしています。
Part 1: IAM ―「Dataflow管理者」だけでは足りない
問題発見: ロールを付けたはずなのにワーカーがコケる
Dataflow を動かすぞ、と意気込んでサービスアカウントに Dataflow 管理者 ロールを付与。ジョブ起動コマンドを叩くと、起動はする。けれどワーカーが立ち上がる段でコケたり、GCS にアクセスできなかったりします。
「管理者って書いてあるロールなのに何が足りないんだ」となるのですが、ここにDataflow の独特な責務分割があります。
解決: Dataflow は「2つのロール」を前提に動く
Dataflow の権限モデルは、メタ操作と実処理で別ロールに分かれています。
| ロール | 担当範囲 | 付与しないと起きること |
|---|---|---|
| Dataflow 管理者 | ジョブの作成・管理・ステータス参照 | そもそもジョブが起動できない |
| Dataflow ワーカー | 実際にデータを処理するワーカーVMの動作 | ワーカーが立ち上がっても処理を進められない |
「管理者 = メタ操作、ワーカー = 実処理」という分かれ方なので、片方だけだとジョブは見えるけど中身が動かないという事態になります。
加えて必要な GCS / Spanner 側の権限
ロールが2つ揃っても、まだ足りません。ワーカーが実際に触るリソースの権限を別経路で付ける必要があります。
-
入力 GCS バケット:
Storage Object Viewer(Avro ファイル読み込み) -
出力 GCS バケット(テンポラリ用途):
Storage Object Creator -
Spanner:
Cloud Spanner Database User
Dataflow ロールはあくまで Dataflow という機能を使う権限で、その先のリソースアクセスは個別のIAMです。ここを混同すると「ロール付けたのに動かない」沼にはまります。
設計判断: どのサービスアカウントに付けるか
検証段階では Compute Engine デフォルトサービスアカウントに上記3カテゴリを集約しておくと、サクッと回せて検証がはかどります。本番では専用サービスアカウントを切るのが原則ですが、検証で「最小権限を完璧に」やろうとすると進まないので、まずは動かす方を優先しました。
Part 2: 「2時間データが入らない」── inputFilePattern と Avro 実パスの不一致
問題発見: ジョブは Running、ログにエラーなし、なのに空
IAM の壁を越えて、いよいよ Dataflow ジョブを起動。コンソールを見ると Running、ワーカーログにも ERROR レベルなし。「初回はスキーマ解析に時間がかかるんだったな」と思って待ちました。
30分。1時間。2時間。
Spanner 側を覗いてもデータが1行も入っていない。さすがにおかしい、と気付くまで2時間溶かしました。
切り分け: まず「1テーブルだけ」で再実行する
全テーブル流すと「どこで止まっているか」が分からなくなります。最速の切り分けは、
1テーブルだけを指定して再実行する
これに尽きます。1テーブルなら期待件数が小さく、入らない時すぐ気付ける。流す前の Avro ファイル一覧もチェックしやすい。問題が**「全体に起きている」のか「特定テーブルだけ」なのかが5分**で分かります。
原因: glob とパス階層の微妙なズレ
調べた結果、inputFilePattern の glob が、Datastream が実際に出力するパス階層と合っていませんでした。
Datastream は内部的にこんな階層で Avro を吐きます(実際の構造は環境による)。
gs://bucket/<schema>/<table>/<YYYY>/<MM>/<DD>/<HH>/<MM>/*.avro
Dataflow の inputFilePattern 側でこの階層を ** で吸う書き方をしていなかったため、Dataflow は**「対象ファイルなし」と判定して待ち続けていた**わけです。
ここが本当に厄介なのは、エラーにならないこと。「ファイルが見つかりません」と言ってくれれば1分で気付けるのですが、Dataflow はストリーミングモードで「これから来るかもしれない」前提で静かに待つので、見た目は完全に正常です。
起動前チェックリスト3点
それ以来、Dataflow を本起動する前にこの3点を必ず回すようにしました。
-
Avro ファイルが実際に出力されているかを
gsutil lsで実物確認 -
inputFilePatternの glob が実物のパスにマッチするかを手元で簡易展開して確認 -
Datastream の
streamNameと Dataflow パラメータのstreamNameが一致しているか
1番目の「実物確認」を飛ばすと、上の2時間トラップに突っ込みます。「設定上は合っているはず」と「実物が来ている」は別です。
Part 3: Spanner DDL の「非同期完了」を待ってから Dataflow を起動する
問題発見: インデックス作成中に Dataflow を投げてはいけない
DDL を流して CREATE INDEX がずらっと並んだあと、すぐに Dataflow を起動すると、ワーカーの書き込みとインデックス構築が取り合いになり、スループットが大きく落ちます。最悪、ジョブが詰まります。
ここで一つ嵌るのが、Spanner の DDL は非同期であることです。
gcloud spanner databases ddl update DB --ddl-file=schema.ddl
# ↑ コマンドは即座に返ってくる
# 実体のインデックス構築はバックエンドで進行中
コマンドが返ってきても、バックエンドではまだ作業中。ここで Dataflow を投げると上記の取り合いが発生します。
解決: gcloud spanner operations list でポーリング
完了検知の正攻法は gcloud spanner operations list で進行中のオペレーションを列挙し、すべて DONE になるまで待つことです。
gcloud spanner operations list \
--instance=$INSTANCE --database=$DB \
--filter="metadata.@type:UpdateDatabaseDdlMetadata AND NOT done"
進行中のものだけが返るので、戻り値が空になったら完了、というシンプルな判定にできます。
ポーリングスクリプトの設計判断 4要素
実際に組んだ「完了待ち → 自動 Dataflow 起動」スクリプトでは、以下の4要素を必ず入れています。
| 要素 | 設計値 | 理由 |
|---|---|---|
| 完了判定 | 進行中オペが0件 | DONE 状態を確認するより、進行中フィルタの方が確実 |
| 失敗の早期検知 |
ERROR 状態を別途検出して即停止 |
永久に「失敗して止まったまま」を待たないため |
| インターバル | 30秒〜1分 | 短すぎると API リクエスト無駄、長すぎると完了に気付くのが遅い |
| タイムアウト | 想定時間の2倍 | 「いつまで経っても終わらない」時に永久ループしないため |
疑似コードレベルだとこんな構成です。
START=$(date +%s)
while true; do
PENDING=$(gcloud spanner operations list ... --filter="...AND NOT done" --format=...)
[ -z "$PENDING" ] && break # 完了
ERR=$(gcloud spanner operations list ... --filter="metadata.error:*" --format=...)
[ -n "$ERR" ] && { echo "DDL failed: $ERR"; exit 1; } # 失敗検知
[ $(($(date +%s) - START)) -gt $TIMEOUT_SEC ] && { echo "Timeout"; exit 2; }
sleep 60
done
# ここで初めて Dataflow を起動
gcloud dataflow flex-template run ...
「完了したら起動」だけだと、永久にループするか失敗を見逃すかのどちらかに転びがちなので、失敗検知とタイムアウトをセットで入れるのが現場の知恵です。
副次の学び: 並行実行の可否は事前に決めておく
そもそも「インデックス作成中に Dataflow を流していいのか」は、本番の現場で迷ってはいけない判断です。検証環境で実際に並行実行を試して、
- スループット低下幅
- ジョブ詰まりの有無
- インデックス完成までの遅延
を計測し、「並行禁止 / 条件付き許可 / 並行可」の方針を移行手順書に明文化しておきます。本番で「これ並行で流していいんだっけ?」と迷うのは事故の温床です。
Part 4: 起動前にあと2つだけ ── 細かい罠
主役3軸のついでに、同日踏んだ細かい罠も補足しておきます。
DDL update は既存インデックスをスキップする(バッチ投入時)
gcloud spanner databases ddl update でまとめて流す場合、既に存在するインデックスはスキップされます。CREATE INDEX IF NOT EXISTS 相当の挙動です。
ただし単発の CREATE INDEX は既存だとエラーになります。「DDLバッチ投入なら冪等」「単発は非冪等」と覚えておくと安全です。
PostgreSQL → Spanner DDL 変換: デフォルト値は移行しない
PostgreSQL の DEFAULT '0' のようなカラムデフォルトは、Spanner への変換時に削除する方針にしました。理由はシンプルで、
- Spanner 側のデフォルト値表現は PostgreSQL とは別
- アプリ層でデフォルト値を担保する設計に揃えるほうが運用がシンプル
「DBに任せられるものは任せる」よりも、「移行に伴って曖昧になる責務は明確化する」を優先しました。
pg_hba.conf の認証順序
検証用 VM に PostgreSQL を立てたら、ローカルから psql でログインできない事象に遭遇しました。
psql: error: FATAL: Peer authentication failed for user "postgres"
pg_hba.conf は上から順にマッチします。peer 認証を md5 より先に書かないと、ローカル接続で意図せずパスワードを求められたり、peer 認証で弾かれたりします。順序が意味を持つ設定ファイルは他にもあるので、初見では必ず公式の優先順位を確認しましょう。
加えて、Datastream の接続にはパスワード認証が必須です。検証環境でも md5 認証を有効化しておく必要がありました。
おわりに
3つの罠は別レイヤ(IAM / パス整合 / 非同期完了)ですが、起動前チェックリスト1枚で大半潰せます。
2時間データが入らないを踏む前の3チェック
- IAM: Dataflow 管理者 + Dataflow ワーカー + 入出力リソース(GCS/Spanner)の権限が揃っているか
- inputFilePattern: 実 Avro パスと glob が実物確認で一致するか
- DDL 完了: インデックス作成が全て
DONEになってから Dataflow を起動するか
冒頭で書いた通り、この組み合わせの本質的な怖さは、
エラーにならない罠が一番怖い
という一点に尽きます。ステータス緑・エラーログなし・なのにデータが入らない、という状態を待ち続けてはいけない。短い時間で気付くために、1テーブル指定の単体テストを最初に必ず通す、というのが今回得た一番の教訓でした。
Datastream + Dataflow + Spanner の組み合わせを初めて回す方の参考になれば嬉しいです。