分散調整フレームワークZookeeperのコア設計の理解と実践

分散調整フレームワークZookeeperのコア設計の理解と実践

[[413943]]

この記事はWeChatの公開アカウント「KK Architecture」から転載したもので、著者はwangkaiです。記事の転載についてはKK Architectureの公式アカウントまでご連絡ください。

1. はじめに

昔、ある顧客サイトで、突然マイクロサービス B がマイクロサービス A を呼び出せなくなったことを覚えています。できるだけ早くサービスを正常に戻すために、マイクロサービス B が再起動されました。

しかし、顧客はこの問題の原因を尋ね続けたため、私は杭州から深センまで飛んで現地で問題を調査しました。

最終的な結論は、zk には特定の瞬間にマスターとスレーブの切り替えがあるということです。このとき、マイクロサービス A (dubbo ベース) を zk に再登録する必要がありますが、ポート番号が変更されています。

ただし、マイクロサービス B にはマイクロサービス A の RPC インターフェースのローカル キャッシュがあり、そのキャッシュには古いポートがまだ含まれているため、呼び出すことができません。

解決策は、マイクロサービスの RPC ポート番号を固定のものに変更することです。

理由はわかりましたが、Zookeeper に対する私の理解はまだ十分ではなかったので、Zookeeper のコア設計を改めて学び、皆さんがお互いに励まし合えるようにこの記事に記録しました。

2. Zookeeperコアアーキテクチャ設計

1. Zookeeperの機能

(1)Zookeeperは、複数のノードの状態が不一致になる問題を解決するために設計され、仲介役として機能する分散調整サービスです。不整合が発生した場合、その不整合は Zookeeper に書き込まれ、Zookeeper は応答を返します。応答が成功した場合、合意に達したことを意味します。

たとえば、クラスターが起動すると、ノード A、B、C はマスター ノードを選択する必要があります。このとき、A、B、C は Zookeeper に一時ノードを同時に登録するだけで済みます。最初に登録に成功した人がマスターノードになります。

(2)Zookeeperはクラスターですが、データは異なるノードに保存されません。代わりに、各ノードはクラスターのすべてのデータを保存します。

ノードの 1 つはマスター ノードとして機能し、分散トランザクションの書き込みサービスを提供します。他のノードは、マスターノードとのステータスの一貫性を保つために、このノードとデータを同期します。

(3)すべてのZookeeperノードのデータステータスはZabプロトコルを通じて一貫したまま維持されます。クラスター内にリーダー ノードが存在しない場合は、内部で選出が実行されます。選挙後、フォロワーとリーダーは実行ステータスを同期します。リーダー ノードが存在する場合、リーダーは ZAB プロトコルを通じて分散トランザクションの実行を支配し、すべてのトランザクションはシリアルに実行されます。

(4)Zookeeperノードの数は直線的に拡張することはできない。ノードの数が増えるほど、データを同期する圧力が大きくなり、分散トランザクションの実行パフォーマンスが低下します。推奨される数字は 3、5、7 です。

2. 飼育係の役割を理解する

Zookeeper はマスター/スレーブの概念を使用しませんが、リーダー、フォロワー、オブザーバーという 3 つの役割を導入します。

リーダー選出アルゴリズムを通じて、リーダー ノードとして機能するサーバーが選択されます。リーダー サーバーは、クライアントに読み取りおよび書き込みサービスを提供します。

フォロワー ノードは選挙に参加し、クライアントからの読み取り要求を受け入れることができますが、クライアントから書き込み要求を受信すると、処理のためにリーダー サーバーに転送します。

オブザーバー ロールは読み取りサービスのみを提供でき、選出または選出することはできません。したがって、その重要性は、書き込みパフォーマンスに影響を与えずにクラスターの読み取りパフォーマンスを向上させることです。

3. Zookeeper は同時に CAP も満たしていますか?

答えは「いいえ」です。CAP は同時に 2 つしか満たすことができません。

Zookeeper にはトレードオフがあります。 C 読み取り一貫性を犠牲にして、A 可用性、P パーティション耐性、および C 書き込み一貫性を実現します。

