聊聊服務發(fā)現(xiàn)的推拉模型
前言
過去一年,我的工作重心投入到了 API 網(wǎng)關(阿里云 CSB)中,這對于我來說是一個新的領域,但和之前接觸的微服務治理方向又密不可分。API 網(wǎng)關適配微服務場景需要完成一些基礎能力的建設,其一便是對接注冊中心,從而作為微服務的入口流量,例如 Zuul、SpringCloud Gateway 都實現(xiàn)了這樣的功能。實際上很多開源網(wǎng)關在這一特性上均存在較大的局限性,本文暫不討論這些局限性,而是針對服務發(fā)現(xiàn)這一通用的場景,分享我對它的一些思考。
概念澄清
服務發(fā)現(xiàn)這個詞說實話還是有點抽象的,在微服務這一特定的領域具象化討論才有意義。“服務發(fā)現(xiàn)”指的是“服務消費者獲取服務提供者服務地址”的這一過程,而“服務”這一名詞在不同微服務框架中代指也可能有所不同,不過大多數(shù)都是代指的應用、接口等信息。
SpringCloud 以應用維度表示服務 Dubbo2.x 以接口維度表示服務;Dubbo3.x 以應用維度表示服務

服務從 Provider -> Registry -> Consumer 的這一流動過程便是本文重點討論的內(nèi)容。
數(shù)據(jù)傳遞的兩種方式:推模型和拉模型,一直是老生常談的話題,在服務發(fā)現(xiàn)中也不妨一談。先不要急著回答服務發(fā)現(xiàn)這一場景中,推拉到底誰好的問題,讓我們先看看一些微服務框架中的服務發(fā)現(xiàn)是如何實現(xiàn)的。
微服務框架中的服務發(fā)現(xiàn)
這一節(jié)以 Dubbo 和 SpringCloud 兩個微服務框架為引子,看看它們的服務發(fā)現(xiàn)到底使用的是拉模型還是推模型。
Dubbo 服務發(fā)現(xiàn)
public interface RegistryService {
void register(URL url);
void unregister(URL url);
/**
* Subscribe to eligible registered data and automatically push when the registered data is changed.
*/
void subscribe(URL url, NotifyListener listener);
void unsubscribe(URL url, NotifyListener listener);
List<URL> lookup(URL url);
}
Dubbo 管理服務發(fā)現(xiàn)的核心接口 RegistryService 直接給出了答案,通過 subscribe 和 notify 這些關鍵字便可以猜測到 Dubbo 使用的是推模型。

上圖是一個推模型的工作流程。
SpringCloud 服務發(fā)現(xiàn)
public interface DiscoveryClient extends Ordered {
int DEFAULT_ORDER = 0;
String description();
List<ServiceInstance> getInstances(String serviceId);
List<String> getServices();
default void probe() {
this.getServices();
}
default int getOrder() {
return 0;
}
}
DiscoveryClient 是 SpringCloud 中一個核心服務發(fā)現(xiàn)的接口,通過 getInstances 基本可以看出,SpringCloud 使用的是拉模型。

