異なるシステム間での大量データの同期について

はじめに

メリークリスマス!!
はてなのエンジニアのid:shallow1729です。この記事ははてなエンジニア Advent Calendar 2019の25日目の記事です。昨日はid:hitode909さんの以下の記事でした! blog.sushi.money

今回はタイトルにある「異なるシステム間での大量データの同期」について、仕事でいくつか関わったので悩んだ事をログに残そうと思います。一緒に難しいねって笑ってくれたら嬉しいです。

「異なるシステム間での大量データの同期」って何のこと?

これは自分がつけた名前なのですが、例としてはシステムを完全にリニューアルするとかでDBのスキーマを再設計したシステムにデータを移すような場合や、あるシステム上の大量のデータをスプレッドシートのようなものに同期させてデータ分析をするようなケースを想定しています。前者のようなシステム移行は一度きりの同期でよくて、後者の場合はどの程度リアルタイム性が必要かによりますが定期的に同期が必要です。

f:id:shallow1729:20191224210106j:plain
図1 単に同期元のデータを全て同期するのではなく、間で変換して同期させる
このような問題では二つのシステムのDBのスキーマが異なるのでただ同期元のデータをダンプして同期先に渡すのではなく、間にデータ変換の仕組みも必要です。このような同期は一般に以下の点で難しいです。

  • 同期元のデータを正しく変換する事
  • 同期を短時間で済ませる事
  • データの整合性を保つ事

前者については、データを正しく変換するには同期元と同期先のデータの両方を理解する必要があるという点で難しいです。一般に一つのシステムのテーブル構成を理解するのはすごく大変ですし、利便性を考えてあまり直感的で無いスキーマ設計をする事もあると思うので、そういうコンテキストも含めてある程度理解しないとうまく変換が出来ないです。また、テーブルのデータを丸ごと同期するような場合はうまく実装しないと数日では済まないようなものになってしまったりします。最後に、そのような大量のデータを同期しつつ、全体として整合性を保つのは非常に困難です。(困難な理由はあとで説明します。)こういう感じでとても難しいのですが、難しいなりに考えたことをまとめていきます。

12/25追記: こういったタスクにはETLという名前があるそうです。そちらで検索するといろいろ詳しい知見が得られそうです!

同期の仕組みの設計

f:id:shallow1729:20191224210722j:plain
図2 上は同期元の開発チームにAPIを提供してもらい、それを介してデータを取得する場合、下は直接同期元のデータを見に行く場合
同期を行うために、同期元のDBからデータを取得して同期先のDBにデータを送るまでの構成を考えます。パッと考えられるのは図2のように間に同期元のAPIを挟む構成と挟まない構成です。同期元のシステム開発チームと同期先のシステム開発チームの関係もいろいろあると思っていて、同じチームで完結する場合もあれば他の部署や会社のチームの事もあるはずで、状況に応じてどちらがいいかは変わるのかなと思います。同期元にとってAPIを提供した方が良い理由としては、APIによってある程度同期元で自由に開発ができたり負荷のコントロールができる点です。つまり、APIで渡すデータの種類や形式を決めてしまえば他はDBのスキーマを含めて修正する事ができます。また、APIのリクエスト頻度などを制限する事で同期によって負荷が大きくなってシステムに障害が発生する事も防げます。このような仕組みは特にデータ同期を行なっている間もシステムを稼働させる必要がある場合には有用だと思います。また、同期先にとっても同期元の生のDBではなくAPIで渡されるデータを見れば良いのでテーブル構成の理解が少なくて済みます。
とはいえAPIの設計はコミュニケーションも開発工数も非常に多くかかるものだと思うので、直接DBを見に行くという選択も十分あると思います。状況に応じて考える、で良いと思います。

データの同期方法

データの不整合が発生する仕組みと解決策

APIを介するにしても同期元のDBを直接見に行くにしても、大量のデータを整合性を保って取得することは非常に困難です。例えばある日の22時00分に、前日の22時00分から先に更新された全てのレコードを取得する場合を考えます。この場合前日の22時00分から当日の22時00分までのデータが取得されるのが期待できます。ですが、実際にはリクエストを順番にさばくためにあるテーブルのデータを取得し始めるのが22時05分からだとすると、あるデータは22時00分のもの、別のものは22時05分のものになります。これをある程度解決する方法としては検索範囲を前日の22時00分から当日の22時00分までとして取得することです。(つまり、リクエストの範囲について後ろも指定する。)この場合でも例えば22時05分にあるデータを取得し始めた場合、もし22時00分から22時05分の間に更新が行われた場合には22時00分のデータは消えてしまうことになります。

f:id:shallow1729:20191224214409j:plain
図3 22時00分の時点のデータを同期するつもりが22時03分に更新されてしまったのでそれ以降のタイミングでは22時00分のデータがすでに消えてしまっている
このように、大量のデータを整合性を持つように取得するのは非常に難しいです。解決策としてはいくつか考える事ができて、一つは同期中同期元のDBへの書き込みを禁止する事です。一度きりのシステム移行ならそれで良いですが、定期的に同期を行う場合は短時間でもちょっと厳しいです。別の方法としては同期元が変更履歴を持つという方法もあります。履歴の持ち方としてはgitのようにリビジョンが溜まっていくような仕組みが考えられますが、このようなテーブルを作るという選択はなかなか相談する事が多いと思います。

差分同期と一括同期

データ同期のさせ方としては、同期させるタイミングで都度全てのデータを一気に同期する一括同期と、事前に大半のデータを同期させておいて、都度差分だけを同期する方法があります。差分同期のメリットとしては前回の同期からの差分だけを同期させれば良いので短時間で処理が完了する事です。ですが差分同期は特にデータの削除が行われる場合実装が難しいので、使えるタイミングは限られると思います。

差分同期

差分同期(データは作成しかされない場合)

データが更新、削除しない場合差分同期は非常に有効な手段になると思います。 例えば毎日22時のデータを同期する場合は前日の22時に作成されたデータからその日の22時までに作成されたデータを同期すれば良いです。 また、s3やcloud storageのオブジェクトを他のバケットにコピーするような場合は、作成日時のような情報で範囲指定はできないですが、単純にバケットのデータのキーのリストを同期先のデータのキーのリストと比較して差分をインポートするという実装でも、都度全て同期するよりはずっと短時間で完了します。(当たり前といえば当たり前ですが)

差分同期(データの更新も行われる場合)

更新があった場合は先ほどのデータ不整合の仕組みで説明したような事が起きてしまいます。差分同期の場合の先ほど紹介しなかった他の解決策としては、同期元のシステムから更新リクエストを都度もらうという手もあるように思います。あとは、システム移行のように最後に一度同期が取れておけば良いという場合や、多少データ不整合があっても良いという場合には先ほど説明したような比較的起こりにくいデータ不整合は許容するという手もあると思います。

差分同期(データの削除が行われる場合)

差分同期の難しい点はデータの削除が行われた場合にそれを同期するのが困難な点です。前日に22時以降に更新されたデータを同期する、という条件で同期する場合に前日の22時以前に作成されて同期済みのデータが削除されたとすると、このデータの削除を同期する事ができません。なので、仕組みとしては削除履歴のようなものを同期元に用意するか、削除リクエストを同期元からもらう必要があると思います。あとは毎回全てのデータについてユニークキーを比較して消えてるものを見つけて削除する、という事もできると思います。

一括同期

一括同期は前回同期したデータを全て消して再度全て同期するような実装です。あるいは、システム移行の場合は一度に全てを移行するような実装です。 一括同期であっても同期中に同期元のDBの書き込みがあったりするとデータの不整合は生じてしまうのですが、特に削除される事のあるデータの場合差分同期より実装が楽なので、データがむちゃくちゃ多すぎてどうしようもない時でなければ一括同期は良い選択だと思います。

その他

パフォーマンスチューニング

今回話しているようなケースの場合、データのINSERTが大量にあるので積極的にバルクインサートを使ってN+1を解決するのが重要かなと思います。SELECTの時のN+1は身近な気がしますけど、INSERTでもN+1はかなり強烈で、実際これを解決するだけで10倍程度速度改善できた事もあります。他にはUUIDを払い出す場合もSELECT文が都度発行されるのでN+1にならないように一括で取得すると結構効いたりします。

ページング

APIにしてもDBから直接データを取るにしても、あまり大量のデータを取得しようとするとネットワークのタイムアウトなどがありうるので考慮しておく必要があります。SQLであってもAPIであってもオフセット法やシーク法などの実装があると思います。

エラーログと再実行の処理

システム移行の場合はデータの同期に数時間から数日かかる可能性もあります。そのような時に想定外の事が起きてエラーで止まった場合に備える事が必要です。備えとして必要なことは

  • 問題を認識できるようにログを出力すること
  • 同期処理を再度実行できるようにすること(トランザクションを適切に貼る)
  • 同期処理を途中から実行できるようにすること(一回のトランザクションの粒度を考える)

などです。 パフォーマンスの点では一つのトランザクションが大きい方がN+1の問題を解決できて良いですが、あまり大きすぎて一つの実行に時間がかかりすぎると途中で失敗した時につらくなるので適切な粒度を考える必要があるかなと思います。

あとがき

以上、異なるシステム間での大量データの同期について、自分なりに考えたことをまとめました。拙い文章ですが最後まで読んでくださってありがとうございます。 2020年もよろしくお願いいたします!