從零開始實現簡單 RPC 框架 4:注冊中心


RPC 中服務消費端(Consumer) 需要請求服務提供方(Provider)的接口,必須要知道 Provider 的地址才能請求到。
那么,Consumer 要從哪里獲取 Provider 的地址呢?

能不能 Consumer 自己配置 Provider 的地址?
這種方式理論上是可行的,不過事實上沒人這么做。這種方式有以下缺點:

  1. Consumer 每引用一個接口,需要配置一次 Provider 的服務地址,配置繁瑣易錯。
  2. Consumer 引用其他業務組的服務,需要跨團隊溝通,溝通成本高。
  3. Provider 如果換服務器、掛掉、新增,都需要通知到 Consumer 去修改服務地址,配置修改可能不及時造成服務異常。
  4. Consumer 如果引用很多服務,那么配置會非常雜亂,管理起來非常麻煩。

從上面的缺點來看,最好的方式是找個地方把配置管理起來
例如,把配置放到統一的數據庫中,Provider 啟動的時候,把自己的地址和接口寫到表中; Consumer 在請求接口之前,就可以從表里獲取該接口對應的Provider地址。
其實,這種把配置統一管理的地方,就叫 注冊中心

注冊中心就像中間橋梁,連接ProviderConsumer。三方關系示意圖如下:
RPC框架最簡單的結構
注冊中心 只是 Provider 感知 Consumer 的一種方式而已,最終 Provider 調用 Consumer 接口還是以直連的方式進行。
Provider 注冊或者取消注冊,注冊中心會通知 Consumer,保證 Consumer 感知服務狀態的及時性。

注冊中心的特性

一個合格的注冊中心,需要有以下的特性:

1. 存儲

可以簡單地將注冊中心理解為一個存儲系統,存儲着服務與服務提供方的映射表。一般注冊中心對存儲沒有太多特別的要求,甚至誇張一點,你可以基於數據庫來實現一個注冊中心。

2. 高可用

注冊中心一旦掛掉,Consumer 將無法獲取 Provider 的地址,整個微服務將無法運轉。
當然 Consumer 可以添加本地緩存,從某種角度上看,是允許注冊中心短暫掛掉的。

3. 健康檢查

Provider 向注冊中心注冊服務之后,注冊中心需要定時向 Provider 發起健康檢查,當 Provider 宕機的時候,注冊中心能更快發現 ,從而將宕機的 Provider 從注冊表中移除。
這特性數據庫、Redis 都不具有,因此他們不適合做注冊中心。

4. 監聽狀態

當服務增加、減少 Provider 的時候,注冊中心除了能及時更新,還要能主動通知 Consumer,以便 Consumer 能快速更新本地緩存,減少錯誤請求的次數。
這一特性同樣數據庫、Redis都不具有。

目前主流的注冊中心有:ZookeeperEurekaNacosConsul 等。
由於本文主要是講注冊中心的實現,就不詳細講各種注冊中心的差異、優缺點了,有興趣的同學可以看這里

下面我們來講 ccx-rpc 的注冊中心是如何實現的。

注冊中心的設計與實現

接口定義

下面是注冊中心的接口,最簡單就包含兩個方法:注冊查找

public interface Registry {

    /**
     * 向注冊中心注冊服務
     *
     * @param url 注冊者的信息
     */
    void register(URL url);

    /**
     * 查找注冊的服務
     *
     * @param condition 查詢條件
     * @return 符合查詢條件的所有注冊者
     */
    List<URL> lookup(URL condition);
}

本地緩存

為了減緩注冊中心的壓力,需要加上本地緩存,減少請求。同時也可以增加可用性,當注冊中心掛的時候,本地還可以使用緩存中的數據。這部分邏輯否裝在 AbstractRegistry 中,其他的實現都繼承 AbstractRegistry

變量 registered 將服務信息緩存在 Map 中,服務名為 Key,Value 則是該服務注冊的 Provider 列表。

/**
* 已注冊的服務的本地緩存。{serviceName: [URL]}
*/
private final Map<String, Set<String>> registered = new ConcurrentHashMap<>();

當注冊的 Provider 增加、減少的時候,會全量更新該服務下的 Provider 列表。

/**
 * 重置。真實拿出注冊信息,然后加到緩存中。
 */
