マイクロサービス間のデータ同期についての考え方

週末暇なのでサービス間のデータ同期(主に注目の話題)についてのブログに来よう。特定のビジネス シナリオの例はありません。

ps:完全に個人的な戯言ですので、間違いや不足があればご指摘ください。さて、本題に入りましょう。

ビジネスプロセス

主な業務プロセスは以下の通りです。

  • ユーザーが操作してサービス A にデータを保存します。サービス A が保存に成功した後、一部のデータをサービス B に同期します。サービス B はデータを受信して​​正常に保存し、プロセスは終了します。

ここではサービス A->サービス B のデータ同期の問題について説明します。次の 2 点を確認する必要があります。

1. 1 つ目はデータの正確さです (銀行に 10,000 入金しても残高が 100 しか増えないなら、なぜそうしないでしょうか。逆に、100 入金して残高が 10,000 増えた場合、銀行はやってもらえますか?)

バージョン 1 - データの正確性の保証

絵を描くのは時間がかかりすぎるので、完成版を説明するために絵を描いて、ここで言葉で説明しましょう。

サービス A の疑似コード:

分散トランザクションの開始 { 
		  //データ検証、時間は 20 ミリ秒
          //ビジネスを DB に保存、時間は 10 ミリ秒

		  //同期が必要なデータを取得、rpc はサービス B の保存メソッドをリモートで呼び出し、時間は 40 ミリ秒

     }すべて成功した場合はトランザクションを送信します。失敗した場合はトランザクションをコミットします。

サービス B の疑似コード:

//保存の成功には約 40 ミリ秒かかります
 save メソッド { 

 //検証データを分析します、30 ミリ秒

 //検証は成功し、データを保存します、10 ミリ秒

 //検証は失敗し、エラーを返し、サービス A に保存に失敗したことを通知します

 }保存に成功しましたトランザクションを送信し、操作失敗のロールバック トランザクションを実行します。
save メソッド { 
  
  if (ケース 1) { 
    
  }else if (ケース 2){ 
  	
  }else if (ケース 3){ 
  	//リモートで他を呼び出すか、サポートするビジネス データをキャッシュから取得する
  	//データを確認する
  	//さまざまなログを記録する
  	/ /Save data 
  	//Update the status of xx table 
  } 
}正常にコミットされたトランザクションを保存し、操作が失敗した場合はトランザクションをロールバックします

  最終的に、このインターフェイスの速度は 100 ミリ秒未満から 500 ミリ秒を超えました。さらに悪いことに、トラフィックがピークに達すると、多くのユーザーが操作できなくなり、保存が二転三転し、さらにはさまざまなタイムアウトが発生し、ユーザー エクスペリエンスはますます悪化します (Tomcat を例に挙げると、内部処理要求が発生します)。はスレッド プール、リソースは制限されており、前のリクエストのリソースは解放されず、後続のリクエストは拒否されるか待機します)。途中でサーバーを大量に追加しましたが、それでも時々問題が発生し、苦情の手紙が次々と届きました。ある日、上司が技術責任者をオフィスに呼び、「できるのか、出て行け」と言いました。 。技術部長は額の汗をぬぐいながら「わかった、わかった」と言いました。上司:「分かった、解決はやめておこう」!(事の重大さを示しています)

バージョン 2 - 統合メッセージ ミドルウェア (ここでは、rabbitMQ を例として取り上げます)

技術マネージャーは開発者のグループをオフィスに呼び、長時間議論した結果、トラフィックのピーククリッピングの問題を解決できるだけでなく、コードを分離し、さまざまな詳細や注意事項について話し合うことができる特定のミドルウェアを使用することにしました。明確に指摘します。チーム全体が数日間徹夜で働き、特定のメッセージ ミドルウェアを使用して同様のビジネス ポイントを 1 つずつ実装しました。コードはおおよそ次のようになります。

サービス A の疑似コード:

トランザクションの開始 { 
	 //データ検証、時間は 20 ミリ秒
     //ビジネスを DB に保存、時間は 10 ミリ秒
	//メッセージ ミドルウェアへのデータ同期、
	(送信失敗の場合は 20 ミリ秒){ 
		//例外をスローする
	} 
}正常に保存して送信トランザクション、操作はトランザクションのロールバックに失敗します

