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

小新是也發表於2021-08-24

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

相關文章