つまり、Zookeeper は読み取られたデータが必ずしも最新であることを保証するものではありません。最新バージョンが必要な場合は、同期コールバックを使用する必要があります。

3. コアメカニズム1: ZNodeデータモデル

Zookeeper の ZNode モデルは、実際には、次に示すようにファイル システムとして理解できます。

1. ZNodeは大量のデータの保存には適していません

なぜファイルシステムのようなシステムなのでしょうか? ZNode モデルにはファイルとフォルダーの概念がないためです。各ノードは子ノードを持つことができ、データを保存することもできます。

各ノードはデータを保存できるので、無制限にデータを保存できますか?答えはノーです。 Zookeeper では、各ノードに保存できるデータは 1 MB 未満に制限されています。実際のアプリケーションでは、1kb を超えないようにするのが最適です。

理由は4つあります。

  • 同期の圧力: 各 Zookeeper ノードはすべての Zookeeper データを保存します。各ノードのステータスはリーダーと一致している必要があります。同期プロセスが成功したとみなされるには、少なくとも半数のノードが正常に同期されていることを確認する必要があります。データが大きくなるほど、書き込みが難しくなります。
  • リクエストのブロック: 書き込みの強力な一貫性を確保するために、Zookeeper は書き込み順序に従って厳密にシリアル順序で実行され、一度に実行できるトランザクションは 1 つだけです。前のトランザクションの実行に長い時間がかかる場合、後続のリクエストはブロックされます。
  • ストレージの負荷: 各 Zookeeper ノードは完全なデータを保存するため、各 ZNode に保存されるデータが大きいほど、消費される物理メモリも多くなります。
  • 設計意図: Zookeeper の本来の目的は、大規模なストレージ サービスを提供するのではなく、分散型の問題を解決するためのデータ モデルを提供することでした。

2. ZNodeの分類

(1)ライフサイクルによる分類

宣言サイクルに応じて、ZNode は永続ノードと一時ノードに分けられます。

永続ノードは明示的に削除する必要があることは容易に理解できます。そうしないと、永続ノードは常に存在し続けます。一時ノードはセッションにバインドされ、セッションが切断されると、セッションによって作成されたすべてのノードは Zookeeper システムによって削除されます。

(2)シリアルナンバーの有無による分類

たとえば、シリアル番号の場合、コード内に /a ノードを作成すると、作成後の実際のノードは /a000000000000001 になります。再度作成すると、/a0000000000000002 のようになります。

シリアル番号がない場合、作成されたものになります。

(3)したがって、ZNodeには4つの種類がある。

  • 永久、番号なし
  • 永久シリアル番号
  • シリアル番号なしの一時
  • 仮シリアル番号

(4)留意点

一時ノードは、その下に子ノードをマウントすることはできず、他のノードのリーフ ノードとしてのみ機能します。

3. コードの練習

ZNode のデータ モデルは実は非常にシンプルで、知っておく必要がある知識はこれだけです。これを強化するためにコードを使用しましょう。

ここでは、キュレーター フレームワークを使用してデモを作成します。 (もちろん、Zookeeperの公式APIを使用することもできます)

pom座標をインポートします:

  1. <! -- キュレーターフレームワーク -->  
  2. <依存関係>
  3. <グループ ID>org.apache.curator</グループ ID>
  4. <artifactId>キュレーターフレームワーク</artifactId>
  5. <バージョン>4.2.0</バージョン>
  6. </依存関係>
  7. <! -- キュレーターレシピ -->  
  8. <依存関係>
  9. <グループ ID>org.apache.curator</グループ ID>
  10. <artifactId>キュレーターレシピ</artifactId>
  11. <バージョン>4.2.0</バージョン>
  12. </依存関係>

