dubbo學習(四)-- 註冊中心

註冊中心

在orz框架中(對就是我們在做的這個框架的名字orz~), 消費者是寫死生產者地址的, 爲了後面支持集羣擴展(真的麼…), 我們這次搞一個註冊中心, orz是抄…咳咳…借鑑dubbo的產品(產品…), 所以我們使用zookeeper來作爲註冊中心. 現在先簡單瞭解下注冊中心, 幫助還不熟悉的同學(me~).

下面是zk官方文檔的一些翻譯, 斜體是翻譯, 粗體是個人理解

zookeeper

Design Goals 設計目標

zk允許分佈式進程互相協作, 通過一個共享的分層命名空間, 該空間簡單的組織爲一個標準的文件系統. 命名空間包括了數據註冊器, 在zk中稱作znode, 它們跟文件系統中的文件和目錄很像. 不同於爲了存儲傳統的文件系統, zk的數據是在內存中的, 也就是說zk的有高吞吐量和低延遲.

zk實現在高性能, 高可用性, 嚴格順序性上做的非常出色. 高性能可以支持在大型分佈式系統中使用. 高可用性可以保證在單點掛掉時還可以使用. 嚴格的順序性可以讓客戶端實現複雜的同步功能.

zk是一個類似於文件系統的系統, 它可以協調分佈式系統間的工作; 高性能沒啥說的; 高可用性,這裏後面可以關注下是如何做到的; 嚴格的順序性, 這個是比較有用的功能, 可以幫助客戶端實現一個同步的功能, 類似mq

就像其他分佈式系統一樣, zk自己也會複製多份host, 叫做ensemble.
zk服務必須互相知道對方. 它們會維護一個狀態表在內存中, 通過事物日誌和持久化的快照. 所以只要大部分服務可用, zk就可用.
客戶端連接到某一個單獨的zk服務器. 客戶端會維護一個tcp鏈接, 並使用該鏈接發送請求和接收響應, 事件以及發送心跳. 如果該鏈接斷了, 那麼客戶端會連接到不同的服務器上.


zk會使用數字標記每一次更新, 數字反映了zk事務的順序. 後續操作可以根據這個順序實現一個更高級的抽象, 比如同步操作.

在以讀取操作爲主的場景下, zk的速度特別快. zk運行在多臺機子上, 當它讀取多於寫操作時表現的會很好, 比例10:1(讀:寫)爲最佳.

再次強調嚴格的順序性; 讀取速度比寫快, 大部分系統都這樣, 後面可以關注下, 這裏還特別提到當讀操作比例是寫的10倍的時候性能最佳,恩~不應該是讀越多性能越好麼?

Data model and the hierarchical namespace 數據模型和層級命名空間

zk提供的命名空間特別像一個標準的文件系統. 名稱就是一系列斜槓"/"分割的路徑元素. zk中的每一個節點的名稱都是以路徑命名的.

Nodes and ephemeral nodes 節點和臨時節點

不同於標準的文件系統, zk中的每一個節點可以同時擁有數據和子節點. 就像是一個目錄同時用於數據.(zk被設計來存儲協調數據: 狀態信息, 配置信息, 地址信息等等. 所以存儲到節點的數據一般都很小, 大概是字節到千字節)我們使用znode來表示我們討論的zk數據節點.
znode維護一個狀態結構, 包含了一些狀態修改的版本號, 包括數據修改, ACL(Access Control List 訪問控制表)修改, 時間戳修改, 來支持內存校驗和協調更新. 每次znode數據改變都會將版本號增加. 比如, 無論何時客戶端接收到一條數據, 同時還會接收到該數據的版本.
存儲在znode中的數據支持原子讀和寫. 讀取會獲取znode的所有相關的數據, 寫操作會替換所有znode數據. 每一個節點都有一個acl. 它記錄了誰有權限來做這些事.
zk也有臨時節點的概念. 臨時節點隨着會話創建而創建隨着會話銷燬而刪除. 臨時節點經常被用來實現tbd(?)

zk的文件系統與傳統文件系統最大的區別就是"目錄"不僅有子文件還可以存儲數據(想到B樹和B+樹~), 每一個節點都保存了狀態以及權限等信息, 用於保證數據的一致性. zk也提供了不同的節點類型, 比如臨時節點, 在會話結束的時候就會銷燬, 提問~這裏的tbd到底是什麼?

Conditional updates and watches

*zk支持監控概念. 客戶端可以給znode設置監控watch. 當znode改變時watch可以被觸發和刪除. 當watch被觸發, 客戶端會接收到消息報, 通知它znode被修改了. 如果客戶端和zk某一個服務器斷開鏈接, 客戶端會接收到本地通知. 這些可以被用來實現tbd. *

zk提供的監控功能對於實現配置中心或者其他消息訂閱功能很有幫助

Guarantees

zk很快使用也很簡單, 但是作爲構建多種複雜服務(同步)的基礎設施, zk提供了一系列保證:
1 順序一致性 - 客戶端發送的更新操作會按照他們的發送順序執行.
2 可靠性 - 一旦更新操作被執行, 那麼在客戶端覆蓋這個更新操作之前, 該操作的會一直有效.
3 及時性 - 在一定時間範圍內, zk保證客戶端所看到的系統是最新的.

Simple API

zk目標之一就是提供一套簡單的操作接口, 所以zk只提供瞭如下操作:
create : 在樹形結構中創建一個節點
delete : 刪除一個節點
exists : 測試一個節點是否存在
get data : 從一個節點獲取數據
set data : 寫數據到一個節點
get children : 獲取一個節點的子節點列表
sync : 等待數據有效時傳遞.

Implementation

下圖展示了zk服務的抽象層組成元素. 除了請求處理器, 服務中的每個元素都會複製自己的副本.

複製的數據庫是在內存中, 並且包含完整的數據樹. 更新操作會被記錄到硬盤, 以便後面回覆, 寫操作在應用到內存數據庫之前會被寫到硬盤.
每一個zk服務器都會爲客戶端提供服務. 客戶端實際每次只連接一個服務器去提交請求. 處理請求的數據是來自本地複製的其他所有的數據庫.
請求處理和發送請求都使用一致的協議.
協議中要求, 客戶端發送的所有寫請求都必須發送到一個服務器, 這個服務器稱作leader, 其他的服務器稱作follower, follower接收leader的建議消息並且在消息發送上協調一致. 消息發送層會關注leader由於錯誤掛掉後的替換, 並且會同步給follower.
zk使用一個自定義的具有原子性的消息傳遞協議. 因爲消息傳遞是原子性的, 所以當leader接收到一個寫請求, 它會計算該請求處理後系統的狀態, 然後會將這次的新狀態記錄在事務中.

實現原理後面專門來討論

Uses

zk特別的提供了簡答的api, 但是使用zk可以實現高級的操作, 比如同步操作, 鎖等.

以上就是zk官方文檔的一些翻譯, 讓大家有個初步的瞭解, 推薦一篇 文章

概念明白了, 我們來跑些例子, zookeeper提供的api非常簡單, 下面是java代碼, 主要包括crud, 以及節點的檢查.
環境
jdk 1.8
zookeeper 版本 3.4.14
客戶端maven

<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.4.14</version>
</dependency>

我們可以使用客戶端命令行操作zookeeper, 或者也可以通過ui客戶端操作

public class ZooMain implements Watcher {

    private static ZooKeeper zooKeeper;

    public ZooMain(String host, int timeout) {
        try {
            zooKeeper = new ZooKeeper(host, timeout, this);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 創建節點
     *
     * @param node
     * @param data
     */
    public void createNode(String node, byte[] data) {
        try {
            zooKeeper.create(node, data, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        } catch (KeeperException | InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 刪除節點
     *
     * @param node
     * @param version
     */
    public void delete(String node, int version) {

        try {
            zooKeeper.delete(node, version);
        } catch (InterruptedException | KeeperException e) {
            e.printStackTrace();
        }
    }

    /**
     * 更新節點
     *
     * @param node
     * @param data
     * @param version
     */
    public void update(String node, byte[] data, int version) {

        try {
            zooKeeper.setData(node, data, version);
        } catch (KeeperException | InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 查詢節點
     *
     * @param node
     * @return
     */
    public String select(String node) {

        try {
            byte[] data = zooKeeper.getData(node, this, null);
            return new String(data, StandardCharsets.UTF_8);
        } catch (KeeperException | InterruptedException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 版本信息
     *
     * @param node
     * @return
     */
    public int version(String node) {

        try {
            return zooKeeper.exists(node, true).getVersion();
        } catch (KeeperException | InterruptedException e) {
            e.printStackTrace();
        }

        return -1;
    }

    /**
     * 檢查是否存在
     *
     * @param node
     * @param watch
     * @return
     */
    public boolean exists(String node, boolean watch) {

        try {
            Stat stat = zooKeeper.exists("/fff", watch);
            return stat != null;
        } catch (KeeperException | InterruptedException e) {
            e.printStackTrace();

            throw new RuntimeException();
        }
    }

    /**
     * 監聽事件
     *
     * @param event
     */
    @Override
    public void process(WatchedEvent event) {

        if (event.getType() == Event.EventType.NodeDataChanged) { // 節點變換
            System.out.println("changed");
            System.out.println(select("/java"));
        }
    }
}

如果你在執行exists方法報類似錯誤 “KeeperErrorCode = ConnectionLoss for /node-name”, 網上有各種解決辦法, 如果你嘗試了都不行, 那你可以嘗試根據服務端更換下jar包, 比如我用的zookeeper是3.4.14, 我的客戶端版本是3.5就會報錯, 修改爲3.4.14後就正常了, 這個原因我們記一下, 持續關(bu)注(guan)…

關於監聽watch

1 監聽是一次性的, one-time trigger, 如果需要持續監聽, 需要我們在監聽到變化後繼續設置監聽達到持續監聽.
2 監聽只能在讀取操作的時候getData getChildren exists 纔有可能設置監聽

實現監聽

需要實現Watcher接口並實現下面的方法
public void process(WatchedEvent event);
WatchedEvent數據結構包括三部分

final private KeeperState keeperState;
final private EventType eventType;
private String path;
keeperState

zookeeper的session狀態: Disconnected 關閉, SyncConnected 同步連接, AuthFailed 權限失敗, ConnectedReadOnly 只讀連接, SaslAuthenticated, or Expired 過期.

eventType
NodeCreated 節點創建

創建一個nodeCreated監聽通過執行exists方法.

NodeDeleted 節點刪除

通過exists和getData創建監聽

NodeDataChanged 節點數據改變

通過exists和getData創建監聽

NodeChildrenChanged 節點子節點改變

通過getChildren創建監聽, 上面三個都是針對單一節點, 這個監聽針對的是多個子節點. 只針對第一級子節點.

none

使用none表示zookeeper的session被改變了

znodePath

如果eventType不是none的話, znodePath存在, 表示znode路徑; 如果監聽的是子節點, 返回的是父節點的路徑, 也就是說, 這裏返回的肯定是設置的監聽節點路徑

監聽使用

public byte[] getData(final String path, Watcher watcher, Stat stat);
public byte[] getData(String path, boolean watch, Stat stat);

api提供很多類似這樣的方法, 第一個參數就是操作的節點路徑; 第二個參數是關於監聽的, 第一個方法就是使用一個新的監聽, 第二個方法如果傳遞true表示使用默認的監聽器, 默認監聽器是我們在初始化zookeeper客戶端的時候設置的. 第三個參數是一個zookeeper的一個狀態信息, 如果設置了, 會在方法執行完成後將zookeeper相關信息複製到stat中, 客戶端可以通過這個方法獲取其中的信息.

zookeeper在3.4及以前刪除監聽的方法:
1 監聽執行了process(一次性)
2 session關閉或者過期
zookeeper在3.5之後可以通過執行removeWatches方法刪除

我的註冊中心

以上是學習zookeeper的一些基礎知識, 後面可能(never)會繼續深入學習, 現在我們根據以上所有實現我們自己的註冊中心. 在我們之前的文章中已經實現了服務的掃描工作, 所以註冊就是將掃描到的服務存儲到zookeeper上, 然後客戶端通過服務找到服務器並進行遠程調用.

註冊

那麼首先是註冊操作. 註冊之前首先得知道zookeeper的地址, 如果你是一個敏感的男銀, 肯定會發現, 在之前的代碼中很多參數都是寫死在代碼中的, 包括zookeeper地址等(不寫死代碼就被代碼寫死.). 這次本着我們高大上的原則, 當然要從配置文件中讀取了. 主要代碼如下

初始化properties工具類

private static final String configName = "application.properties";

public void init() {

    properties = new Properties();
    InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(configName);
    if (inputStream != null) {
        try {
            properties.load(inputStream);
        } catch (IOException e) {

            log.error("failed to read properties: [{}]", configName, e);
        }
    }
}

簡單讀取方法

public String read(String key) {

    if (properties == null) {
        log.error("failed to read key [{}] from properties: [{}]", key, configName);
        throw new NullPointerException("properties is null");
    }

    log.info(properties.getProperty(key));

    return properties.getProperty(key);
}

public int readInt(String key) {

    String value = read(key);
    return Integer.parseInt(value);
}

服務初始化階段增加配置初始化和註冊初始化和註冊操作
(以後註釋用英文了.)

public static void init() throws Exception {

    log.info("PRC server init...");
    // config init
    ConfigUtil.instance().init();
    // register init
    Register.instance().init();
    TwoTuples<String, String> twoTuples = ContextUtil.findRoot(classPath());
    // scan service implement
    ContextUtil.scanAndLoad(twoTuples);
    // register sign in service
    Register.instance().signIn();
    start();

    log.info("PRC server init end...");
}

註冊類的初始化和註冊

public void init() {

    ConfigUtil configUtil = ConfigUtil.instance();
    host = configUtil.read("register.host");
    port = configUtil.readInt("register.port");
    log.info("register init...");
    orzZooKeeper = new OrzZooKeeper(host + ":" + port, timeout);
    log.info("register init end");
}

public void signIn() {

    Map<Key, String> implMap = ServiceContext.INSTANCE.getAllServiceImpls();
    if (orzZooKeeper.notExists(root, false)) {
        orzZooKeeper.createNode(root);
    }

    implMap.forEach((k, v) -> {

        orzZooKeeper.createDeepNode("", k.orzName(), ip() + ":" + RPCServer.port);
    });
}

OrzZooKeeper是操作zookeeper的工具類, 包括最基本的增刪改查等操作, 具體代碼就不貼了, 跟上面學習zookeeper中例子差不多.
執行後zookeeper中存儲如圖

獲取註冊服務

服務端註冊好服務後, 客戶端在執行遠程調用時就需要去獲取提供服務的地址. 這裏我們主要對RPCClient類改造, 初始化方法如下

private static RPCClient client = new RPCClient();

private boolean initOk = false;

public static RPCClient instance() {
    return client;
}

public void init() {

    if (initOk)
        return;

    ConfigUtil.instance().init();
    Register.instance().init();

    initOk = true;
}

配置類和註冊中心的初始化和服務端是一樣的.
之前RPCClient啓動先執行一個static塊代碼, 創建和服務端的Socket連接(地址寫死的), 這回使用註冊中心, 這裏發送的邏輯需要修改

public Object sendMsg(String msg, Key key) throws IOException {

    PrintStream out = outputStream(key);
    BufferedReader input = inputStream(key);
    out.println(msg);

    String ret = input.readLine();
    log.info("服務器端返回過來的是: " + ret);

    out.close();
    input.close();

    return ret;
}

public PrintStream outputStream(Key key) {

    if (outMap.get(key) != null)
        return outMap.get(key);

    buildSocket(key);

    return outMap.get(key);
}

private void buildSocket(Key key) {

    ServiceAddress address = Register.instance().scan(key);
    Socket socket;
    try {
        socket = new Socket(address.getHost(), address.getPort());
        BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        PrintStream out = new PrintStream(socket.getOutputStream());
        inputMap.put(key, input);
        outMap.put(key, out);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

代碼很簡單, 就是發送消息前會先獲取和服務端的連接, 服務端地址來源於註冊中心:

public ServiceAddress scan(Key key) {

    String node = root + "/" + key.orzName();

    List<String> children = orzZooKeeper.selectChildren(node);

    return loadStrategy(children);
}

/**
 * @param children
 * @return
 */
private ServiceAddress loadStrategy(List<String> children) {
    // TODO taiyn 2020/4/11
    if (CollectionUtil.isEmpty(children))
        throw new IllegalArgumentException();

    String[] address = children.get(0).split(":");
    return new ServiceAddress(address[0], Integer.parseInt(address[1]));
}

總結

本次加入了註冊中心, 順便學習了一些zookeeper的知識. orz差的東西還很多, 下一篇會學習下dubbo的註冊過程. 下下篇是netty的加入, 下下下篇就是dubbo中網絡通信的學習. 先安排到這.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章