2025年4月20日·1分で読めます

チェックポイントによる増分データ同期:システムを安全に整合させる

チェックポイントを使った増分データ同期は、カーソル、ハッシュ、再開トークンで安全に再開できるようにし、再インポートを避けてシステムの整合性を保ちます。

チェックポイントによる増分データ同期:システムを安全に整合させる

なぜ全件再インポートは問題を繰り返すのか

フル再インポートは見た目には簡単に見えるので安心に感じます:削除して、再ロードして、終わり。実際には、遅い同期、コスト増、データの混乱を生む最も簡単な方法の一つです。

最初の問題は時間とコストです。毎回全データセットを取得すると、同じレコードを何度もダウンロードします。毎晩50万件の顧客を同期している場合、実際に変更が200件しかなくても、計算、APIコール、データベース書き込みに対して支払いが発生します。

二つ目の問題は正確性です。フル再インポートは重複を生んだり(マッチングルールが完璧でないため)、エクスポートに含まれていた古いデータで新しい編集を上書きしてしまうことがあります。多くのチームは「削除して再ロード」が途中で静かに失敗して合計がずれていくのを経験します。

典型的な症状は次の通りです:

  • 実行後にシステム間で件数が合わない
  • レコードが小さな差異(メールの大文字小文字、電話番号の書式)で二重に存在する
  • 最近更新したはずのフィールドが古い値に戻る
  • 同期が「完了」するがデータの一部を取りこぼす
  • 各インポート後にサポートチケットが急増する

チェックポイントは「ここまでは処理した」という小さな保存マーカーに過ぎません。次回はそのマーカーから続ければよく、最初からやり直す必要はありません。マーカーはタイムスタンプ、レコードID、バージョン番号、APIが返すトークンなどになり得ます。

もし本当の目的が二つのシステムを長期的に揃えておくことであれば、チェックポイントを使った増分データ同期の方が通常は適切です。データが頻繁に変わる、エクスポートが大きい、APIにレート制限がある、またはジョブが途中で失敗した場合に安全に再開する必要があるときに特に役立ちます(例えば、AppMasterのようなプラットフォームで社内ツールを作った場合など)。

方法を選ぶ前に同期の目的を定義する

チェックポイントによる増分同期は、「正しい」とは何かが明確でないと上手く動きません。これを飛ばしていきなりカーソルやハッシュに進むと、ルールが書き留められておらず後で同期を作り直す羽目になります。

まずシステムの名前を挙げ、どちらが真実の所有者(source of truth)かを決めます。例えば、CRMが顧客名や電話番号の正しい情報で、請求ツールがサブスクリプション状態の正しい情報かもしれません。両方のシステムが同じフィールドを編集できるなら、単一のソースオブトゥルースはなく、競合処理を計画する必要があります。

次に「整合している」とは何かを定義します。常に完全一致が必要ですか、それとも数分以内に更新が反映されれば十分ですか?完全一致は順序を厳密に保つこと、チェックポイント周りの強い保証、削除の慎重な扱いを意味することが多いです。最終的整合性(eventual consistency)は通常コストが低く、一時的な失敗に寛容です。

同期の方向性も決めます。一方向の同期は単純です:システムAがシステムBへ送る。双方向同期は難しく、すべての更新が競合になる可能性があり、片方がもう片方を「直す」無限ループを避けなければなりません。

構築前に答えるべき質問

全員が合意する簡単なルールを書き出しましょう:

  • 各フィールド(または各オブジェクトタイプ)についてどのシステムが真実の所有者か?
  • 許容できる遅延はどの程度か(秒、分、時間)?
  • これは一方向か双方向か。どのイベントがどちらの方向に流れるか?
  • 削除はどう扱うか(ハードデリート、ソフトデリート、トゥームストーン)?
  • 双方が同じレコードを変更したときはどうするか?

実用的な競合ルールは「課金関連フィールドはbillingが勝つ、連絡先フィールドはCRMが勝つ、それ以外は最新の更新が勝つ」のようにシンプルでよいです。AppMasterのようなツールで統合を構築するなら、これらのルールをBusiness Processのロジックに記録して可視化・テスト可能にしておくと、誰かの記憶に頼るより安全です。

カーソル、ハッシュ、再開トークン:基本要素

チェックポイントを使った増分同期は通常、保存して再利用できる三つの「位置」のいずれかに依存します。どれを選ぶかは、ソースシステムが何を保証できるか、どのような障害に耐えたいかによります。

カーソル型チェックポイントは最も単純です。「最後に処理したもの」、例えば最後のID、updated_atのタイムスタンプ、シーケンス番号などを保存します。次回はそのポイントより後のレコードを取得します。ソースが一貫してソートされ、IDやタイムスタンプが確実に前に進むならうまく機能します。遅延更新が来る、時計がずれている、過去に挿入される可能性がある(バックフィルの例)場合には破綻します。

ハッシュはカーソルだけでは不十分なときに変更を検出するのに役立ちます。関心のあるフィールドに基づいて各レコードをハッシュし、ハッシュが変わったときだけ同期することができます。あるいはバッチ全体をハッシュしてドリフトを素早く検出し、問題があれば詳細に調べます。レコード毎のハッシュは正確ですが保存と計算コストが増えます。バッチハッシュは安価ですが、どのアイテムが変わったか分かりにくいです。

再開トークンは、ソースが発行する不透明な値で、ページネーションやイベントストリームでよく使われます。解釈せずに保存して戻すだけです。APIが複雑な場合に便利ですが、トークンが期限切れになったり、保持期間の後に無効になる、環境ごとに挙動が異なる、といった問題があります。

何を使うべきか、そして何が起こり得るか

  • カーソル:高速で単純だが、順序が崩れると危険。\n- レコード単位ハッシュ:正確な変更検出だがコスト高。\n- バッチハッシュ:安価なドリフト信号だが特定は難しい。\n- 再開トークン:ページネーションでは安全だが期限切れや1回限りの扱いになることがある。\n- ハイブリッド(カーソル+ハッシュ):updated_atが完全に信頼できない場合によく使われる。

AppMasterのようなツールで同期を構築する場合、これらのチェックポイントは小さな「sync state」テーブルに格納され、各実行が推測なしで再開できるようにするのが一般的です。

チェックポイント保存の設計

チェックポイント保存は、チェックポイントを使った増分同期を信頼できるものにする小さな部分です。読みづらい、上書きされやすい、特定のジョブに紐づいていないといった状態だと、ある時点までは問題なく見えても一度失敗すると推測で対応する羽目になります。

まず、チェックポイントをどこに置くかを選びます。データベースのテーブルが最も安全です。トランザクション、監査、簡単なクエリをサポートします。キー・バリュー ストアでも原子更新をサポートしていれば使えます。設定ファイルは単一ユーザーや低リスクの同期でしか現実的ではありません。ロックが難しく紛失しやすいためです。

何を保存するか(なぜか)

チェックポイントは単なるカーソル以上の情報を持つべきです。デバッグ、再開、ドリフト検出に十分なコンテキストを保存しましょう:

  • ジョブの識別:ジョブ名、テナント/アカウントID、オブジェクトタイプ(例:customers)
  • 進捗:カーソル値または再開トークン、加えてカーソルの種類(time, id, token)
  • ヘルス指標:最終実行時刻、ステータス、読み取った件数・書き込んだ件数
  • 安全性:最後に成功したカーソル(試行した最後のカーソルではなく)、最新の失敗の短いエラーメッセージ

変更検出ハッシュを使う場合は、ハッシュ方法のバージョンも保存してください。そうしないと後でハッシュを変えたときに、すべてが「変更あり」と誤認される可能性があります。

バージョニングと多数の同期ジョブ

データモデルが変わるとチェックポイントもバージョン管理する必要があります。最も簡単な方法は schema_version フィールドを追加し、新しいバージョン用に新しい行を作成することです。古い行はしばらく保持してロールバックできるようにします。

複数ジョブがある場合は名前空間を分けます。良いキーの例は (tenant_id, integration_id, object_name, job_version) です。これで二つのジョブが1つのカーソルを共有してデータを静かに飛ばすという古典的なバグを避けられます。

具体例:内部ツールをAppMasterで作るなら、PostgreSQLにテナントとオブジェクトごとに1行ずつチェックポイントを保存し、成功したバッチコミットの後だけ更新する、という運用がわかりやすいです。

ステップバイステップ:増分同期ループの実装

同期バックエンドを早く出荷する
手作業でGoを書くことなく、統合用の本番向けバックエンドを生成します。
バックエンドを作成

チェックポイントを使った増分同期は、ループがつまらなく予測可能であるほどうまく動きます。目標は単純です:安定した順序で変更を読み、安全に書き込み、書き込みが確実に終わったときだけチェックポイントを前に進めること。

信頼できるシンプルなループ

まず、同じレコードに対して決して変わらない順序を選びます。タイムスタンプは使えますが、同時刻で並ぶ更新をシャッフルしないようにタイブレーカー(例えばID)を必ず含めてください。

