はじめに
メリークリスマス!!
はてなのエンジニアのid:shallow1729です。この記事ははてなエンジニア Advent Calendar 2019の25日目の記事です。昨日は
id:hitode909さんの以下の記事でした!
blog.sushi.money
今回はタイトルにある「異なるシステム間での大量データの同期」について、仕事でいくつか関わったので悩んだ事をログに残そうと思います。一緒に難しいねって笑ってくれたら嬉しいです。
「異なるシステム間での大量データの同期」って何のこと?
これは自分がつけた名前なのですが、例としてはシステムを完全にリニューアルするとかでDBのスキーマを再設計したシステムにデータを移すような場合や、あるシステム上の大量のデータをスプレッドシートのようなものに同期させてデータ分析をするようなケースを想定しています。前者のようなシステム移行は一度きりの同期でよくて、後者の場合はどの程度リアルタイム性が必要かによりますが定期的に同期が必要です。
- 同期元のデータを正しく変換する事
- 同期を短時間で済ませる事
- データの整合性を保つ事
前者については、データを正しく変換するには同期元と同期先のデータの両方を理解する必要があるという点で難しいです。一般に一つのシステムのテーブル構成を理解するのはすごく大変ですし、利便性を考えてあまり直感的で無いスキーマ設計をする事もあると思うので、そういうコンテキストも含めてある程度理解しないとうまく変換が出来ないです。また、テーブルのデータを丸ごと同期するような場合はうまく実装しないと数日では済まないようなものになってしまったりします。最後に、そのような大量のデータを同期しつつ、全体として整合性を保つのは非常に困難です。(困難な理由はあとで説明します。)こういう感じでとても難しいのですが、難しいなりに考えたことをまとめていきます。
同期の仕組みの設計
とはいえ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分のデータは消えてしまうことになります。
差分同期と一括同期
データ同期のさせ方としては、同期させるタイミングで都度全てのデータを一気に同期する一括同期と、事前に大半のデータを同期させておいて、都度差分だけを同期する方法があります。差分同期のメリットとしては前回の同期からの差分だけを同期させれば良いので短時間で処理が完了する事です。ですが差分同期は特にデータの削除が行われる場合実装が難しいので、使えるタイミングは限られると思います。
差分同期
差分同期(データは作成しかされない場合)
データが更新、削除しない場合差分同期は非常に有効な手段になると思います。 例えば毎日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年もよろしくお願いいたします!