本記事は、体験談であり、一般的な設計指針を示すものではありません。
- 想定読者は、Databricksを触り始めた方
- これから実務でJobsを使おうとしている方
はじめに:Jobsの知識が、思っていたよりずっと浅かった
去年の11月頃からDatabricksに取り組み始めました。Jobsの機能は、ドキュメントで名前と概要を読んだ程度でした。「こういう機能があるらしい」と知っているだけで、実際の業務ケースでどう設計すればいいかという経験はほぼありませんでした。
それでも当時の私は、あまり心配していませんでした。
AIとGenie / Genie Codeを組み合わせれば、Jobsの設計くらい任せておけば余裕だろう
今読み返すと、かなり恥ずかしい認識です。実際に私がやっていたのは、Jobsの仕様を理解しないまま、AIにジョブを組ませることに寄りかかっていただけでした。
この記事は、その慢心が本番規模のデータで崩れ、失敗を繰り返す中でようやく腹落ちするまでの記録です。成功談ではありません。
題材:数十万件規模のデータをDatabricksに取り込む
取り組んだ内容は、次のような構成です。
- 外部のデータソースに、数十万件規模のデータがある
- 各データにはメタデータが紐づいており、管理用のテーブルでも保持されている
- これらのデータをDatabricksに取り込み、メタデータと突き合わせられる状態にする
データの中身や種類は、この記事の主題ではないので触れません。重要なのは、件数が多く、1回の処理では終わらない規模だったという点だけです。
最初のつまずき:検証で通用したやり方が本番規模で崩れる
「いけるだろう」という判断
最終的に数十万件規模のデータを取り込むことは、当初から想定していました。ただ、検証の段階で動作確認をしていたのは、そのうち数万件程度の規模です。「この規模で問題なく動いているから、あとは件数が増えるだけだろう」と、私はそこで判断してしまいました。
今振り返ると、これも認識不足のひとつです。件数が1桁変われば、処理にかかる時間もAPIの呼び出し回数も、質的に別物になります。検証結果を、規模の違いを踏まえずにそのまま延長して判断してしまいました。
最初に作ったジョブ
最初に作ったのは、次のような一体型のジョブです。
[1つのジョブ]
データを一覧取得
↓
一覧に出てきたデータを保存
数万件程度の検証では、問題なく動きました。「動いた」ので、そのまま数十万件規模で実行しました。
24時間でタイムアウトして失敗
結果は、24時間でタイムアウトして失敗でした。数万件では起きなかった問題が、数十万件の規模になった途端に表面化した形です。
このとき私は、なぜ落ちたのかの当たりがつけられませんでした。ここが一番の反省点です。
- データソース側のレート制限に引っかかったのか
- 単に処理が遅いのか
- Databricks側の制限なのか
- コードのバグなのか
切り分ける軸を持っていないので、どれも決め手がありません。AIに聞けば分かるだろうと質問はしました。ただ、返ってきた回答が合っているのかどうか、自分に判断する土台がありませんでした。もっともらしい説明が返ってきても、正しいのか的外れなのか見分けられません。
これが今回の失敗の構造でした。Jobsのタイムアウト仕様を理解していなかったので、原因の切り分けに時間がかかりました。そして、AIの回答を評価する足場がなかったので、その時間を短縮することもできませんでした。
原因を切り分けていく過程
腰を据えて、一つずつ潰していきました。
論点1:一覧取得の方式が、規模に対して成立していなかった
最初の一覧取得は、データソース側の検索APIで、対象範囲をまとめて検索する方式で行っていました。ところが、私の環境では、この検索APIのページング用パラメータに約1万件のハードキャップがあり、それを超えるとエラーになることが分かりました。
数十万件を検索で列挙しようとする方式そのものが、規模に対して成立していなかったのです。
代わりに採用したのが、一覧取得APIを再帰的に呼び出し、マーカー(カーソル)ベースのページネーションで辿る方式です。こちらは大規模データでも最後までたどることができました。
論点2:24時間は「絶対的な制約」ではなかった
次に、そもそも24時間のタイムアウトとは何なのかを調べました。
結論から言うと、Databricksの絶対的な制約ではなく、timeout_secondsという設定値でした。ジョブ単体で設定されている場合もあれば、ワークスペースのクラスターポリシーで強制されている場合もあります。
「Databricksは24時間で切れる」と漠然と思い込んでいたのですが、実際は設定の問題でした。timeout_secondsという名前自体は、ドキュメントで目にしていたはずです。それでも、自分の身に降りかかるまで、何のための設定なのか分かっていませんでした。
ただし、ここで安易にタイムアウトを延ばして解決した気になるのは危険です。設定値を大きくすれば動く可能性はあります。しかしそれは、次の論点に目をつぶった対症療法です。
論点3:本当の問題は設計だった
ここで、根本的な設計ミスに気づきました。
「一覧取得」と「取得・保存」を1本のジョブに一体化していたことが、そもそもの問題でした。
| 処理 | 性質 |
|---|---|
| 一覧取得 | メタデータだけ取得する軽い処理 |
| 取得・保存 | 実データの転送を伴う重い処理 |
性質がまったく違う2つの処理を1つのタスクに押し込んでいたので、長時間走り続け、どこかで落ちたら何が起きたのか分からなくなります。
実際、対象の大半は取り込めていました。しかし一部が漏れており、「どこが漏れているか」を機械的に把握する仕組み自体が存在していませんでした。取り込めた件数は分かっても、足りない分がどれかを特定できない。これでは再実行のしようがありません。
タイムアウトはきっかけにすぎず、本質は「途中経過を確認できず、続きから再開できない設計」にありました。数十万件という規模は事前に分かっていたにもかかわらず、この軽重の違いに設計段階で気づけていなかったことが、一番の反省点です。
設計の転換:Jobs/Tasksの使い方が変わった瞬間
一体型のジョブを、次の3つのタスクに分けました。
[列挙タスク] メタデータだけを軽く取得
↓
[突合タスク] 取り込み済みの記録と突き合わせ、未取り込み分を特定
↓
[取得タスク] 未取り込み分だけを取得してVolumeへ保存
タスク間はdepends_onで連結し、順序と依存関係を保証しました。突合タスクでは、データを一意に識別するIDをキーに、両者の差分を取る結合(ANTI JOIN)で未取り込みのデータだけを抽出しています。
さらに、**「1タスクを長時間走らせない」**ために、取得タスクを一定の単位にグループ化したFor-eachタスクで並列化しました。1つひとつのタスクが短く済むので、失敗してもそのグループだけをやり直せますし、進捗も見えるようになります。並列度は環境やデータソース側のレート制限に応じて調整が必要です。
ここで初めて、ドキュメントで名前を見ただけだったJobsの機能が、実感として繋がりました。
-
depends_onは、処理を分けたときに順序と依存関係を保証するためにある - For-eachタスクは、大量の対象を短いタスクに割って並列に処理するためにある
-
timeout_secondsは、暴走したタスクを止めるための安全装置であり、設計の粒度を映す鏡でもある
失敗する前は、どれも「そういう機能があるらしい」という知識でした。「これのためにあったのか」と思えたのは、失敗した後です。
本番運用に向けた仕上げ:初回だけでなく、継続運用の設計
全量取り込みが終わっても、運用の設計はまだ終わっていません。元のデータは、その後も更新され続けるからです。
初回以降は差分更新に切り替える
初回の全量取り込みが完了した後は、データソース側の変更イベントを、ストリーム位置を起点に読み進める差分更新に切り替える方針にしました。毎回全件を列挙し直す必要はなくなります。
取りこぼしに備えて、月次で軽量な整合性チェック
ただし、差分更新は取りこぼしがゼロだという保証がありません。イベントを使うなら、取りこぼしが起きうる前提で設計するべきだと考えました。
そこで、月次で次の運用を行うことにしました。
- 列挙のみを行う(取得・保存はしない)
- メタデータのテーブルと突き合わせ、整合性を確認する
これは、最初のつまずきで得た教訓の応用です。列挙は軽い処理なので、切り出しておけば定期的に回せます。列挙と突合を独立したタスクにしていたからこそ、月次の整合性チェックとして再利用できました。
| フェーズ | 方式 | 目的 |
|---|---|---|
| 初回 | 列挙 → 突合 → 取得(For-eachで並列) | 全量取り込みと漏れの解消 |
| 日常 | 変更イベントをストリーム位置起点で読む差分更新 | 変更の追従 |
| 月次 | 列挙のみ + メタデータテーブルとの突合 | 取りこぼしの検知 |
まとめ:検証と本番運用の間にある溝の正体、そして慢心への反省
溝の正体
今回感じた「検証と本番運用の間の溝」を、自分なりに整理すると次の3つです。
- 検証の規模を、本番の規模にそのまま延長してはいけない:数万件で動いたからといって、数十万件でも動くとは限らない
- 「動く」と「運用できる」は別物:途中経過が見えない、続きから再開できない、漏れを機械的に特定できない、といった問題は検証では表に出ない
- 処理の性質ごとに分ける:軽い処理と重い処理を分けることで、失敗の切り分け、再実行、継続運用がすべて楽になる
(※これは今回の経験からの私の解釈です。一般論として言い切れるかどうかは分かりません。)
慢心への反省
「AIに任せれば余裕」と思っていた私は、AIを使いこなしていたのではなく、AIに判断を丸投げしていただけでした。ジョブが落ちるたびにAIへ原因分析と修正を指示していましたが、自分で1次情報から原因を掘り下げる前に「直しては動かす」を繰り返す対症療法になっていたのです。
突発対応に追われ、その場その場でタスクを見つけては修正する状況ではありました。ただ、それを理由に仕様確認を怠っていたのは、忙しさのせいではなく慢心だったと今は思います。timeout_secondsが何であり、どこで設定され、何を意味するのか。それを自分が理解していなければ、AIの回答が合っているかどうかを判断できません。原因の切り分けにも、設計の良し悪しの評価にも、結局は自分の中の土台が必要でした。
AIは強力ですが、土台のない人には、もっともらしい回答を出してくれる存在にしかなりません。土台があるからこそ、AIの回答を検証し、活用できます。
もし今、Jobsを「機能としては知っている」段階の人がいたら、小さくてもいいので、実際の規模感のあるデータで動かしてみてください。そして、うまくいかなかったときに「なぜ落ちたのか」を、AIに聞く前にまず自分の言葉で説明できるところまで踏み込むことをおすすめします。私はそれを、失敗してから知りました。