ループは次のように動かします:

  • カーソル(例:last_updated + id)とページサイズを決める。
  • 保存したチェックポイントより新しい次のページのレコードを取得する。
  • 各レコードをターゲットにアップサート(存在しなければ作成、あれば更新)し、失敗を捕捉する。
  • 成功した書き込みをコミットし、最後に処理したレコードから新しいチェックポイントを永続化する。
  • 繰り返す。ページが空ならスリープして再試行。

フェッチとチェックポイント更新は分離してください。チェックポイントを早く保存しすぎると、クラッシュでデータを静かにスキップしてしまいます。

重複を作らないバックオフとリトライ

呼び出しは失敗することを前提に設計します。フェッチや書き込みが失敗したら短いバックオフ(例:1s、2s、5s)と最大リトライ回数で再試行します。リトライを安全にするためにアップサートを使い、書き込みを冪等にします(同じ入力なら同じ結果になる)。

小さな実例:顧客更新を毎分同期するなら、一度に200件ずつ取得してアップサートし、最後に処理した顧客の (updated_at, id) を新しいカーソルとして保存します。

AppMasterで作る場合、チェックポイントは単純なテーブル(Data Designer)でモデル化し、Business Processで取得・アップサート・チェックポイント更新を一連の制御されたフローとして実行するのが分かりやすいです。

再開を安全にする:冪等性と原子的なチェックポイント

同期が再開できるようにすると、再開は最悪のタイミングで発生します:タイムアウト、クラッシュ、部分的なデプロイの後などです。目的は単純で、同じバッチを再実行しても重複や更新の喪失が起きないことです。

冪等性は安全網です。繰り返しても最終結果が変わらない書き方をします。実務上は挿入(insert)ではなくアップサート(存在すれば更新、なければ挿入)を使うことが多いです。

良い「書き込みキー」はリトライ中でも信頼できるものです。一般的にはソースシステムの自然IDか、最初に見たときに作る合成キーが使われます。ユニーク制約を付けて、二つのワーカーが競合してもDB側でルールを強制するのが安全です。

チェックポイントの原子性も同じくらい重要です。データがコミットされる前にチェックポイントを進めると、クラッシュでレコードを永久にスキップしてしまう可能性があります。チェックポイント更新をデータ書き込みと同じ単位の作業として扱いましょう。

増分同期のシンプルなパターン:

  • 最後のチェックポイント(カーソルまたはトークン)以降の変更を読む。\n- 各レコードを重複除去キーでアップサートする。\n- トランザクションをコミットする。\n- その後でのみ新しいチェックポイントを永続化する。

順序が前後する更新や遅れて到着するデータもよく起きます。あるレコードは10:01に更新されても10:02のレコードより後で届くことがあります。ソースから古い変更がリトライで届くこともあります。これに対処するには、ソースの last_modified を保存して「最後に書かれたものが勝つ(last write wins)」ルールを適用し、受信データが既存より新しい場合にのみ上書きするようにします。

より強い保護が必要なら、小さなオーバーラップ窓(例えば直近数分を再読)を持ち、冪等なアップサートで重複を無視する方法が有効です。手間は少し増えますが、再開がつまらなくなる(=安定する)ので価値があります。

AppMasterでは同じ考え方をBusiness Processフローにマップできます:まずアップサートロジックを実行してコミットし、最後のステップでカーソルや再開トークンを保存します。

増分同期を壊す一般的なミス

同期状態テーブルを作る
AppMaster Data DesignerでPostgreSQLに同期状態をモデル化しましょう。
構築を開始

大抵の同期バグはコードの問題ではありません。安全に感じるいくつかの仮定が本番データで崩れることで発生します。チェックポイントによる増分同期を信頼できるものにするには、次の落とし穴に早めに注意してください。

よくある失敗ポイント

updated_at を過信することはよくあるミスです。一部のシステムはバックフィル、タイムゾーン修正、バルク編集、あるいは読み取り修復の際にタイムスタンプを書き換えます。カーソルがタイムスタンプだけだと、タイムスタンプが後退してレコードを見逃したり、急に前進して大量を再処理したりします。

別の落とし穴はIDが連続しているとか単調増加だと仮定することです。インポート、シャーディング、UUID、削除された行などによりその想定は破綻します。「最後に見たID」をチェックポイントにするとギャップや順序の逆転でレコードを取りこぼします。

最も有害なのは、部分成功の状態でチェックポイントを進めてしまうことです。例えば1,000件を取得して700件を書き込み、クラッシュしたにもかかわらずフェッチの「次のカーソル」を保存してしまうと、残りの300件は再試行されません。