public List<URL> reset(URL condition) {
    // 獲取服務名
    String serviceName = getServiceNameFromUrl(condition);
    // 將原來注冊信息本地緩存刪掉
    registered.remove(serviceName);
    // 重新從注冊中心獲取
    List<URL> urls = doLookup(condition);
    for (URL url : urls) {
        // 將所有 Provider 添加到本地緩存
        addToLocalCache(url);
    }
    return urls;
}

/**
 * 添加到本地緩存
 */
private void addToLocalCache(URL url) {
    String serviceName = getServiceNameFromUrl(url);
    if (!registered.containsKey(serviceName)) {
        registered.put(serviceName, new ConcurrentHashSet<>());
    }
    registered.get(serviceName).add(url.toFullString());
}

Zookeeper 實現

ccx-rpc 中,注冊中心實現了 zookeeper,實現類是 ZkRegistry
Zookeeper 客戶端使用的是 Curator 框架,比官方的好用多了。

1. 注冊

服務注冊的時候,會在 /ccx-rpc/${serviceName}/providers 下創建一個臨時節點
為什么是臨時節點呢?臨時節點有個功能就是,當客戶端斷開連接的時候,該客戶端創建的節點都會自動刪除,這個特性非常適合注冊中心。

public void doRegister(URL url) {
    zkClient.createEphemeralNode(toUrlPath(url));
    watch(url);
}

創建的臨時節點的內容是 Provider 的 URL 信息
示例:ccx-rpc://192.168.10.111:5525?interface=com.ccx.rpc.demo.service.api.UserService&version=
因為 URL 中包含 /,所以需要進行 url 編碼,最終在 Zookeeper 存的是:
ccx-rpc%3A%2F%2F192.168.10.111%3A5525%3Finterface=com.ccx.rpc.demo.service.api.UserService&version=

/**
 * 轉成全路徑,包括節點內容。
 * 例如:/ccx-rpc/com.ccx.rpc.demo.service.api.UserService/providers/ccx-rpc%3A%2F%2F192.168.10.111%3A5525%3Finterface=com.ccx.rpc.demo.service.api.UserService&version=
 */
private String toUrlPath(URL url) {
    return toServicePath(url) + "/" + urlEncoder.encode(url.toFullString(), charset);
}

/**
 * 轉成服務的路徑。
 * 例如:/ccx-rpc/com.ccx.rpc.demo.service.api.UserService/providers
 */
private String toServicePath(URL url) {
    return getServiceNameFromUrl(url) + "/" + RegistryConst.PROVIDERS_CATEGORY;
}

2. 查找

Consumer 直接獲取服務路徑下的所有子節點即可。

public List<URL> doLookup(URL condition) {
    List<String> children = zkClient.getChildren(toServicePath(condition));
    List<URL> urls = children.stream()
            .map(s -> URLParser.toURL(URLDecoder.decode(s, charset)))
            .collect(Collectors.toList());
    return urls;
}

3. 監聽

Zookeeper 還有一個很強的功能:監聽。當監聽的路徑發生狀態變化時,會全量更新(reset)對應的服務的本地緩存。reset 方法在上面的 AbstractRegistry 有講到,這里就不重復貼代碼了。

/**
 * 監聽
 */
private void watch(URL url) {
    String path = toServicePath(url);
    zkClient.addListener(path, (type, oldData, data) -> {
        reset(url);
    });
}

那么,我們是如何知道要監聽哪些路徑的呢?當 AbstractRegistry 本地緩存不存在的時候,會請求到 ZkRegistrydoLookup,請求出來的 Provider 都進行監聽。

public List<URL> doLookup(URL condition) {
    List<String> children = zkClient.getChildren(toServicePath(condition));
    List<URL> urls = children.stream()
            .map(s -> URLParser.toURL(URLDecoder.decode(s, charset)))
            .collect(Collectors.toList());
    // 獲取到的每個都添加監聽
    for (URL url : urls) {
        watch(url);
    }
    return urls;
}

總結

注冊中心的設計比較簡單,一個注冊register和查找lookup就能簡單滿足要求。
為了提高性能和可用性,AbstractRegistry 還增加了本地緩存,其他實現繼承 AbstractRegistry
最后我們講了 ZkRegistry 的實現,主要就是注冊查找監聽
其他類型的注冊中心按照這個模板,實現起來就會非常簡單啦,如果有童鞋想實現其他的注冊中心,歡迎給 ccx-rpc 提 PR。

ccx-rpc 代碼已經開源
Github:https://github.com/chenchuxin/ccx-rpc
Gitee:https://gitee.com/imccx/ccx-rpc


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM