聊聊服務註冊與發現
服務發現,作爲互聯網從業人員,大家應該都不陌生,一個完善的服務集羣,服務發現是必不可少的功能之一。
最近一直想寫這個話題,也一直在構思,但不知道從何入手,或者說不知道寫哪方面。如果單純寫如何實現,這個未免太乏味枯燥了;而如果只是介紹現有成熟方案呢,卻達不到我的目的。想了很久,準備先從微服務的架構入手,切入 服務發現 要解決什麼問題,搭配常見的處理模式,最後介紹下現有的處理方案。
微服務服務於分佈式系統,是個分散式系統。服務部署跨主機、網段、機房乃至省市自治區。各個服務之間通過 RPC(remote procedure call) 進行調用。在架構上最重要的一環,就是服務發現。如果說服務發現是微服務架構的靈魂也當之無愧,試想一下,當一個系統被拆分成多個服務,且被大量部署的時候,有什麼能比 "找到" 想調用的服務在哪裏,以及能否正常提供服務重要呢?同樣的,有新服務啓動時,如何讓其他服務知道其存在呢?
微服務考驗的是治理大量服務的能力,包含多種服務,同樣也包含多個實例。
概念
服務發現是指使用一個註冊中心來記錄分佈式系統中的全部服務的信息,以便其他服務能夠快速的找到這些已註冊的服務。
服務發現之所以重要,是因爲它解決了微服務架構最關鍵的問題:如何精準的定位需要調用的服務 ip 以及端口。無論使用哪種方式來提供服務發現功能,大致上都包含以下三點:
-
Register, 服務啓動時候進行註冊
-
Query, 查詢已註冊服務信息
-
Healthy Check, 確認服務狀態是否健康
整個過程很簡單。大致就是在服務啓動的時候,先去進行註冊,並且定時反饋本身功能是否正常。由服務發現機制統一負責維護一份正確或者可用的服務清單。因此,服務本身需要能隨時接受查下,反饋調用方服務所要的信息。
註冊模式
一整套服務發現機制順利運行,首先就得維護一份可用的服務列表。包含服務註冊與移除功能,以及健康檢查。服務是如何向註冊中心 "宣告" 自身的存在?健康檢查,是如何確認這些服務是可用的呢?
做法大致分爲兩類:
-
自注冊模式
自注冊,顧名思義,就是上述這些動作,由服務提供者 (client) 本身來維護。每個服務啓動後,需要到統一的服務註冊中心進行註冊登記,服務正常終止後,也可以到註冊中心移除自身的註冊記錄。在服務執行過程中,通過不斷的發送心跳信息,來通知註冊中心,本服務運行正常。註冊中心只要超過一定的時間沒有收到心跳消息,就可以將這個服務狀態判斷爲異常,進而移除該服務的註冊記錄。
-
三方註冊模式
這個模式與自注冊模式相比,區別就是健康檢查的動作不是由服務本身 (client) 來負責,而是由其它第三方服務來確認。有時候服務自身發送心跳信息的方式並不精確,因爲可能服務本身已經存在故障,某些接口功能不可用,但仍然可以不斷的發送心跳信息,導致註冊中心沒有發覺該服務已經異常,從而源源不斷的將流量打到已經異常的服務上來。
這時候,要確認服務是否正常運轉的健康檢查機制,就不能只依靠心跳,必須通過其它第三方的驗證 (ping),不斷的從外部來確認服務本身的健康狀態。
這些都是有助於協助註冊中心提高服務列表精確到的方法。能越精確的提高服務清單狀態的可靠性,整套微服務架構的可靠度就會更高。這些方法不是互斥的,在必要的時候,可以搭配使用。
發現模式
服務發現的發現機制主要包括三個方面:
-
服務提供者:服務啓動時將服務信息註冊到註冊中心,服務退出時將註冊中心的服務信息刪除掉。
-
服務消費者:從服務註冊表獲取服務提供者的最新網絡位置等服務信息,維護與服務提供者之間的通信
-
註冊中心:服務提供者和服務消費者之間的一個橋樑
服務發現機制的關鍵部分是註冊中心。註冊中心提供管理和查詢服務註冊信息的 API。當服務提供者的實例發生變更時(新增 / 刪除服務),服務註冊表更新最新的狀態列表,並將其最新列表以適當的方式通知給服務消費者。目前大多數的微服務框架使用 Netflix Eureka、Etcd、Consul 或 Apache Zookeeper 等作爲註冊中心。
爲了說明服務發現模式是如何解決微服務實例地址動態變化的問題,下面介紹兩種主要的服務發現模式:
-
客戶端發現模式
-
服務端發現模式。
客戶端模式與服務端模式,兩者的本質區別在於,客戶端是否保存服務列表信息。
客戶端發現模式
在客戶端模式下,如果要進行微服務調用,首先要進行的是到服務註冊中心獲取服務列表,然後再根據調用端本地的負載均衡策略,進行服務調用。
在上圖中,client 端提供了負載均衡的功能,其首先從註冊中心獲取服務提供者的列表,然後通過自身負載均衡算法,選擇一個最合理的服務提供者進行調用:
1、 服務提供者向註冊中心進行註冊,提交自己的相關信息
2、 服務消費者定期從註冊中心獲取服務提供者列表
3、 服務消費者通過自身的負載均衡算法,在服務提供者列表裏面選擇一個合適的服務提供者,進行訪問
客戶端發現模式的優缺點如下:
-
優點:
-
負載均衡作爲 client 中一個功能,用自身的算法,從服務提供者列表中選擇一個合適服務提供者進行訪問,因此 client 端可以定製化負載均衡算法。優點是服務客戶端可以靈活、智能地制定負載均衡策略,包括輪詢、加權輪詢、一致性哈希等策略。
-
可以實現點對點的網狀通訊,即去中心化的通訊。可以有效避開單點造成的性能瓶頸和可靠性下降等問題。
-
服務客戶端通常以 SDK 的方式直接引入到項目,這種方式語言的整合程度最佳,程序執行性能最佳,程序錯誤排查更加容易。
-
缺點:
-
當負載均衡算法需要更新時候,很難做到同一時間全部更新,所以就造成新舊算法同時運行
-
與註冊中心緊密耦合,如果要換註冊中心,需要去修改代碼,重新上線。微服務的規模越大,服務更新越困難,這在一定程度上違背了微服務架構提倡的技術獨立性。
目前來說,大部分服務發現的實現都採取了客戶端模式(sofapbrpc、brpc 等)。
服務端發現模式
在服務端模式下,調用方直接向服務註冊中心進行請求,服務註冊中心再通過自身負載均衡策略,對微服務進行調用。這個模式下,調用方不需要在自身節點維護服務發現邏輯以及服務註冊信息。
在服務端模式下:
1、 服務提供者向註冊中心進行服務註冊
2、 註冊中心提供負載均衡功能
3、 服務消費者去請求註冊中心,由註冊中心根據服務提供列表的健康情況,選擇合適的服務提供者供服務消費者調用
現代容器化部署平臺(如 Docker 和 Kubernetes)就是服務端服務發現模式的一個例子,這些部署平臺都具有內置的服務註冊表和服務發現機制。容器化部署平臺爲每個服務提供路由請求的能力。服務客戶端向路由器(或者負載均衡器)發出請求,容器化部署平臺自動將請求路由到目標服務一個可用的服務實例。因此,服務註冊,服務發現和請求路由完全由容器化部署平臺處理。
服務端發現模式的特點如下:
-
優點:
-
服務消費者不需要關心服務提供者的列表,以及其採取何種負載均衡策略
-
負載均衡策略的改變,只需要註冊中心修改就行,不會出現新老算法同時存在的現象
-
服務提供者上下線,對於服務消費者來說無感知
-
缺點:
-
rt 增加,因爲每次請求都要請求註冊中心,由其返回一個服務提供者
-
註冊中心成爲瓶頸,所有的請求都要經過註冊中心,如果註冊服務過多,服務消費者流量過大,可能會導致註冊中心不可用
-
微服務的一個目標是故障隔離,將整個系統切割爲多個服務共同運行,如果某服務無法正常運行,只會影響到整個系統的相關部分功能,其它功能能夠正常運行,即去中心化。然而,服務端發現模式實際上是集中式的做法,如果路由器或者負載均衡器無法提供服務,那麼將導致整個系統癱瘓。
實現方案
file
以文件的形式實現服務發現,這是一個比較簡單的方案。其基本原理就是將服務提供者的信息 (ip:port) 寫入文件中,服務消費者加載該文件,獲取服務提供者的信息,根據一定的策略,進行訪問。
需要注意的是,因爲以文件形式提供服務發現,服務消費者要定期的去訪問該文件,以獲得最新的服務提供者列表,這裏有個小優化點,就是可以有個線程定時去做該任務,首先去用該文件的最後一次修改時間跟服務上一次讀取文件時候存儲的修改時間做對比,如果時間一致,表明文件未做修改,那麼就不需要重新做加載了,反之,重新加載文件。
文件方式實現服務發現,其特點顯而易見:
-
優點:實現簡單,去中心化
-
缺點:需要服務消費者去定時操作,如果某一個文件推送失敗,那麼就會造成異常現象
zookeeper
ZooKeeper 是一個集中式服務,用於維護配置信息、命名、提供分佈式同步和提供組服務。
zookeeper 是一個樹形結構,如上圖所示。
使用 zookeeper 實現服務發現的功能,簡單來講,就是使用 zookeeper 作爲註冊中心。服務提供者在啓動的時候,向 zookeeper 註冊其信息,這個註冊過程其實就是實際上在 zookeeper 中創建了一個 znode 節點,該節點存儲了 ip 以及端口等信息,服務消費者向 zookeeper 獲取服務提供者的信息。服務註冊、發現過程簡述如下:
-
服務提供者啓動時,會將其服務名稱,ip 地址註冊到配置中心
-
服務消費者在第一次調用服務時,會通過註冊中心找到相應的服務的 IP 地址列表,並緩存到本地,以供後續使用。當消費者調用服務時,不會再去請求註冊中心,而是直接通過負載均衡算法從 IP 列表中獲取一個服務提供者的服務器調用服務
-
當服務提供者的某臺服務器宕機或下線時,相應的 ip 會從服務提供者 IP 列表中移除。同時,註冊中心會將新的服務 IP 地址列表發送給服務消費者機器,緩存在消費者本機
-
當某個服務的所有服務器都下線了,那麼這個服務也就下線了
-
同樣,當服務提供者的某臺服務器上線時,註冊中心會將新的服務 IP 地址列表發送給服務消費者機器,緩存在消費者本機
-
服務提供方可以根據服務消費者的數量來作爲服務下線的依據
服務註冊
假設我們服務提供者的服務名稱爲 services, 首先在 zookeeper 上創建一個 path /services, 在服務提供者啓動時候,向 zookeeper 進行註冊,其註冊的原理就是創建一個路徑,路徑爲 / services/$ip:port,其中 ip:port 爲服務提供者實例的 ip 和端口。如下圖所示,我們現在 services 實例有三個,其 ip:port 分別爲 192.168.1.1:1234、192.168.1.2:1234、192.168.1.3:1234 和 192.168.1.4:1234,如下圖所示:
健康檢查
zookeeper 實現了一種 TTL 的機制,就是如果客戶端在一定時間內沒有向註冊中心發送心跳,則會將這個客戶端摘除。
獲取服務提供者的列表
前面有提過,zookeeper 實際上是一個樹形結構,那麼服務消費者是如何獲取到服務提供者的信息呢?最重要的也是必須的一點就是 知道服務提供者信息的父節點路徑。以上圖爲例,我們需要知道
/services
通過 zookeeper client 提供的接口 getchildren(path) 來獲取所有的子節點。
感知服務上線與下線
zookeeper 提供了 “心跳檢測” 功能,它會定時向各個服務提供者發送一個請求(實際上建立的是一個 socket 長連接),如果長期沒有響應,服務中心就認爲該服務提供者已經“掛了”,並將其剔除,比如 192.168.1.2 這臺機器如果宕機了,那麼 zookeeper 上的路徑 / services / 下就會只剩下 192.168.1.1:1234, 192.168.1.2:1234,192.168.1.4:1234。如下圖所示:
假設此時,重新上線一個實例,其 ip 爲 192.168.1.5,那麼此時 zookeeper 樹形結構如下圖所示:
服務消費者會去監聽相應路徑(/services),一旦路徑上的數據有任務變化(增加或減少),zookeeper 都會通知服務消費方服務提供者地址列表已經發生改變,從而進行更新。
實現
下面是服務提供者在 zookeeper 註冊中心註冊時候的核心代碼:
int ZKClient::Init(const std::string& host, int timeout,
int retry_times) {
host_ = host;
timeout_ = timeout;
retry_times_ = retry_times;
hthandle_ = zookeeper_init(host_.c_str(), GlobalWatcher, timeout_,
NULL, this, 0);
return (hthandle_ != NULL) ? 0 : -1;
}
int ZKClient::CreateNode(const std::string& path,
const std::string& value,
int type) {
int flags;
if (type == Normal) {
flags = 0;
} else if (type == Ephemeral) {
flags = ZOO_EPHEMERAL;
} else {
return -1;
}
int ret = zoo_exists(hthandle_, path.c_str(), 0, NULL);
if (ret == ZOK) {
return -1;
}
if (ret != ZNONODE) {
return -1;
}
ret = zoo_create(hthandle_, path.c_str(), value.c_str(), value.length(),
&ZOO_OPEN_ACL_UNSAFE, flags, NULL, 0);
return ret == ZOK ? 0 : -1;
}
int main() {
std::string ip; // 當前服務ip
int port; // 當前服務的端口
std::string path = "/services/" + ip + ":" + std::to_string(port);
ZKClient zk;
zk.init(...);
//初始化zk客戶端
zk.CreateNode(path, "", Ephemeral);
...
return 0
}
上面是服務提供者所做的一些操作,其核心功能就是:
在服務啓動的時候,使用 zookeeper 的客戶端,創建一個臨時 (Ephemeral) 節點
從代碼中可以看出,創建 znode 的時候,指定了其 node 類型爲 Ephemeral,這塊非常重要,在 zookeeper 中,如果 znode 類型爲 Ephemeral,表明,在服務提供者跟註冊中心斷開連接的時候,這個節點會自動消失,進而註冊中心會通知服務消費者重新獲取服務提供者列表。
下面是服務消費者的核心代碼:
int ZKClient::Init(const std::string& host, int timeout,
int retry_times) {
host_ = host;
timeout_ = timeout;
retry_times_ = retry_times;
hthandle_ = zookeeper_init(host_.c_str(), GlobalWatcher, timeout_,
NULL, this, 0);
return (hthandle_ != NULL) ? 0 : -1;
}
int ZKClient::GetChildren(const std::string& path,
const std::function<int(const std::vector<std::tuple<std::string, std::string>>)>& on_change,
std::vector<std::tuple<std::string, std::string>>* children) {
std::lock_guard<std::recursive_mutex> lk(mutex_);
int ret = zoo_get_children(handle, path, 1, children); // 通過來獲取子節點
if (ret == ZOK) {
node2children_[path] = std::make_tuple(on_change, *children); // 註冊事件
}
return ret;
}
int main() {
ZKClient zk;
zk.Init(...);
std::vector<std::string> children
// 設置回調通知,即在某個path下子節點發生變化時,進行通知
zk.GetChildren("/services", callback, children);
...
return 0;
}
對於服務消費者來說,其需要有兩個功能:
-
獲取服務提供者列表
-
在服務提供者列表發生變化時,能得到通知
其中第一點可以通過
zoo_get_children(handle, path, 1, children);
來獲取列表,那麼如何在服務提供者列表發生變化時得到通知呢?這就用到了 zookeeper 中的 watcher 機制。
watcher 目的是在 znode 以某種方式發生變化時得到通知。watcher 僅被觸發一次。如果您想要重複通知,您將需要重新註冊觀察者。讀取操作(例如 exists、get_children、get_data)可能會創建監視。
okeeper 中的 watcher 機制,不在本文的討論範圍內,有興趣的讀者,可以去查閱相關書籍或者資料。
下面,我們對使用 zookeeper 作爲註冊中心,服務提供者和消費者需要做的操作進行下簡單的總結:
-
服務提供者
-
在服務啓動的時候,使用 zookeeper 的客戶端,創建一個臨時 (Ephemeral) 節點
-
服務消費者
-
通過 zoo_get_children 獲取子節點
-
註冊 watcher 回調,在 path 下發生變化時候,會直接調用該回調函數,在回調函數內重新獲取子節點,並重新註冊回調
etcd
Etcd 是基於 Go 語言實現的一個 KV 結構的存儲系統,支持服務註冊與發現的功能,官方將其定義爲一個可信賴的分佈式鍵值存儲服務,主要用於共享配置和服務發現。其特點如下:
-
:安裝配置簡單,而且提供了 HTTP API 進行交互,使用也很簡單 鍵值對存儲:
-
據存儲在分層組織的目錄中,如同在標準文件系統中
-
變更:監測特定的鍵或目錄以進行更改,並對值的更改做出反應
-
:根據官方提供的 benchmark 數據,單實例支持每秒 2k+ 讀操作
-
:採用 Raft 算法,實現分佈式系統數據的可用性和一致性
服務註冊
每一個服務器啓動之後,會向 Etcd 發起註冊請求,同時將自己的基本信息發送給 etcd 服務器。服務器的信息是通過 KV 鍵值進行存儲。key 是用戶真實的 key, value 是對應所有的版本信息。keyIndex 保存 key 的所有版本信息,每刪除一次都會生成一個 generation,每個 generation 保存了這個生命週期內從創建到刪除中間的所有版本號。
更新數據時,會開啓寫事務。
-
會根據當前版本的 key,rev 在 keyindex 中查找是否有當前 key 版本的記錄。主要獲取 created 與 ver 的信息。
-
生成新的 KeyValue 信息。
-
更新 keyindex 記錄。
健康檢查
在註冊時,會初始化一個心跳週期 ttl 與租約週期 lease。服務器需要在心跳週期之內向 etcd 發送數據包,表示自己能夠正常工作。如果在規定的心跳週期內,etcd 沒有收到心跳包,則表示該服務器異常,etcd 會將該服務器對應的信息進行刪除。如果心跳包正常,但是服務器的租約週期結束,則需要重新申請新的租約,如果不申請,則 etcd 會刪除對應租約的所有信息。
在 etcd 中,並不是在磁盤中刪除對應的 keyValue 信息,而是對其進行標記刪除。
-
首先在 delete 中會生成一個 ibytes,對其追加標記,表示這個 revision 是 delete。
-
生成一個 KeyValue,該 KeyValue 只包含 Key 的信息。
-
同時修改 Tombstone 標誌位,結束當前生命週期,生成一個新的 generation,更新 kvindex。
再次需要做個說明,因爲筆者是從事 c++ 開發的,現在線上業務用的 zookeeper 來作爲註冊中心實現服務發現功能。上半年的時候,也曾想轉到 etcd 上,但是 etcd 對 c++ 並不友好,筆者用了將近兩週時間各種調研,編譯,發現竟然不能將其編譯成爲一個靜態庫...
需要特別說明的是,用的是 etcd 官網推薦的 c++ 客戶端 etcd-cpp-apiv3
縱使 etcd 功能再強大,不能支持 c++,算是一個不小的損失。對於筆者來說,算是個遺憾吧,希望後續能夠支持。
下面是 etcd c++ client 不支持靜態庫,作者以及其他使用者的反饋,以此作爲本章節的結束。
結語
微服務架構模式下,服務實例動態配置,因此服務消費者需要動態瞭解到服務提供者的變化,所以必須使用服務發現機制。
服務發現的關鍵部分是註冊中心。註冊中心提供註冊和查詢功能。目前業界開源的有 Netflix Eureka、Etcd、Consul 或 Apache Zookeeper,大家可以根據自己的需求進行選擇。
服務發現主要有兩種發現模式:客戶端發現和服務端發現。客戶端發現模式要求客戶端負責查詢註冊中心,獲取服務提供者的列表信息,使用負載均衡算法選擇一個合適的服務提供者,發送請求。服務端發現模式,客戶端每次都請求註冊中心,由註冊中心內部選擇一個合適的服務提供者,並將請求轉發至該服務提供者,需要注意的是 當一個請求過來的時候,註冊中心內部獲取服務提供者列表和使用負載均衡算法。
這個世界沒有完美的架構和模式,不同的場景都有適合的解決方案。我們在調研決策的時候,一定要根據實際情況去權衡對比,選擇最適合當前階段的方案,然後通過漸進迭代的方式不斷完善優化方案。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/5BJE1gQPDPy55xqA_ib7Fw