削除も無視しがちです。ソースはソフトデリート(フラグ)、ハードデリート(行削除)、あるいは「非公開」にする(ステータス変更)など様々です。アクティブなレコードだけをアップサートしていると、ターゲットは徐々にずれていきます。

最後に、スキーマ変更は古いハッシュを無効にします。ハッシュがあるフィールドセットに基づいていた場合、フィールドを追加・リネームすると「変更なし」が「変更あり」に見えたり逆に見えたりします。ハッシュロジックはバージョン管理しましょう。

安全なデフォルト:

  • 可能なら単調増加するカーソル(イベントID、ログポジション)を生のタイムスタンプより好む。\n- チェックポイント書き込みをデータ書き込みと同じ成功境界に置く。\n- 削除は明示的に追跡する(トゥームストーン、ステータス遷移、定期的な照合)。\n- ハッシュ入力はバージョン管理し、古いバージョンを読めるようにする。\n- ソースが順序を入れ替える可能性があるなら小さなオーバーラップ窓を追加する(最後のN件を再読)。

AppMasterで構築する場合、チェックポイントをData Designerでテーブル化し、「データ書き込み+チェックポイント書き込み」を単一のBusiness Process実行にまとめておけば、リトライで作業をスキップするリスクを減らせます。

監視とドリフト検出(ノイズを増やさない方法)

長期的な技術的負債を避ける
統合が大きくなっても所有権を保てるよう、実際のソースコードを生成して技術的負債を避けます。
コードをエクスポート

チェックポイントによる増分同期の良い監視は「ログを増やすこと」ではなく、各実行で信頼できる少数の数値を持つことです。「何を処理したか、どれくらい時間がかかったか、どこから再開するか」が答えられればほとんどの問題は数分で調査できます。

まず、同期が実行されるたびに一つの簡潔なラン記録を残すことから始めましょう。これを一貫しておけば実行を比較して傾向を見つけやすくなります。

  • 開始カーソル(または再開トークン)と終了カーソル\n- 取得したレコード数、書き込んだレコード数、スキップしたレコード数\n- 実行時間とレコード当たり(またはページ当たり)の平均時間\n- エラー数と上位のエラー理由\n- チェックポイント書き込みのステータス(成功/失敗)

ドリフト検出は次の層です:両方のシステムが「動いている」ように見えてもゆっくりずれているときに知らせてくれます。合計だけだと誤解を生むので、軽量な合計チェックと小さなサンプル比較を組み合わせます。例えば1日1回、両システムのアクティブ顧客数を比較し、20件のランダムな顧客IDをサンプリングして数フィールド(status, updated_at, email)を確認します。合計が違ってサンプルは一致するなら削除やフィルタが原因の可能性があります。サンプルが違うならハッシュやフィールドマッピングが誤っている可能性が高いです。

アラートは稀であり、行動可能であるべきです。簡単なルール:人が今すぐ対処しなければならないときだけアラートする。

  • カーソルが詰まっている(N回の実行で終了カーソルが動かない)\n- エラー率の上昇(例えば1% -> 5%が1時間で発生)\n- 実行時間が通常の上限を超える\n- バックログが増えている(新しい変更の到着が同期より速い)\n- ドリフトが確認された(合計が2回連続で不一致)

障害後は手動でクリーンアップせずに再実行できるのが理想です。最も簡単なのは最後にコミットされたチェックポイントから再開することです。小さなオーバーラップ窓(最後のページを再読)を使う場合は、書き込みを冪等にして stable ID でアップサートし、書き込み成功後にのみチェックポイントを進めます。AppMasterではこれらのチェックをBusiness Processに組み込み、メール/SMS/Telegramモジュールで可視化することが多いです。

本番投入前のクイックチェックリスト

チェックポイントを使った増分同期を本番化する前に、遅れて発生する驚きを防ぐための数点を数分で確認しましょう。

実運用でよく問題を起こすチェックリスト:

  • 順序付けに使うフィールド(タイムスタンプ、シーケンス、ID)が本当に安定しており、ソース側にインデックスがあるか確認する。後から変わる可能性があるとカーソルがずれる。\n- アップサートキーが一意であること、および両システムが同じ扱い(大文字小文字、トリミング、書式)をしていることを確認する。片方が"ABC"で他方が"abc"だと重複が発生する。\n- チェックポイントは各ジョブ・各データセットごとに分離して保存する。"グローバルな最後のカーソル"は簡単そうだが、テーブルが二つ、テナントが二つ、フィルタが二つになると壊れる。\n- ソースが最終的一貫性なら小さなオーバーラップ窓を追加する。例えば last_updated = 10:00:00 から再開する場合、09:59:30から再開して冪等なアップサートで重複を無視する。\n- 軽量な照合を計画する:定期的に100件程度のランダムサンプルを取り、主要フィールドを比較して静かなドリフトを検出する。