サービス B の疑似コード:

メッセージ ミドルウェアからデータを取得します {
   実行 保存メソッド
}
保存メソッド { 
        //ビジネス ロジック
} トランザクションを正常に保存して送信し、操作が失敗した場合はトランザクションをロールバックします

コンシューマ側の例外処理と MQ、およびその他の具体的な詳細については、最終バージョンで説明されています。

最初の 2 日間は完璧でした。元の 100ms に戻り、チーム全員が非常に満足しています。今年の年末賞を逃すわけにはいきません。

ところが、ある日突然、「xxxのデータが合わない」というクレームが大量に届きました。このビジネスの責任者である Xiao Wang は愕然としましたが、技術マネージャーが RabbitMq コンソールにアクセスすると、このキューの平均生成量が 1000/s で、消費量が 500/s であることが分かりました。データが蓄積されていました。他の主要なビジネスでも次々とこの問題が発生し、同期が必要な問題が大量に mq に蓄積されました。

サーバーを追加しますか? 生産:消費 = 2:1、上司はこの経費に同意できますか? そして、毎日のトラフィックのピークではなく、ユーザーの数が増加するため、この割合から見ると、金額は決して小さくありません。技術マネージャーは技術マネージャーであり、メッセージを処理するためにマルチスレッドを使用することをすぐに思いつき、担当の開発者を集めて会議を開きました。最終バージョンはこちらです。

最終版

ここでのミドルウェアは例としてrabbitMq、データを保存するデータベースは例としてmysql(innodbエンジン)です。

注1. サービスAはミドルウェアに送信されます

メッセージ ミドルウェアが使用され、生産消費モデルが採用されてプログラム コードが分離され、トラフィック ピーク クリッピングの問題が解決されますが、プログラムの難易度も増加し、データの一貫性の問題も考慮する必要があります。次の問題。

ミドルウェアデータの永続化

MQ がクラッシュしてデータが失われた場合はどうなりますか? 現時点では、構成キュー データの永続性を考慮する必要があります。

発行者確認を使用するかどうか

RabbitMq クライアントの送信メソッドを呼び出して、データがキューに到着するわけではありません。クライアントの API を呼び出すときにデータがブローカーに送信されることのみを保証できますが、交換 (交換には永続化機能がないことに注意してください)、交換からキュー、およびキューへのデータは永続化されました。mq が途中でハングアップすると、メッセージが失われる可能性があります。

別のケースでは、一般に @RabbitListener アノテーションを使用してコンシューマー側でキューを自動的に作成し、そのキューをスイッチにバインドします (または @Bean メソッドを使用します)。プロジェクトの初期化の初期段階 (起動したばかり) では、コンシューマーが開始されていない場合、プロデューサーはメッセージを生成し、API を呼び出すプログラムはエラーを報告しません。現時点ではキューには何もないため、送信したメッセージは失われます。===> これを試してみましたが、もちろん手動で作成することもできます。

どちらの状況もデータ損失につながる可能性があります。

ディスクデータの損失

これは極端中の極端で、たとえば、サーバーのディスクが破損した場合です(遭遇するのは幸運ですが、企業でも同じことが起こりました)。または、プログラマー、運用保守者が誤って永続データを削除してしまいました。

この場合、大量のデータが失われる可能性があります。一般に、データが本番側の mq に同期されるときは、最初にデータのコピーをバックアップすることを検討します。

タイミングタスクの補償メカニズムを使用すると、補償メカニズムを使用してそれを解決できます。

注 2. サービス B はミドルウェアからデータを消費します

手動で署名するかどうか

デフォルトでは、rabbitMq は受信のために自動的に署名します。つまり、コンシューマーからデータをフェッチすると、そのデータは対応するキューから削除されます。スケジュールされたタスクに補償メカニズムがない場合は、補償メカニズムを追加する必要があります (サービスがハングしないこと、データがハングしないこと、またはコードにその他の異常が発生しないことを保証することはできません)。問題がある場合、データは失った。

