從零開始實現簡單 RPC 框架 4:註冊中心
RPC 中服務消費端(Consumer
) 需要請求服務提供方(Provider
)的介面,必須要知道 Provider
的地址才能請求到。
那麼,Consumer
要從哪裡獲取 Provider
的地址呢?
能不能 Consumer
自己配置 Provider
的地址?
這種方式理論上是可行的,不過事實上沒人這麼做。這種方式有以下缺點:
Consumer
每引用一個介面,需要配置一次Provider
的服務地址,配置繁瑣易錯。Consumer
引用其他業務組的服務,需要跨團隊溝通,溝通成本高。Provider
如果換伺服器、掛掉、新增,都需要通知到Consumer
去修改服務地址,配置修改可能不及時造成服務異常。Consumer
如果引用很多服務,那麼配置會非常雜亂,管理起來非常麻煩。
從上面的缺點來看,最好的方式是找個地方把配置管理起來。
例如,把配置放到統一的資料庫中,Provider
啟動的時候,把自己的地址和介面寫到表中; Consumer
在請求介面之前,就可以從表裡獲取該介面對應的Provider
地址。
其實,這種把配置統一管理的地方,就叫 註冊中心
註冊中心就像中間橋樑,連線Provider
和Consumer
。三方關係示意圖如下:
註冊中心 只是 Provider
感知 Consumer
的一種方式而已,最終 Provider
呼叫 Consumer
介面還是以直連的方式進行。
Provider
Consumer
,保證 Consumer
感知服務狀態的及時性。
註冊中心的特性
一個合格的註冊中心,需要有以下的特性:
1. 儲存
可以簡單地將註冊中心理解為一個儲存系統,儲存著服務與服務提供方的對映表。一般註冊中心對儲存沒有太多特別的要求,甚至誇張一點,你可以基於資料庫來實現一個註冊中心。
2. 高可用
註冊中心一旦掛掉,Consumer
將無法獲取 Provider
的地址,整個微服務將無法運轉。
當然 Consumer
可以新增本地快取,從某種角度上看,是允許註冊中心短暫掛掉的。
3. 健康檢查
Provider
向註冊中心註冊服務之後,註冊中心需要定時向 Provider
Provider
宕機的時候,註冊中心能更快發現 ,從而將宕機的 Provider
從登錄檔中移除。這特性資料庫、Redis 都不具有,因此他們不適合做註冊中心。
4. 監聽狀態
當服務增加、減少 Provider
的時候,註冊中心除了能及時更新,還要能主動通知 Consumer
,以便 Consumer
能快速更新本地快取,減少錯誤請求的次數。
這一特性同樣資料庫、Redis都不具有。
目前主流的註冊中心有:Zookeeper
、Eureka
、Nacos
、Consul
等。
由於本文主要是講註冊中心的實現,就不詳細講各種註冊中心的差異、優缺點了,有興趣的同學可以看這裡
下面我們來講 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
本地快取不存在的時候,會請求到 ZkRegistry
的 doLookup
,請求出來的 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