現実テスト:同期を途中で一時停止し、再起動して同じ結果になるか確認してください。再起動で件数が変わったり余分な行ができるなら、本番化前に修正してください。

AppMasterで構築する場合、各統合フローのチェックポイントデータを特定のプロセスとデータセットに紐づけ、無関係な自動化と共有しないようにしましょう。

例:二つのアプリ間で顧客レコードを同期する

信頼できる同期実行をスケジュールする
重複書き込みを防ぎつつリトライとバックオフに対応するスケジュール同期を実行します。
ジョブを設定

簡単な設定を想像してください:CRMが連絡先の真実で、サポートツールやカスタマーポータルにも同じ人物が必要なケースです(チケットを実際の顧客に紐づける、ユーザーが自分のアカウントを見るなど)。

初回はワンタイムのインポートを行います。updated_atid をタイブレーカーにして安定した順序でコンタクトを引き、各バッチを書き込んだ後に last_updated_atlast_id のようなチェックポイントを保存します。これが今後の起点になります。

継続運用ではチェックポイントより新しいレコードだけを取得します。更新は単純です:CRMのコンタクトが既にあればターゲットを更新し、なければ作成します。マージは厄介です。CRMは重複をマージして1つの「勝者」コンタクトを残すことが多いです。これを失った方のコンタクトを非アクティブにする(または勝者へマッピングする)などして、ポータルに同一人物のユーザーが2つできないよう扱います。

削除は通常 updated since クエリには現れないので計画が必要です。一般的な選択肢はソースのソフトデリートフラグ、削除専用のフィード、あるいは定期的な軽量照合でIDの欠落を検出する方法です。

障害ケース:同期が途中でクラッシュしたとします。バッチの終わりにしかチェックポイントを保存していないと、大量を再処理する羽目になります。代わりにバッチごとの再開トークンを使う方法:

  • ラン開始時に run_id(再開トークン)を生成する\n- バッチを処理し、ターゲットへの変更を書き、次に run_id に紐づけてチェックポイントを原子的に保存する\n- 再起動時に最後に保存された run_id のチェックポイントを検出して続行する

成功は地味に見えます:日々の件数が安定し、実行時間が予測可能で、同じウィンドウを再実行しても予期せぬ変更が発生しないことです。

次のステップ:パターンを選び、手戻りを減らして構築する

最初の増分ループが動いたら、手戻りを避ける最速の方法は同期ルールを書き留めることです。短くまとめてください:対象レコード、競合時に勝つフィールド、各実行での「完了」の定義。

小さく始めましょう。一つのデータセット(例:customers)を選んで、初回インポート、増分更新、削除処理、意図的な障害後の再開を順にテストします。想定を後で修正するより今直す方が楽です。

それでもフルリビルが正しい場合があります。チェックポイント状態が壊れたとき、識別子を変更したとき、またはスキーマ変更で変更検出が壊れたとき(例:ハッシュの元になるフィールドの意味が変わった場合)には再インポートを検討します。再インポートは緊急ボタンではなく、管理された操作として扱ってください。

安全な再インポート手順例:

  • シャドウテーブルや並列データセットにインポートし、現在のデータセットは稼働したままにする。\n- 件数とサンプル(NULL、マージされたレコードなどのエッジケースを含む)を検証する。\n- リレーションをバックフィルし、計画的な切り替えでリーダーを新データセットへ切り替える。\n- 古いデータセットは短いロールバックウィンドウのために保持し、その後クリーンアップする。

コードを書かずにこれを作りたいならAppMasterが役立ちます:Data DesignerでPostgreSQLにデータをモデル化し、Business Process Editorで同期ルールを定義し、スケジュールジョブでレコードを取得・変換・アップサートします。AppMasterは要件が変わってもクリーンなコードを再生成するため、「フィールドを一つ追加したい」といった変更のリスクを下げます。

本番でさらに多くのデータセットに拡張する前に、同期契約を文書化し、1つのパターン(カーソル、再開トークン、またはハッシュ)を選び、1つの同期を完全に信頼できるようにしてください。それから次のデータセットへ同じ構造を繰り返しましょう。まずはAppMasterでアプリを作成し、小さなスケジュール同期ジョブを実行して試してみるのが早道です。

始めやすい
何かを作成する 素晴らしい

無料プランで AppMaster を試してみてください。
準備が整ったら、適切なサブスクリプションを選択できます。

始める