https://logmi.jp/tech/articles/326425

マイクロサービス間でのデータ同期方法

榎本悠介氏(以下、榎本):実際にデータ同期をどうやるか、マイクロサービス間でどうやってデータを同期させるかという話に移ります。前提として、プライベートAPIを叩いてデータを同期するのは悪手だと思っています。

申請された情報を渡して請求書レコードを作らせる、申請情報を入れる時に、(スライドを指の)バクラク請求書部分がAPIになっていると同期処理になってしまうので、もともとやりたかった、ただ申請をしたかっただけの人にとっては遅くなったり、他の障害に引きずられて本来の動作ができなくなったりする。

申請をしたかっただけなのに、なぜか請求書側の事情で止まってしまうなど、トランザクションの境界が分かれているので、こっちが失敗した時にリトライの制御が大変です。結論としては、非同期処理をするのがいいと思っています。queueか何かをかますなどして、エンキューして(スライドの)バクラク請求書までの部分でデキューして、ダメだったらリトライして、結合度を下げるのがいいと思います。

エンキューするパターンにもいろいろあります。3つのうち、1つ目を僕はPush型と呼んでいますが、queueの中に申請の実体を丸ごと入れてUPSERTするものです。シンプルで当たり前という感じですが、必要なデータを丸ごとエンキューしてUPSERTすることをPush型と呼んでいます。そうすると(スライドを指して)上から下への依存が発生します。

次にPull型。エンキューする内容を変えます。申請IDだけをエンキューして「申請の丸っとしたペイロードは聞きに来い」という感じで、APIを1回かまして内容をUPSERTする。IDだけエンキューして、実体は一度取りに来させる。

(これの)何がうれしいかというと、強い依存関係が上から下ではなく下から上に変わるんです。もちろん一見循環参照しているんですが、IDだけをエンキューしているので許してもらって、強い依存関係を下から上に変えることができます。

最後に仲介型です。これが一番複雑なのですが。新しいコンポーネントを作ってしまい、これにIDだけを渡して、先ほどと同じようにIDを元に実体を取って来て、その実体をUPSERTしたいリクエストに投げる。これをかますことで、プロダクト間での依存関係はあまり生まれない。強い依存関係はコンポーネント(Data Sync)を元にしているので、2つのプロダクト間にはあまり生まれない方法があり得ると思います。

まとめると、依存関係がどう生まれるかや、コンポーネントの数や複雑さが違うと思っています。どれがいいかはかなりドメインに依存すると思っており、将来ありそうな要件をベースに考えていきました。

開発を進める中で出てきた追加要件

細かくなりますが、バクラク請求書側には、申請する時にマスターデータを入れたいという話が出てきました。会計上の部門です。それを申請の中に入れて表示することで、どの部門で使われた費用かがわかる。管理会計上どの部門が作った費用かがわかるうれしさがあります。

やり方はシンプルで、申請を表示したい時、申請には部門IDがひもづいているので、それを元に部門の実体を取得して表示する。今回のプロダクトは申請から請求書という依存が発生しやすいと理解しました。

そのため結論として、発生しやすい申請から請求書の依存に対応しやすいPush型、一番シンプルなものにしました。まるっとペイロードを入れるパターンです。ここまでがひととおりりの循環参照を避ける旅でした。

補足しますが、データの置き場所はとても大事です。先ほど、マスターデータはバクラク請求書が持っていると話しましたが、そもそもマスターデータを共通管理の場所(一番下のレイヤー)に置いておけば、依存は発生しないはずです。

そのため、要件によっては各プロダクトから参照されやすいマスターデータを共通基盤に逃がしてやる手もあります。どこから更新されるか、どこから参照したくなるかなど、あらゆるユースケースを想像しないと誰の持ち物かが決まらないので、事前に決めるにはメチャクチャなドメイン知識とエスパー力がいる。

設計レベルでも、事前にドメインエキスパートと相談してやっていました。実際に、ミスしたと思って後からマスターデータをマイクロサービス間で引っ越ししたこともあります。

さらに補足ですが、不整合が起こるリスクがあるので、リトライやデータ同期のジョブを監視するのは当たり前ですが、最終的にデータの不整合がないか監視するものを立てて、何かあったらアラートを飛ばす仕組みを入れています。ここまでが循環参照を避ける話でした。

レプリケーション