微信公眾號:九點半的馬拉
路途雖遙遠,將來更美好
學海無涯,大家一起加油!
Dubbo是一款很優(yōu)秀的RPC框架,目前Github的Star數(shù)已經(jīng)達到34.6k,有效的反映出它的受歡迎程度。Dubbo提供高性能的基于代理的遠程調(diào)用能力,服務(wù)以接口為粒度,為開發(fā)者屏蔽遠程調(diào)用底層細節(jié)。Dubbo設(shè)計的穩(wěn)定架構(gòu)為數(shù)萬服務(wù)的穩(wěn)定運行提供了堅實的基礎(chǔ)。
Dubbo的傳統(tǒng)架構(gòu)
對于傳統(tǒng)架構(gòu),Dubbo主要可以分為3個組件:Consumer、Provider和Registry,Monitor組件不是較為重要的組件,主要負責服務(wù)相關(guān)數(shù)據(jù)的統(tǒng)計與查詢,可以忽略掉。

Provider為服務(wù)提供者,在啟動時會根據(jù)設(shè)置的協(xié)議(如:Dubbo協(xié)議),將服務(wù)進行服務(wù)暴露,生成對應(yīng)的Invoker(AbstractProxyInvoker),并存儲在HashMap中,key為端口、接口名、接口版本和接口分組組成的字符串。最后將服務(wù)元數(shù)據(jù)信息注冊到注冊中心。
Consumer為服務(wù)消費者,從注冊中心訂閱要引用的服務(wù)元數(shù)據(jù)信息,封裝成DubboInvoker,最后通過動態(tài)代理轉(zhuǎn)換成一個代理對象。
在服務(wù)消費時,它會通過上述的代理對象進行調(diào)用,在調(diào)用之前會通過Directory獲取所有可以調(diào)用的遠程服務(wù)Invoker列表,然后通過負載均衡策略選擇出一個進行調(diào)用,在具體的調(diào)用之前,它會經(jīng)過一系列的Filter調(diào)用鏈,可以進行處理上下文,限流,回聲檢測,超時日志打印等。
傳統(tǒng)結(jié)構(gòu)的不足
從上面的分析中我們可以看出,傳統(tǒng)的Dubbo架構(gòu)過多依賴注冊中心,當注冊的服務(wù)元數(shù)據(jù)信息發(fā)生變化時,Consumer通過訂閱感知信息變化后,會從注冊中心重新拉取信息,如果變化頻繁,對其網(wǎng)絡(luò)也造成了一定的壓力。同時,注冊中心存儲的是接口級別的服務(wù)信息,容易造成數(shù)據(jù)存儲容量驟增,較多的信息存在冗余。
現(xiàn)在云原生技術(shù)興起,Spring Cloud等提供了面向應(yīng)用的服務(wù)注冊與發(fā)現(xiàn)。從Dubbo2.7.5開始,Dubbo開始提出一種服務(wù)自省的概念,開始提供面向應(yīng)用級別的服務(wù)注冊與發(fā)現(xiàn)。此時,服務(wù)的維度從接口級別降到了應(yīng)用級別,注冊中心存儲應(yīng)用級別的信息,數(shù)據(jù)容量大幅度減少。

假設(shè)定義了兩個服務(wù)DemoService和RoadService,傳統(tǒng)架構(gòu)下的節(jié)點信息和服務(wù)自省下的節(jié)點信息如下所示(選用Zookeeper作為注冊中心)

上圖是傳統(tǒng)架構(gòu)下的注冊中心節(jié)點。它主要是一種樹形結(jié)構(gòu),該結(jié)構(gòu)分為四層。
/dubbo作為服務(wù)信息的根節(jié)點
org.apache.dubbo.demo.DemoService和org.apache.dubbo.demo.RoadService作為第二級節(jié)點,表示服務(wù)的全限定接口名,
comsumers、configcurators、providers和routors為第三級節(jié)點,其中:
consumers下的子節(jié)點表示多個消費者URL元數(shù)據(jù)信息;
configcurators下的子節(jié)點包含多個用于提供者動態(tài)配置URL元數(shù)據(jù)信息;
providers下的子節(jié)點包含多個服務(wù)提供者URL元數(shù)據(jù)信息
routors下的子節(jié)點包含用于多個消費者路由策略URL元數(shù)據(jù)信息。