コード:

  1. パブリッククラスZkTest{
  2.  
  3. // セッションタイムアウト
  4. プライベート最終int SESSION_TIMEOUT = 30 * 1000;
  5.  
  6. // 接続タイムアウト、違いは何ですか
  7. プライベート静的最終int CONNECTION_TIMEOUT = 3 * 1000;
  8.  
  9. プライベート静的最終文字列 CONNECT_ADDR = "localhost:2181" ;
  10.  
  11. プライベート CuratorFramework クライアント = null ;
  12.  
  13. 公共 静的void main(String[] args)は例外をスローします{
  14. // クライアントを作成する
  15. 再試行ポリシー retryPolicy = 新しい ExponentialBackoffRetry(1000, 10);
  16. CuratorFramework クライアント = CuratorFrameworkFactory.builder()
  17. .connectString(CONNECT_ADDR)
  18. .connectionTimeoutMs(接続タイムアウト)
  19. .retryPolicy(再試行ポリシー)
  20. 。建てる();
  21. クライアントを起動します。
  22. システム。出力.println(ZooKeeper.States.CONNECTED);
  23. システム。出力.println(client.getState());
  24.  
  25. // ノード /test1 を作成
  26. クライアント。作成する()
  27. .forPath( "/test1" , "キュレーターデータ" .getBytes(StandardCharsets.UTF_8));
  28.  
  29. システム。出力.println(client.getChildren().forPath( "/" ));
  30.  
  31. // 一時ノード
  32. クライアントを作成します().withMode(CreateMode.EPHEMERAL)
  33. .forPath( "/secondPath" , "hello world" .getBytes(StandardCharsets.UTF_8));
  34. システム。出力.println(new String(client.getData().forPath( "/secondPath" )));
  35.  
  36. クライアントを作成します().withMode(CreateMode.PERSISTENT_SEQUENTIAL)
  37. .forPath( "/abc" , "hello" .getBytes(StandardCharsets.UTF_8));
  38. // 再帰的な作成
  39. クライアント。作成する()
  40. 必要な場合は親コンテナを作成します()
  41. .forPath( "/secondPath1/sencond2/sencond3" );
  42.  
  43.  
  44. スレッドをスリープ状態にします(10000);
  45. }

4. コアメカニズム2: ウォッチャー監視メカニズム

Watcher 監視メカニズムは、さまざまな分散不整合問題を解決するための Zookeeper 独自の方法であり、Zookeeper を学習する上で必須の知識ポイントでもあります。

1. ウォッチャーの仕組みの理解

Zookeeper は、データの公開とサブスクライブの機能を提供します。複数のサブスクライバーが同時にオブジェクトを監視できます。オブジェクト自体の状態が変化すると (たとえば、ノード データやノードの子ノードの数の変化)、Zookeeper システムはこれらのサブスクライバーに通知します。

公開とサブスクライブの概念を理解するために、次のシナリオを参考にしてください。

たとえば、2日前の台風のため、上司は従業員に明日は在宅勤務するように通知したいと考えていました。

そこで、上司はDingTalkグループにメッセージを送信し、従業員はDingTalkを開いて自分で確認します。

このシナリオでは、上司がパブリッシャー、従業員がサブスクライバー、DingTalk グループが Zookeeper システムになります。

上司は従業員に個別にメッセージを送信するのではなく、従業員がメッセージの変化を認識できるようにグループにメッセージを送信します。

購読者スタッフクライアント 1
システムディントークグループ飼育員システム
出版社ボスクライアント2

2. ウォッチャーメカニズムのプロセス

クライアントはまずサーバーに Watcher を登録し、クライアントの Watcher マネージャーに Watcher オブジェクトを保存します。 Zookeeper サーバーはデータ ステータスの変更を検出すると、まずクライアントにアクティブに通知し、次にクライアントの Watcher マネージャーが関連する Watcher をトリガーして応答ロジックをコールバックし、全体的なパブリッシュ/サブスクライブ プロセスを完了します。

リスナーウォッチャーの定義:

  1. パブリックインターフェースウォッチャー{
  2. // WatchedEvent オブジェクトには、Zookeeper ステータス、イベント タイプ、パスの 3 つのプロパティがあります。
  3. // 最終的なプライベート KeeperState keeperState;
  4. // 最終的なプライベート EventType eventType;
  5. // プライベート文字列パス;
  6. 抽象パブリックvoid プロセス(WatchedEvent イベント);
  7. }

以下は監視の一般的なフローチャートです。

少し説明します:

1. Client1 と Client2 は両方とも /app2 ノードのデータ ステータスの変更を懸念しているため、Zookeeper に /app2 のリスナーを登録します。

2. Client3 が /app2 の値を変更すると、Zookeeper は Client1 と Client2 に積極的に通知し、リスナー メソッドをコールバックします。

もちろん、ここでのデータ ステータスの変更は次の種類になります。

  • ノードが作成されます。
  • ノードが削除されます。
  • ノードデータの変更。
  • ノードの子ノードの数が変わります。

3. コードを通じて予備的な理解を得る

Curator フレームワークを使用してこのリスナーを検証してみましょう。

コードは非常にシンプルです。ここでは、TreeCache を使用して /app2 の監視を表し、監視メソッドを登録します。

  1. パブリッククラス CuratorWatcher {
  2.  
  3. 公共 静的void main(String[] args)は例外をスローします{
  4. CuratorFramework クライアント = CuratorFrameworkFactory.builder().connectString( "localhost:2181" )
  5. .接続タイムアウトMs(10000)
  6. .retryPolicy(新しいExponentialBackoffRetry(1000, 10))
  7. 。建てる();
  8. クライアントを起動します。
  9.  
  10. 文字列パス = "/app2" ;
  11.  
  12. TreeCache treeCache = 新しい TreeCache(クライアント、パス);
  13. ツリーキャッシュを開始します。
  14.  
  15. treeCache.getListenable().addListener((client1, イベント) -> {
  16. システム。出力.println( "event.getData()," + event.getData());
  17. システム。出力.println( "event.getType()," + event.getType());
  18. });
  19.  
  20. スレッド.スリープ(整数.MAX_VALUE);
  21. }
  22. }

/app2 の状態が変化すると、リスニング メソッドが呼び出されます。

Curator はネイティブ Zookeeper Api をカプセル化したものです。ネイティブ Zookeeper によって提供される API では、リスナーを登録した後、データが変更されると、リスナーはサーバーによって削除され、リスナーを再度登録する必要があります。

Curator はこれに対応するカプセル化と改善を行いました。

5. コード演習: マスター/スレーブ選出の実装

ここで実現したい主な機能は次のとおりです。

  • bigdata001 と bigdata002 の 2 つのノードがあり、これらは互いにマスター ノードとバックアップ ノードです。
  • bigdata001 は起動すると、zk 上に一時ノード /ElectorLock (ロック) を登録し、マスターノードであることを示すために /ActiveMaster の下に子ノードを登録します。
  • bigdata002 を起動すると、一時ノード /ElectorLock が存在することがわかります。これは、現在のシステムにすでにマスター ノードがあることを意味します。次に、スタンバイであることを示すために、/StandbyMaster の下にノードを登録します。
  • bigdata001 が終了すると、/ElectorLock が解放され、/activeMaster の下のノードが削除されます。
  • bigdata002 は、/ElectorLock が存在しないことを感知すると、/ElectorLock を登録し、自身を /ActiveMaster の下に登録して、マスターノードになったことを示します。

コードは引き続き Curator フレームワークを使用して実装されています。

  1. パッケージ com.kkarc.zookeeper;
  2.  
  3. cn.hutool.core.util.StrUtil をインポートします。
  4. lombok.extern.slf4j.Slf4j をインポートします。
  5. org.apache.curator.framework.CuratorFramework をインポートします。
  6. org.apache.curator.framework.recipes.cache.TreeCache をインポートします。
  7. org.apache.curator.framework.recipes.cache.TreeCacheEvent をインポートします。
  8. org.apache.zookeeper.CreateMode をインポートします。
  9.  
  10. java.nio.charset.StandardCharsets をインポートします。
  11.  
  12. /**
  13. * 分散選挙
  14. *
  15. * @著者 wangkai
  16. * @時間2021/7/25 20:12
  17. */
  18. 翻訳者
  19. パブリッククラスElectorTest {
  20.  
  21. プライベート静的最終文字列 PARENT = "/cluster_ha" ;
  22. プライベート静的最終文字列 ACTIVE = PARENT + "/ActiveMaster" ;
  23. プライベート静的最終文字列 STANDBY = PARENT + "/StandbyMaster" ;
  24. プライベート静的最終文字列 LOCK = PARENT + "/ElectorLock" ;
  25.  
  26. プライベート静的最終文字列HOSTNAME = "bigdata05" ;
  27. プライベート静的最終文字列 activeMasterPath = ACTIVE + "/" + HOSTNAME;
  28. プライベート静的最終文字列standByMasterPath = STANDBY + "/" + HOSTNAME;
  29.  
  30. 公共 静的void main(String[] args)は例外をスローします{
  31. CuratorFramework zk = ZkUtil.createZkClient( "localhost:2181" );
  32. zk.start();
  33.  
  34. // モニターを登録する
  35. TreeCache treeCache = 新しい TreeCache(zk, PARENT);
  36. ツリーキャッシュを開始します。
  37.  
  38. treeCache.getListenable().addListener((クライアント、イベント) -> {
  39. event.getType() が TreeCacheEvent.Type.INITIALIZED と等しい場合 || event.getType() が TreeCacheEvent.Type.CONNECTION_LOST と等しい場合
  40. || event.getType().equals(TreeCacheEvent.Type.CONNECTION_RECONNECTED) || event.getType().equals(TreeCacheEvent.Type.CONNECTION_SUSPENDED)) {
  41. 戻る;
  42. }
  43. システム。出力.println(event.getData());
  44. // Activeの下のノードが削除され、ノードが存在しない場合は、Activeに対して実行する必要があります。
  45. StrUtil.startWith(event.getData().getPath(), ACTIVE) && event.getType().equals(TreeCacheEvent.Type.NODE_REMOVED) の場合 {
  46. getChildrenNumber(zk, ACTIVE) == 0 の場合
  47. createZNode(クライアント、LOCK、HOSTNAME.getBytes(StandardCharsets.UTF_8)、CreateMode.EPHEMERAL);
  48. システム。 out .println(HOSTNAME + "ロックを取得しました" );
  49. }
  50. }
  51. // ロック ノードが作成された場合、それが自分で作成したものかどうかを判断します。もしそうなら、ステータスをACTIVEに切り替えてください
  52. そうでない場合、(StrUtil.equals(event.getData().getPath(), LOCK) && event.getType().equals(TreeCacheEvent.Type.NODE_ADDED)) {
  53. if (StrUtil.equals(新しいString(event.getData().getData()), HOSTNAME)) {
  54. createZNode(zk、activeMasterPath、HOSTNAME.getBytes(StandardCharsets.UTF_8)、CreateMode.EPHEMERAL);
  55. if (checkExists(クライアント、standByMasterPath)) {
  56. ZNode を削除します (クライアント、standByMasterPath);
  57. }
  58. }
  59. }
  60. });
  61.  
  62. // 最初にアクティブノードとスタンバイノードを作成します
  63. (zk.checkExists().forPath(ACTIVE) == null の場合) {
  64. zk.create ().createParentContainersIfNeeded().forPath(ACTIVE);
  65. }
  66. (zk.checkExists().forPath(STANDBY) == null の場合) {
  67. zk.create ().createParentContainersIfNeeded().forPath(STANDBY);
  68. }
  69.  
  70. // ACTIVE の下に子ノードがあるかどうかを確認し、ない場合はロックを取得します
  71. getChildrenNumber(zk, ACTIVE) == 0 の場合
  72. createZNode(zk、LOCK、HOSTNAME.getBytes(StandardCharsets.UTF_8)、CreateMode.EPHEMERAL);
  73. }
  74. // はいの場合、スタンバイ状態になります
  75. それ以外{
  76. ZNode を作成します (zk、standByMasterPath、HOSTNAME.getBytes(StandardCharsets.UTF_8)、CreateMode.EPHEMERAL);
  77. }
  78.  
  79.  
  80. スレッドをスリープ状態にします(1000000000);
  81.  
  82.  
  83. }
  84.  
  85. 公共 静的  int getChildrenNumber(CuratorFramework client, String path) は例外をスローします {
  86. client.getChildren().forPath(path) .size ()を返します
  87. }
  88.  
  89. 公共  static void createZNode(CuratorFramework クライアント、文字列パス、byte[] データ、CreateMode モード) {
  90. 試す {
  91. client.create ().withMode(mode).forPath(path, data);
  92. } キャッチ (例外 e) {
  93. log.error( "ノードの作成に失敗しました" , e);
  94. システム。 out .println( "ノードの作成に失敗しました" );
  95. }
  96. }
  97.  
  98. 公共 静的ブール値 checkExists(CuratorFramework クライアント、文字列パス) は例外をスローします {
  99. client.checkExists().forPath(path) != nullを返します
  100. }
  101.  
  102. 公共 静的void deleteZNode(CuratorFrameworkクライアント、文字列パス) {
  103. 試す {
  104. if (checkExists(クライアント、パス)) {
  105. クライアントを削除しますパスの取得
  106. }
  107. } キャッチ (例外 e) {
  108. log.error( "ノードの削除に失敗しました" , e);
  109. }
  110. }
  111. }