毎回 mq からフェッチされるメッセージの数 (prefetch_count)

顧客値のバージョンごとに異なる場合があります。この値は消費者側で設定できます。

公平な分配

各サーバーのマシン構成とパフォーマンスは異なり、rabbitMQ はデフォルトでポーリング モードを使用します。2 人のコンシューマ A、B、100 メッセージがあると仮定すると、それぞれ 50 メッセージになります。

それぞれ 50 ずつ、まあまあ、あなたも 50、私も 50、50/50 (あなたはジュニア プログラマです。あなたと上級プログラマは 50/50 に分けてください。快適ですか? それを終えてから数日が経ちました)。このとき作業に応じて配分し、先に処理が終わった人がその作業を引き継ぎ、能力のある人はより多くの作業を行うことになります。

リピート消費

次の疑似コードがあります。

//ステップ 1: データを挿入する

//ステップ 2: ログを記録する

//ステップ 3: ステータスを更新する

// エラーは報告されず、手動で署名します

次の状況を考えてみましょう。

1. 例外の再試行をオンにします。

コードの 3 を突然実行し、データを更新するときに、更新されたデータベースに突然問題が発生したとします。後で例外が再試行されると、データベースは再び正常になり、挿入操作が 2 回実行されます。同じメッセージが複数回消費され、操作が繰り返されることを避けるための措置を講じる必要があります (例: メッセージ ID の追加、消費は再び挿入されません)

2. 手動受信

手順 3 でエラーが報告された場合、メッセージは手動で署名されず、メッセージはメッセージ キューにまだ存在し、再配信されます (もちろん、ここではエラーが報告された後の操作を指定します。例:他のコンシューマーが消費できるようにキューに戻る、キューからメッセージを削除する、メッセージの確認など、エラーに対処する方法にはボタンのビジネス処理が必要です)

3. タイミングタスクの補正

サービス A の同期ステータスが false の場合、メッセージは一定期間後にメッセージ ミドルウェアに再度配信されます。ステップ 3 の操作のデータベースは初めて失敗し、後で同じデータが再び来ます。

注 3、タスクは処理のためにスレッド プールに渡されます。

スレッドプールのパラメータ設定

まず最初に、コア スレッドの数、スレッドの総数、キュー サイズ (デフォルトの int の最大値) など、スレッド プールのいくつかの主要パラメータを適切に設定する必要があります。これらのパラメータは、次の時点では設定できません。運用環境を組み合わせてスレッドを観察する プール拒否率、スレッド使用率、蓄積タスク数に適切なパラメータ サイズを設定する (マシンごとに構成が異なる場合があるため、特定のマシンに対して特定の分析を実行する必要がある) )、サービスのダウンタイムを引き起こすことなくマシンの使用率を最大化するため(たとえば、キュ​​ー サイズを設定しないと、一度に大量のタスクが送信され、リークが発生します)

次に、スレッド プールの拒否戦略 (デフォルトでは、例外のスローを拒否する戦略です) という知識ポイントを知っておく必要があります (java.util.concurrent.RejectedExecutionHandler の実装クラスを参照)。タスクがスレッド プールの制限を超えると、タスクは拒否されます。拒否された場合、呼び出し元のスレッドに処理を渡す必要がありますか (これは悪いことではなく、スレッドのタスクは破棄されません)、古いタスクを破棄するか、新しいタスクを破棄するか、拒否例外をスローする必要があります。 ? 私たち全員がそれについてよく考えなければなりません。

注4. スレッドプールは非同期で保存を実行します。

データの順序を保証する

ユーザーがデータを保存し、それをサービス B に同期すると仮定します (仮定: 同じユーザーのデータは、値を変更するだけでなく、データの数を増やしたり削除したりすることもできます)、次の疑似コードがあります。

//元のものを削除(delete)

//新しいものを追加(insert)

ユーザーのデータ、同時に実行されている複数のスレッドがあるかどうかを考慮する必要があり、実行中にデータの順序が保証される必要があります。例えば:

1. ユーザー端末には10秒ごとに保存される自動保存機能があり、自動保存直後はすぐに変更されて保存されます。

2. ユーザーの最後の同期は成功しませんでした。スケジュールされたタスクの同期が開始され、ユーザーはこの時点でデータを変更しています。

上記の状況では、新しいデータと古いデータの間に非常に短い差異が生じるか、同時にメッセージ ミドルウェアに配信されることもあります (追記: ここでは、新しいデータと元の古いデータを表すために新と旧の 2 つの漢字を使用しています)データまたは最後のデータ)

異常事態:

最初にマシン 1 が「古い」状態になり、マシン 2 が「新しい」状態になります (各マシンに対応するスレッド プールには他にも多くのタスクがあります)。しかし、現時点では、マシン 1 の CPU が高すぎるか、マシンのパフォーマンスが良くなく、実行が遅いか、マシンが運悪く「古い」スレッドを実行できず、タイム スライスを取得していません。 CPU が長時間使用されるため、save メソッドが一緒に実行されたり、古いメソッドが新しいメソッドに実行されたりすることがあります。何が起こるか?

saveメソッドを一緒に実行したとして、マシン1の削除と挿入、マシン2の削除と挿入を一緒に実行すると、順列と結合の問題が発生し、データは一体どうなったのかわかりません

例:
状況 1:
マシン 1 (古いデータ) -
マシン 2 (新しいデータ) を削除 -
マシン 1 (古いデータ) を削除 -
マシン 2 (新しいデータ) を挿入 - 
// 元の新しいデータ、古いデータの 1 行を削除しましたデータ、あなたは今私に追加しました

状況 2:
マシン 2 (新しいデータ) -
マシン 2 (新しいデータ)を削除 -
マシン 1 (古いデータ)を挿入 -
マシン 1 (古いデータ) を削除 - 挿入
//ゴーストを作成、無駄に変更したのでしょうか?古いデータが残っているため、
他のケースはリストされません

新しいものが最初に実行されると仮定すると、上記のケース 2 と同じです。

この状況にどう対処すべきでしょうか? 分散ロックのみ使用できます。キーはデータ識別子で、値は新しいデータと古いデータを識別するためのデータの時間サイズにすることができます。古いものがロックを取得しても、新しいものは待つだけで、データにはまったく問題ありません。新しいデータが最初にロックを取得する (最初に実行される) 場合、古いデータとこの時間の値を比較し、データが小さすぎる場合は保存しません。

ロックの説明: ここでは、単一のユーザー ビジネス ID でのみロックできます。たとえば、サービス B がスタンドアロン マシンの場合、次のことはできません。

public synchronized void save(){....}

ロックの粒度が大きすぎるため、無関係なスレッドがロックされ、プログラムの効率に影響します。

削除および更新操作で mysql インデックスが使用されていることを確認する

まず第一に、mysql は削除および更新操作を実行するときにインデックスを使用せずにテーブルをロックすることを知っておく必要があります。インデックスは使用されておらず、削除または更新時のテーブル全体のスキャンが遅いです。テーブルはロックされており、他のスレッドや他のサービスがあなたを待っています。スネークをプレイするのはどうですか?

クエリの最適化についてはここでは説明しません。

自動インクリメント主キーの使用を検討してください。

> すべての「InnoDB」テーブルには、行データを格納するクラスター化インデックスと呼ばれる特別なインデックスがあります。通常、クラスター化インデックスは主キーと同義です。クエリ、挿入、その他のデータベース操作から最高のパフォーマンスを得るには、「InnoDB」がクラスター化インデックスを使用して一般的な検索と DML 操作を最適化する方法を理解することが重要です。
> 
> - `PRIMARY KEY` テーブルに定義されている場合、`InnoDB` はそれをクラスター化インデックスとして使用します。主キーはテーブルごとに定義する必要があります。論理的に一意で NULL ではない列または列のセットが主キーを使用していない場合は、自動インクリメント列を追加します。自動インクリメント列の値は一意であり、新しい行が挿入されると自動的に追加されます。
> - テーブルに `PRIMARY KEY` が定義されていない場合、`InnoDB` は最初の `UNIQUE` インデックスを使用し、すべてのキー列を `NOT NULL` クラスター化インデックスとして定義します。
> - テーブルにインデックス付きの `PRIMARY KEY` または適切な `UNIQUE` インデックスがない場合、`InnoDB` は行 ID 値を含む合成列 `GEN_CLUST_INDEX` にちなんで名付けられた非表示のクラスター化インデックスを生成します。行は、「InnoDB」によって割り当てられた行 ID によってソートされます。行 ID は 6 バイトのフィールドで、新しい行が挿入されると単調に増加します。したがって、ROWIDでソートされた行は物理的に挿入順になります。

