[[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座標をインポートします: - <!
- <依存関係>
- <グループ ID>org.apache.curator</グループ ID>
- <artifactId>キュレーターフレームワーク</artifactId>
- <バージョン>4.2.0</バージョン>
- </依存関係>
- <!
- <依存関係>
- <グループ ID>org.apache.curator</グループ ID>
- <artifactId>キュレーターレシピ</artifactId>
- <バージョン>4.2.0</バージョン>
- </依存関係>
コード: - パブリッククラスZkTest{
-
- // セッションタイムアウト
- プライベート最終int SESSION_TIMEOUT = 30 * 1000;
-
- // 接続タイムアウト、違いは何ですか
- プライベート静的最終int CONNECTION_TIMEOUT = 3 * 1000;
-
- プライベート静的最終文字列 CONNECT_ADDR = "localhost:2181" ;
-
- プライベート CuratorFramework クライアント = null ;
-
- 公共 静的void main(String[] args)は例外をスローします{
- // クライアントを作成する
- 再試行ポリシー retryPolicy = 新しい ExponentialBackoffRetry(1000, 10);
- CuratorFramework クライアント = CuratorFrameworkFactory.builder()
- .connectString(CONNECT_ADDR)
- .connectionTimeoutMs(接続タイムアウト)
- .retryPolicy(再試行ポリシー)
- 。建てる();
- クライアントを起動します。
- システム。出力.println(ZooKeeper.States.CONNECTED);
- システム。出力.println(client.getState());
-
- // ノード /test1 を作成
- クライアント。作成する()
- .forPath( "/test1" , "キュレーターデータ" .getBytes(StandardCharsets.UTF_8));
-
- システム。出力.println(client.getChildren().forPath( "/" ));
-
- // 一時ノード
- クライアントを作成します().withMode(CreateMode.EPHEMERAL)
- .forPath( "/secondPath" , "hello world" .getBytes(StandardCharsets.UTF_8));
- システム。出力.println(new String(client.getData().forPath( "/secondPath" )));
-
- クライアントを作成します().withMode(CreateMode.PERSISTENT_SEQUENTIAL)
- .forPath( "/abc" , "hello" .getBytes(StandardCharsets.UTF_8));
- // 再帰的な作成
- クライアント。作成する()
- 必要な場合は親コンテナを作成します()
- .forPath( "/secondPath1/sencond2/sencond3" );
-
-
- スレッドをスリープ状態にします(10000);
- }
4. コアメカニズム2: ウォッチャー監視メカニズムWatcher 監視メカニズムは、さまざまな分散不整合問題を解決するための Zookeeper 独自の方法であり、Zookeeper を学習する上で必須の知識ポイントでもあります。 1. ウォッチャーの仕組みの理解Zookeeper は、データの公開とサブスクライブの機能を提供します。複数のサブスクライバーが同時にオブジェクトを監視できます。オブジェクト自体の状態が変化すると (たとえば、ノード データやノードの子ノードの数の変化)、Zookeeper システムはこれらのサブスクライバーに通知します。 公開とサブスクライブの概念を理解するために、次のシナリオを参考にしてください。 たとえば、2日前の台風のため、上司は従業員に明日は在宅勤務するように通知したいと考えていました。 そこで、上司はDingTalkグループにメッセージを送信し、従業員はDingTalkを開いて自分で確認します。 このシナリオでは、上司がパブリッシャー、従業員がサブスクライバー、DingTalk グループが Zookeeper システムになります。 上司は従業員に個別にメッセージを送信するのではなく、従業員がメッセージの変化を認識できるようにグループにメッセージを送信します。 購読者 | スタッフ | クライアント 1 |
---|
システム | ディントークグループ | 飼育員システム | 出版社 | ボス | クライアント2 |
2. ウォッチャーメカニズムのプロセスクライアントはまずサーバーに Watcher を登録し、クライアントの Watcher マネージャーに Watcher オブジェクトを保存します。 Zookeeper サーバーはデータ ステータスの変更を検出すると、まずクライアントにアクティブに通知し、次にクライアントの Watcher マネージャーが関連する Watcher をトリガーして応答ロジックをコールバックし、全体的なパブリッシュ/サブスクライブ プロセスを完了します。 リスナーウォッチャーの定義: - パブリックインターフェースウォッチャー{
- // WatchedEvent オブジェクトには、Zookeeper ステータス、イベント タイプ、パスの 3 つのプロパティがあります。
- // 最終的なプライベート KeeperState keeperState;
- // 最終的なプライベート EventType eventType;
- // プライベート文字列パス;
- 抽象パブリックvoid プロセス(WatchedEvent イベント);
- }
以下は監視の一般的なフローチャートです。 少し説明します: 1. Client1 と Client2 は両方とも /app2 ノードのデータ ステータスの変更を懸念しているため、Zookeeper に /app2 のリスナーを登録します。 2. Client3 が /app2 の値を変更すると、Zookeeper は Client1 と Client2 に積極的に通知し、リスナー メソッドをコールバックします。 もちろん、ここでのデータ ステータスの変更は次の種類になります。 - ノードが作成されます。
- ノードが削除されます。
- ノードデータの変更。
- ノードの子ノードの数が変わります。
3. コードを通じて予備的な理解を得るCurator フレームワークを使用してこのリスナーを検証してみましょう。 コードは非常にシンプルです。ここでは、TreeCache を使用して /app2 の監視を表し、監視メソッドを登録します。 - パブリッククラス CuratorWatcher {
-
- 公共 静的void main(String[] args)は例外をスローします{
- CuratorFramework クライアント = CuratorFrameworkFactory.builder().connectString( "localhost:2181" )
- .接続タイムアウトMs(10000)
- .retryPolicy(新しいExponentialBackoffRetry(1000, 10))
- 。建てる();
- クライアントを起動します。
-
- 文字列パス = "/app2" ;
-
- TreeCache treeCache = 新しい TreeCache(クライアント、パス);
- ツリーキャッシュを開始します。
-
- treeCache.getListenable().addListener((client1, イベント) -> {
- システム。出力.println( "event.getData()," + event.getData());
- システム。出力.println( "event.getType()," + event.getType());
- });
-
- スレッド.スリープ(整数.MAX_VALUE);
- }
- }
/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 フレームワークを使用して実装されています。 - パッケージ com.kkarc.zookeeper;
-
- cn.hutool.core.util.StrUtil をインポートします。
- lombok.extern.slf4j.Slf4j をインポートします。
- org.apache.curator.framework.CuratorFramework をインポートします。
- org.apache.curator.framework.recipes.cache.TreeCache をインポートします。
- org.apache.curator.framework.recipes.cache.TreeCacheEvent をインポートします。
- org.apache.zookeeper.CreateMode をインポートします。
-
- java.nio.charset.StandardCharsets をインポートします。
-
- /**
- * 分散選挙
- *
- * @著者 wangkai
- * @時間2021/7/25 20:12
- */
- 翻訳者
- パブリッククラスElectorTest {
-
- プライベート静的最終文字列 PARENT = "/cluster_ha" ;
- プライベート静的最終文字列 ACTIVE = PARENT + "/ActiveMaster" ;
- プライベート静的最終文字列 STANDBY = PARENT + "/StandbyMaster" ;
- プライベート静的最終文字列 LOCK = PARENT + "/ElectorLock" ;
-
- プライベート静的最終文字列HOSTNAME = "bigdata05" ;
- プライベート静的最終文字列 activeMasterPath = ACTIVE + "/" + HOSTNAME;
- プライベート静的最終文字列standByMasterPath = STANDBY + "/" + HOSTNAME;
-
- 公共 静的void main(String[] args)は例外をスローします{
- CuratorFramework zk = ZkUtil.createZkClient( "localhost:2181" );
- zk.start();
-
- // モニターを登録する
- TreeCache treeCache = 新しい TreeCache(zk, PARENT);
- ツリーキャッシュを開始します。
-
- treeCache.getListenable().addListener((クライアント、イベント) -> {
- event.getType() が TreeCacheEvent.Type.INITIALIZED と等しい場合 || event.getType() が TreeCacheEvent.Type.CONNECTION_LOST と等しい場合
- || event.getType().equals(TreeCacheEvent.Type.CONNECTION_RECONNECTED) || event.getType().equals(TreeCacheEvent.Type.CONNECTION_SUSPENDED)) {
- 戻る;
- }
- システム。出力.println(event.getData());
- // Activeの下のノードが削除され、ノードが存在しない場合は、Activeに対して実行する必要があります。
- StrUtil.startWith(event.getData().getPath(), ACTIVE) && event.getType().equals(TreeCacheEvent.Type.NODE_REMOVED) の場合 {
- getChildrenNumber(zk, ACTIVE) == 0 の場合
- createZNode(クライアント、LOCK、HOSTNAME.getBytes(StandardCharsets.UTF_8)、CreateMode.EPHEMERAL);
- システム。 out .println(HOSTNAME + "ロックを取得しました" );
- }
- }
- // ロック ノードが作成された場合、それが自分で作成したものかどうかを判断します。もしそうなら、ステータスをACTIVEに切り替えてください
- そうでない場合、(StrUtil.equals(event.getData().getPath(), LOCK) && event.getType().equals(TreeCacheEvent.Type.NODE_ADDED)) {
- if (StrUtil.equals(新しいString(event.getData().getData()), HOSTNAME)) {
- createZNode(zk、activeMasterPath、HOSTNAME.getBytes(StandardCharsets.UTF_8)、CreateMode.EPHEMERAL);
- if (checkExists(クライアント、standByMasterPath)) {
- ZNode を削除します (クライアント、standByMasterPath);
- }
- }
- }
- });
-
- // 最初にアクティブノードとスタンバイノードを作成します
- (zk.checkExists().forPath(ACTIVE) == null の場合) {
- zk.create ().createParentContainersIfNeeded().forPath(ACTIVE);
- }
- (zk.checkExists().forPath(STANDBY) == null の場合) {
- zk.create ().createParentContainersIfNeeded().forPath(STANDBY);
- }
-
- // ACTIVE の下に子ノードがあるかどうかを確認し、ない場合はロックを取得します
- getChildrenNumber(zk, ACTIVE) == 0 の場合
- createZNode(zk、LOCK、HOSTNAME.getBytes(StandardCharsets.UTF_8)、CreateMode.EPHEMERAL);
- }
- // はいの場合、スタンバイ状態になります
- それ以外{
- ZNode を作成します (zk、standByMasterPath、HOSTNAME.getBytes(StandardCharsets.UTF_8)、CreateMode.EPHEMERAL);
- }
-
-
- スレッドをスリープ状態にします(1000000000);
-
-
- }
-
- 公共 静的 int getChildrenNumber(CuratorFramework client, String path) は例外をスローします {
- client.getChildren().forPath(path) .size ()を返します。
- }
-
- 公共 static void createZNode(CuratorFramework クライアント、文字列パス、byte[] データ、CreateMode モード) {
- 試す {
- client.create ().withMode(mode).forPath(path, data);
- } キャッチ (例外 e) {
- log.error( "ノードの作成に失敗しました" , e);
- システム。 out .println( "ノードの作成に失敗しました" );
- }
- }
-
- 公共 静的ブール値 checkExists(CuratorFramework クライアント、文字列パス) は例外をスローします {
- client.checkExists().forPath(path) != nullを返します。
- }
-
- 公共 静的void deleteZNode(CuratorFrameworkクライアント、文字列パス) {
- 試す {
- if (checkExists(クライアント、パス)) {
- クライアントを削除します。パスの取得
- }
- } キャッチ (例外 e) {
- log.error( "ノードの削除に失敗しました" , e);
- }
- }
- }
|