上圖是服務(wù)自省機制下的注冊中心節(jié)點信息。(注意,為了方便,我們將Zookeeper也作為配置中心使用)
在最外層節(jié)點中,有兩個重要的節(jié)點dubbo和service
對于service節(jié)點
它主要存儲的不同應(yīng)用的元數(shù)據(jù)信息。比如,dubbo-zookeeper-service-introspection-provider-sample表示一個應(yīng)用名,它的子節(jié)點名是該應(yīng)用的ip地址,節(jié)點值存儲的是有關(guān)元數(shù)據(jù)信息。
對于dubbo節(jié)點
最重要的是config節(jié)點,該節(jié)點是配置中心的節(jié)點,下面的子節(jié)點org.apache.dubbo.spring.boot.sample.consumer.DemoService和org.apache.dubbo.spring.boot.sample.consumer.RoadServce表示Dubbo服務(wù)接口名,下面的子節(jié)點表示對應(yīng)的提供對應(yīng)服務(wù)的應(yīng)用名。這樣就做到了Dubbo服務(wù)接口與應(yīng)用的映射。
服務(wù)自省在服務(wù)端和消費端都有體現(xiàn),出于篇幅的考慮,將服務(wù)自省拆分為服務(wù)端和消費端,各自用部分篇幅介紹。
服務(wù)端的服務(wù)自省部分
DubboBootstrap主要處理dubbo所有的配置信息,當調(diào)用start方法時,表示dubbo進行啟動,服務(wù)暴露和服務(wù)自省等均在該方法中執(zhí)行。
執(zhí)行時機
我們可以使用注解@EnableDubbo開啟Dubbo,其中包含了@DubboComponentScan注解。
通過@DubboComponentScan引入類DubboComponentScanRegistrar,后續(xù)創(chuàng)建了ServiceAnnotationBeanPostProcessor后置處理器,它實現(xiàn)了BeanDefinitionRegistryPostProcessor接口,Spring容器中的所有Bean注冊之后會回調(diào)postProcessBeanDefinitionRegistry方法,將@Service注解的服務(wù)BeanDefinition注冊到spring容器中,Spring容器啟動完成后會發(fā)布ContextRefreshEvent事件,DubboBootstrapApplicationListener類會監(jiān)聽到該事件,之后調(diào)用dubboBootstrap.start方法,此時開啟了Dubbo相關(guān)邏輯。

我們對其中的幾個重要方法進行解釋。
1. exportServices方法
在exportServices方法中進行服務(wù)暴露,這是一個很重要的額工作。
我們對定義的一個ServiceBean變量進行解釋,即上面提到的DemoService。比如:
beanName : ServiceBean:org.apache.dubbo.spring.boot.sample.consumer.DemoService:1.0.0
interfaceName : org.apache.dubbo.spring.boot.sample.consumer.DemoService
ref : 實現(xiàn)該接口的具體類
id: ServiceBean:org.apache.dubbo.spring.boot.sample.consumer.DemoService:1.0.0


獲取到具體的ServiceConfig對象后,開始執(zhí)行它的export方法,進行服務(wù)暴露。
在具體的服務(wù)暴露之前,會更新一些相關(guān)的serviceMetadata,然后調(diào)用doExportUrls方法進行暴露。

2. doExportUrls方法
2.1)獲取ServiceRepository對象,(該對象記錄發(fā)布的服務(wù)信息,客戶端需要訪問的服務(wù)),從中獲取 ServiceDescriptor對象存儲暴露的服務(wù)
public class ApplicationModel {
private static final ExtensionLoader<FrameworkExt> LOADER = ExtensionLoader.getExtensionLoader(FrameworkExt.class);
public static ServiceRepository getServiceRepository() {
return (ServiceRepository)LOADER.getExtension("repository");
}
}
public ServiceDescriptor registerService(Class<?> interfaceClazz) {
return (ServiceDescriptor)this.services.computeIfAbsent(interfaceClazz.getName(), (_k) -> {
return new ServiceDescriptor(interfaceClazz);
});
}

2.2)在ServiceRepository注冊服務(wù)提供者的詳細信息

在ServiceRepository中有一個ConcurrentHashMap,key為服務(wù)接口名+":"+group+":"+":"+version組成的,value為ProviderModel,主要包含接口的實現(xiàn)實例,上述的serviceDescriptor,serviceConfig和seviceMetadata
2.3)獲取當前服務(wù)對應(yīng)的注冊中心實例,本案例中只設(shè)置了Zookeeper,其中最重要的一點是會判斷是否開啟了服務(wù)自省,最終的URL的協(xié)議頭是不一樣的,在開啟時,添加一個變量service-discovery-registry=service
private static String extractRegistryType(URL url) {
return UrlUtils.isServiceDiscoveryRegistryType(url) ? "service-discovery-registry" : "registry";
}
public static boolean isServiceDiscoveryRegistryType(Map<String, String> parameters) {
return parameters != null && !parameters.isEmpty() ? "service".equals(parameters.get("registry-type")) : false;
}
開啟了服務(wù)自省后,對應(yīng)的URL樣例:
service-discovery-registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=dubbo-zookeeper-service-introspection-provider-sample&dubbo=2.0.2&metadata-type=composite&pid=14508&qos.enable=false®istry=zookeeper®istry-type=service&release=2.7.8×tamp=1612096358401
如果是原先普通的話,開頭 的協(xié)議應(yīng)該為registry
2.4)獲取ProtocolConfig,調(diào)用doExportUrlsFor1Protocol方法進行暴露。