順序付けされていない主キーがあるとします。主キーの値は毎回ほぼランダムであるため、新しいレコードはそれぞれ既存のインデックス ページの中央に挿入する必要があります。このとき、MySQL は新しいレコードを挿入する必要があります。データを適切な場所 (mysql の innodb の B+TREE データはリーフ ノードに配置されます) にコピーし、対象のページさえもディスクに書き戻され、キャッシュからクリアされている可能性があります。このとき、リードバックする必要があります。ディスクから削除されます。これにより、多くのオーバーヘッドが追加されます。同時に、頻繁な移動操作やページング操作によって多くの断片化が発生し、その結果、インデックス構造が十分にコンパクトになりません。その後、テーブルを再構築してページを埋める必要があります。 OPTIMIZE TABLE による最適化あり

b ツリー (クラスター化インデックス ツリーに対応するノードにデータが配置される) であるか、b+tree (データがリーフ ノードに対応するクラスター化インデックス ツリーにのみ配置される) であるかに関係なく、この問題は次の場合に発生します。順序なしの主キーを使用します。

知らせ:

mongo や postgrepSql など、B ツリーを使用するデータベースも数多くありますが、いずれもこの問題を抱えており、無秩序なクラスタード インデックス ツリーの再構築によってもたらされる問題を皆で考慮する必要があります。

データ削除の最適化

本当にデータを削除すると、再びツリーの再構築の問題が発生します。これはデータベースやプログラムの効率にとって好ましくありません。削除状態を追加し、削除によって状態が更新されるだけです。データを本当に削除する必要がある場合は、スケジュールされたタスクを使用して、深夜の空き時間にデータを削除できます。

注5. 同期結果を更新する

サービス A では、ユーザーが保存するたびに同期状態が false に設定されます。同期が成功したら、状態値を変更します。この種の操作ではインデックスを使用すると、一般に更新が非常に高速になります。更新をリモートから直接呼び出すことができますが、ミドルウェアを使用すると、プログラムの難易度が上がります。

注6. 同期が必要なデータはバックアップしてください。

同期する必要があるデータをバックアップすると、次の 2 つの問題を解決できます。

1. MQ ディスクのデータ損失データ問題

2. その他さまざまな異常によりサービス B にデータが届かなくなり、タイミングサービスが定期的にサービス A を呼び出してデータの同期を行うため、サービス A ではデータが同期できるという問題が発生します。

正常に同期されたデータのこの部分については、ビジネスで不要になった場合は、定期的にバックアップを移行するか、定期的に削除することができます。

要約する

  • データを同期するときは、さまざまな異常 (コードの異常、マシンの障害)、および異常が同期データに影響を与えるかどうかを考慮します。最終的にはデータ同期の精度を確保することであり、スケジュールされたタスクの補償メカニズムにより、データの最終的な一貫性を保証できます。データの正確性を確保しつつ、プログラムの効率性を考慮し、ユーザーフレンドリーなエクスペリエンスを提供する必要があります。新しいテクノロジーを使用するときは、落とし穴を知っておく必要があります。mq を使用する場合と同様に、データ損失やデータの繰り返し消費が発生する可能性があります。マルチスレッドを使用する場合は、データの順序を確保する必要があります。同じデータを同時に変更する場合は、ロック (単一マシンに対して同期された cas または Lock) を追加する必要があります。ロックにより粒度も制御できます。ロックの。

おすすめ

転載: blog.csdn.net/qq_41221596/article/details/132390578