<<:  分散システムにおける「スプリットブレイン」とは一体何でしょうか?

>>:  Kafka Connect を使用してリアルタイム データを処理するためのオープン ソース データ パイプラインを作成する方法は?

推薦する

消費者連合がお金を稼ぐ方法と関連する質問への回答を共有

2018年最もホットなプロジェクト:テレマーケティングロボットがあなたの参加を待っています消費者連盟...

胡ペイビン: SEO 最適化の学習に関する 20 の誤解

ここで、IDSEM実名ネットワークマーケティングの胡培斌がSEO業界に参入したばかりの友人のよくある...

alienlayer-OVZ/XEN/KVM/スペシャルエディション/全品40%オフキャンペーン

alienlayerさん、この人のことをまだ覚えていますか?エイリアンホスト!少なくとも私は2010...

検察はQQアカウント盗難と詐欺の背後にある闇産業チェーンを暴露

まずQQアカウントを盗み、次にアカウント所有者になりすましてQQの友人からお金を借り、さらには「身元...

リサーチインスティテュート:世界のクラウドコンピューティング市場規模は2024年に1兆ドルを超えると予想

海外メディアの報道によると、今年の流行により企業のクラウドコンピューティングへの移行が加速し、世界の...

コンテンツマーケティングの実践 - 執筆のインスピレーションを得るために最前線にいる同僚に聞く