上圖是一個拉模型的工作流程。
盡管我們還沒有詳細領略到兩個模型背后的優(yōu)化和實現(xiàn)細節(jié),但從事實來看,Dubbo 和 SpringCloud 使用了不同的服務發(fā)現(xiàn)機制,都能讓微服務玩轉(zhuǎn)起來。
此時,如果你心里已經(jīng)有了 Dubbo 是推模型,SpringCloud 是拉模型的認知,不妨再繼續(xù)看下一節(jié),可能這樣的認知又會有了動搖。
注冊中心中的推拉
上一節(jié)站在微服務框架的角度,介紹了服務發(fā)現(xiàn)的推拉模型,這一節(jié)則是站在注冊中心的角度來分析。說到底,無論是 Dubbo 還是 SpringCloud,總得對接一款注冊中心才可以獲得服務發(fā)現(xiàn)的能力,可以是 Zookeeper,可以是 Nacos,可以是 Eureka,也可以是任意的其他提供了服務發(fā)現(xiàn)能力的組件。
我就先以 Nacos 為例介紹下它的推拉模型。先看 Nacos 的 Naming 模塊提供的核心接口:
public interface NamingService {
void registerInstance(String serviceName, String ip, int port) throws NacosException;
void deregisterInstance(String serviceName, String ip, int port) throws NacosException;
List<Instance> getAllInstances(String serviceName, boolean subscribe) throws NacosException;
List<Instance> selectInstances(String serviceName, String groupName, boolean healthy, boolean subscribe)
throws NacosException;
void subscribe(String serviceName, EventListener listener) throws NacosException;
void unsubscribe(String serviceName, List<String> clusters, EventListener listener) throws NacosException;
}
為了方便閱讀,我刪除了大部分重載的接口以及非核心的接口。可以發(fā)現(xiàn),從 API 角度,Nacos 是同時提供了推模型和拉模型兩套接口的,這樣也是方便其被微服務框架集成,有興趣的讀者,可以自行去閱讀下 Dubbo/SpringCloud Alibaba 集成 Nacos 的代碼,Dubbo 使用的便是 subscribe 這一套推模型的接口,SpringCloud Alibaba 則是使用的 selectInstances 這一套拉模型的接口。
那是否說"Nacos 是一個推拉模型結(jié)合的注冊中心"呢,不夠嚴謹。且看 getAllInstances,selectInstances 這兩個方法都有一個 subscribe 入?yún)ⅲ幌略创a探究一下
public List<Instance> selectInstances(String serviceName, String groupName, List<String> clusters, boolean healthy,
boolean subscribe) throws NacosException {
ServiceInfo serviceInfo;
if (subscribe) {
serviceInfo = hostReactor.getServiceInfo(NamingUtils.getGroupedName(serviceName, groupName),
StringUtils.join(clusters, ","));
} else {
serviceInfo = hostReactor
.getServiceInfoDirectlyFromServer(NamingUtils.getGroupedName(serviceName, groupName),
StringUtils.join(clusters, ","));
}
return selectInstances(serviceInfo, healthy);
}
可以發(fā)現(xiàn) subscribe 這個參數(shù)控制的是是否直接從注冊中心拉取服務,subscribe=false 時,實際是從 Nacos 自身維護的一塊本地緩存中獲取到的服務,大多數(shù)情況下,獲取服務使用的是 subscribe=true 的重載方法。所以,selectInstance 看起來是在拉服務,subscribe 看起來是在推服務,實際上 Nacos 內(nèi)核維護緩存的方式我們并未得知。
從上述 subscribe 的提示中,我們可以得出結(jié)論,我們并不能直接通過個別接口就得出該注冊中心使用的是推模型還是拉模型,究竟何種模型,還是要看 client 端是如何從 server 端加載/更新服務信息的。
那么,真實情況下,Nacos cleint 究竟是如何從 server 端獲取到服務列表的呢?也不賣關子了,直接給結(jié)論
在 Nacos 1.x 中,Nacos 采用的是定時拉 + udp 推送的機制。客戶端會啟動一個定時器,每 10s 拉取一次服務,確保服務端服務版本一致,為了解決 10s 間隔內(nèi)服務更新了,客戶端卻沒有及時收到通知的這一問題,Nacos 還在服務端服務更新時,觸發(fā)了一次 udp 推送。
在 Nacos 2.x 中,Nacos 采用的是服務端 tcp 推送的機制。客戶端啟動時會跟服務端建立一條 tcp 長連接,服務端服務變更后,會復用客戶端建立的這條連接進行數(shù)據(jù)推送。
所以在回答,Nacos 到底是推模型還是拉模型時,需要區(qū)分版本來回答。
結(jié)論:Nacos 1.x 是拉模型;Nacos 2.x 是推模型
不知道有沒有讀者好奇 Nacos 為什么這么設計,我簡單用一些 QA 快速解答一些可能的疑問:
Q:為什么 Nacos 1.x 使用了 udp 推送,卻把 Nacos 1.x 定義為拉模型?
A:Nacos 1.x 中 udp 推送主要是為了降低服務更新延時而設計的,并且在復雜網(wǎng)絡部署架構(gòu)中,例如 client 與 server 只能單向訪問,或者有 SLB 中間介質(zhì)時,udp 就會失效;且 udp 本身就是不穩(wěn)定的,Nacos 嘗試兩次失敗后就會放棄推送。所以主要還是在用拉模式來保障。
Q:為什么 Nacos 1.x 一開始不使用 Nacos 2.x 中的架構(gòu),使用 tcp 推送?
A:個人猜測是因為拉模型實現(xiàn)起來簡單,Nacos 2.x 才引入了 grpc 實現(xiàn)長連接
Q:為什么 Nacos 1.x 的服務發(fā)現(xiàn)使用的是短輪詢,不像配置中心那樣使用長輪詢?
A:在服務發(fā)現(xiàn)場景中,服務端比較在意內(nèi)存消耗,長輪詢雖然不會占用線程,但服務端依舊會 hold 住 request/response,造成不必要的內(nèi)存浪費。
一些常見注冊中心的推拉模型:
Zookeeper:推模型 Nacos 1.x:拉模型 Nacos 2.x:推模型 Eureka:拉模型
好,介紹完了注冊中心視角的服務發(fā)現(xiàn)推拉模型了,再回過頭來看一個問題:如果使用了 SpringCloud Alibaba + Nacos 2.1 版本,那它的服務發(fā)現(xiàn)就是走的哪種模型呢?
正確答案:在微服務框架視角,sca 走的是拉模型這種同步拉取服務的機制;在注冊中心視角,應用作為客戶端是使用的推模型在接收服務變更的推送。
那有一部分比較帥的人可能會有問了,到底什么模型比較好呢?哎,下面就容我對比下二者。
推拉對比
實時性
服務推送的實時性是服務發(fā)現(xiàn)主要的 SLA 指標,其指的是當服務地址發(fā)生變化時,客戶端感知到變化的延遲。試想一下,服務端正在發(fā)布,IP 地址發(fā)生了變化,但是由于地址推送不及時,客戶端過了 10 分鐘還在調(diào)用舊的服務地址,這是多么可怕的一件事。
拉模型感知服務變化的延遲便是短輪詢間隔+拉取服務的耗時,在 Nacos 中,SLA 為 10s。
推模型感知服務變化的延遲則為服務端推送服務的耗時,在 Nacos 中,SLA 為 1s。
在實時性上,推模型有較大優(yōu)勢。
壓力
拉模型和推模型都會對服務端造成壓力,但是二者的時機不同。
拉模型的壓力是固定的,取決于輪詢間隔。 推模型的壓力取決于服務變更的頻率。
用兩個場景來做對比:
常態(tài)化場景。日常運行時,服務列表一般不會變化,拉模型會導致不必要的開銷,對服務端造成較大壓力。 快上快下場景。機器快速擴容或者縮容,導致服務地址頻繁變更,推送量會瞬時變大,對服務端和客戶端造成較大壓力。
針對快上快下場景,也可以進行一系列的優(yōu)化,例如推送合并,增量推送,數(shù)據(jù)分離等等,目前 Nacos 支持了推送合并這一優(yōu)化。
代碼復雜度
碼齡越大,越發(fā)意識到代碼復雜度是一個非常重要的技術選型指標,越簡單的代碼越容易維護,也具備持久的生命力,架構(gòu)師需要在代碼復雜度和性能的 trade off 中,找到一個平衡點,不得不承認,推模型的復雜度往往要比拉模型高出很多,例如多出了長連接的狀態(tài)管理這一環(huán)節(jié)。
拉模型完全勝出。
總結(jié)
這篇文章不希望大家陷入到字眼中,判斷某一個框架或者工具是推或者拉模型,而是希望能介紹清楚服務發(fā)現(xiàn)中推拉模型的工作流程,方便大家對這些微服務框架也好,注冊中心也好,有一個更深的理解。
總結(jié)一下,主流的微服務框架和注冊中心的服務發(fā)現(xiàn)機制中,推模型和拉模型均有使用,具體如何選擇,如何優(yōu)化,可以根據(jù)自身服務的特點,以及服務的規(guī)模去選擇使用。
在我負責的 API 網(wǎng)關(阿里云 CSB)中,采用了一套獨立的服務發(fā)現(xiàn)機制,同時支持拉模型和推模型,以適配部分僅支持推模型或者僅支持拉模型的注冊中心。