2.5)獲取元數(shù)據(jù)注冊中心
2.6)封裝URL進行本地服務(wù)暴露,通過動態(tài)代理轉(zhuǎn)換成Invoker,這幾步和之前的相同
2.7)調(diào)用protocol.export方法進行服務(wù)暴露,開啟了服務(wù)自省,協(xié)議為service-discovery-registry
具體調(diào)用RegistryProtocol.export方法,里面有一步要將服務(wù)信息注冊到注冊中心,這里有一些區(qū)別。
服務(wù)自省是獲取ServiceDiscoveryRegistry類,而普通的是ZookeeperRegistry
在這里主要簡單講下服務(wù)自省下的服務(wù)信息注冊。

2.8)根據(jù)serviceKey找到對應(yīng)的ProviderModel,將url添加進去
2.9)獲取WritableMetadataService,默認是(InMemoryWritableMetadataService),調(diào)用publishServiceDefinition方法,將url中的pid,timestamp,bind.ip,bind.port等參數(shù)字段移除掉,讀取side字段,判斷是provider端還是consumer端,根據(jù)url生成ServiceDefinition,并生成json字符串,并存放到InMemoryWritableMetadataService的serviceDefinitions(ConcurrentSkipListMap)中。
2.10)發(fā)布ServiceConfigExportedEvent事件和ServiceBeanExportedEvent事件
在上面介紹服務(wù)自省下的Zookeeper樹型圖時存在一個/dubbo/mapping/接口名/應(yīng)用名的路徑配置,進行接口名和應(yīng)用名的映射,那它是在什么時候產(chǎn)生的呢?
這時就要借助上述的ServiceConfigExportedEvent事件。
ServiceNameMappingListener監(jiān)聽該事件,調(diào)用onEvent方法

然后調(diào)用DynamicConfigurationServiceNameMapping的map方法。
其中,MetadataService服務(wù)信息不發(fā)布到配置中心,該類在服務(wù)自省中發(fā)揮著重要的作用,具體的解釋在以后的篇幅解釋。
private static final List<String> IGNORED_SERVICE_INTERFACES = Arrays.asList(MetadataService.class.getName());
public void map(URL exportedURL) {
String serviceInterface = exportedURL.getServiceInterface();
// MetadataService服務(wù)信息不發(fā)布到配置中心
if (!IGNORED_SERVICE_INTERFACES.contains(serviceInterface)) {
String group = exportedURL.getParameter("group");
String version = exportedURL.getParameter("version");
String protocol = exportedURL.getProtocol();
String key = ApplicationModel.getName();
String content = String.valueOf(System.currentTimeMillis());
this.execute(() -> {
// 將服務(wù)信息發(fā)布到配置中心,可能有多個配置中心 DynamicConfiguration.getDynamicConfiguration().publishConfig(key, buildGroup(serviceInterface, group, version, protocol), content);
}
});
}
}
上述基本為服務(wù)暴露的過程,繼續(xù)講解Bootstrap.start()方法的接下來的步驟。
4.registerServiceInstance方法

當開啟服務(wù)自省后,MetadataService中會存儲暴露的url信息,
此時,會調(diào)用內(nèi)部的exportMetadataService()和registerServiceInstance()方法。
在exportMetadataService方法中,會暴露相關(guān)的metadataServiceExporter,主要有兩個,即ConfigurableMetadataServiceExporter和RemoteMetadataServiceExporter。
在樣例的配置信息設(shè)置了dubbo.application.metadata-type=composite信息,默認值為local,
當為local時,暴露ConfigurableMetadataServiceExporter類,具體的暴露過程與上述的暴露過程一致,具體的暴露方法如下所示:

當為composite時,除了暴露上面的exporter,另外也暴露RemoteMetadataServiceExporter類,
在暴露該類時,不會將信息注冊到配置中心中,這是與普通的服務(wù)的重要區(qū)別之一。
之后,執(zhí)行registerServiceInstance方法。
在上述的Zookeeper樹形圖中存在一個類似/service/應(yīng)用名/host:port的路徑,通過該路徑,我們可以把應(yīng)用相關(guān)的信息記錄下來,進而可以面向應(yīng)用調(diào)用,而不是往常的面向接口調(diào)用,存儲容量大大降低。
在該方法中主要是獲取服務(wù)端發(fā)布的任一一個服務(wù)URL,從中提取出host和port,然后與應(yīng)用名等信息創(chuàng)建一個ServiceInstance,具體結(jié)構(gòu)如下所示:

然后遍歷具體的ServiceDiscovery.register方法,樣例配置的是ZookeeperServiceDiscovery,將ServiceInstace信息注冊到上述路徑中。
至此,在服務(wù)端的啟動部分就介紹結(jié)束了,在另一篇中,將會介紹在消費端的服務(wù)自省部分。