新しい職場に入ってから、私が最初に解決しなければならなかった問題は、会社のウェブサイトのコンテンツを...

Kafka を使い始める

著者: ninetyhe、Tencent CDG バックエンド開発エンジニア古いものを見直して新しい...

WeChat Readingの「Unlimited Card」活動のプロモーション手法を分析!

1. 事件のハイライト1. 1 回のゲームプレイで、半年以内に少なくとも数百万人のユーザーを獲得しま...

洞察:ブログ管理はもはやアウトプット率を期待できない

最近はマーケティングでかなり忙しく、手近の固定プロジェクト以外では、頻繁に更新しているブログにあまり...

Helm Charts 開発の完全な例

Helmの使用は比較的簡単ですが、主にgoテンプレートのせいで、Chartパッケージを自分で開発する...

小さくて美しいマーケティングアプローチは、独自の特徴を出すことです

最近、「小さくて美しい」という言葉をよく耳にします。小さくて美しいとは、いったいどういう意味でしょう...

高品質な外部リンクを構築する方法についての簡単な説明

みなさんこんにちは。私はハルビンバーチャルアンドリアルウェブサイトデザインです。今日は、高品質の外部...

安定したウェブサイト構築: IPXcore - 4月は40%オフ/Openvz/Kvm

IPXcore は、価格に関係なく製品を作るために最善を尽くす中小企業の 1 つです。IPXcore...

新華社がツイッターアカウントを開設、フォロワーは5,000人超に(写真)

新華社通信のTwitterアカウントのホームページ。ファンは5,000人以上、投稿されたメッセージは...