轉載地址:https://my.oschina.net/bieber/blog/378738
要說這個話題之前先講講之所以要做這個的需求。一般選擇DUBBO來進行服務管理,都是在分佈式應用的前提下,涉及到多個子系統之間的調用,DUBBO所做的事情就是維護各個子系統暴露的接口和自動發現對應接口的遠程地址從而實現分佈式RPC服務管理。
有了上面前提之後,那麼在項目開發和測試過程中涉及到一個問題,就是接口的聯調。如果每個子系統自己維護自己系統的聯調環境,那麼可能會導致別人調用接口的不穩定,因爲環境是系統自己人來維護,可能掛了也可能調整接口沒通知相關人員,這對開發接口聯調測試是一個問題。那麼如何做好這件事情呢?下面提出了STABLE環境的概念,一看字面意思就知道是一個穩定的環境,這個環境是和線上保持同步的,並且不是由開發負責維護,而是有專門的運維人負責維護,這樣STABLE環境就相對比較穩定,那麼調用這個環境的接口也就比較穩定了(你可能會問,怎麼調用這個環境的接口?在項目的dubbo.properties裏面把註冊中心指向STABLE環境即可)。那麼問題來了,STABLE不是開發維護,那麼會導致如果一個項目涉及多個子系統變更呢?要說明這個問題,我先來個圖先:
上面是整個STABLE環境的調用圖,不管是哪個項目,將涉及到改動的子系統遷移出來構造一個子環境,然後最後一個節點切入到stable環境中,這樣既保證了接口聯調的穩定性,也確保了各個項目的開發並行化。關於STABLE環境的介紹不是本篇的內容,所以不做過多的解釋。下面談談DUBBO怎麼隔離各個子環境的服務。
直連加不發佈服務
DUBBO的配置屬性裏面對消費端提供了不從註冊中心發現服務的機制,直接配置遠程接口的地址,這樣可以保證消費端連接到制定的環境接口。這樣消費端是解決了問題,但是服務提供端呢?如上圖的B1它即是消費端也是服務提供端,它提供A1所依賴的接口,那麼如果B1將它的服務發佈到註冊中心裏面(這裏需要提醒,STABLE環境機制裏面所有子環境公用一個註冊中心),那麼勢必會導致stable環境裏面的A會發現B1提供的服務?勢必會導致stable環境的不穩定(stable環境的機制是stable環境只能進不能出,就是不能調用外部其他子環境的服務)?所以B1不能發佈服務到註冊中心,dubbo也提供了相關的配置屬性來支持這一點。下面我例舉出通過哪些配置可以實現這種方案:
服務消費端:
DUBBO在消費端提供了一個url的屬性來指定某個服務端的地址
<!--lang:xml-->
<dubbo:reference interface="com.alibaba.dubbo.demo.HelloWorldService" check="false" id="helloWorldService"/>
默認的方式是從註冊中心發現接口爲com.alibaba.dubbo.demo.HelloWorldService
的服務,但是如果需要直連,可以在dubbo.properties下面配置dubbo.reference.helloWorldService.url=dubbo://ip:port/com.alibaba.dubbo.demo.HelloWorldService
可以通過配置dubbo.reference.url=dubbo://ip:port/
來讓某個消費者系統的服務都指向制定的服務器地址(關於配置信息可以參考《DUBBO配置規則詳解》)
服務提供端:
只需要在dubbo.properties裏面添加dubbo.registry.register=false
即表示當前系統的服務不發佈到註冊中心。
這種方式服務發佈和服務消費就和註冊中心沒一點關係了,個人感覺這是一個退步,我們用dubbo就是它的服務管理,而這種方案是直接將我們打入了原始社會。這樣也會導致如果一個項目設計的子系統很多,那麼搭建一個項目的子環境將會比較頭疼,因爲你要配置那些直連地址。
注意:這裏爲什麼一直通過配置在dubbo.properties文件中來達到目的?其實dubbo提供了多種配置的渠道(見《DUBBO配置規則詳解》)。因爲是爲了達到環境的隔離,最好不用爲了切換環境而調整源碼,這樣容易導致將調整的代碼發佈到產線,所以排除通過Spring的XML來配置。一般情況下dubbo.properties可以被定義爲是存放環境的配置,因爲不同的環境註冊中心地址不一樣,如果將這些地址信息配置在Spring裏面,難免會帶來失誤。所以建議dubbo.properties文件不要放在項目中,而是放在環境的容器裏面,通過容器來加載這個文件(比如JBOSS,可以將這個文件放在modules下面),這樣對代碼會比較穩定。
通過服務分組或者版本號來隔離
熟悉DUBBO的童鞋應該知道DUBBO對每個接口都支持分組和版本號,然後服務消費方指定調用哪個分組或者哪個版本號就可以調用對應的接口。那麼通過這個來描述一下怎麼通過它們來隔離。在談這些之前還是先上一個各個子系統和註冊中心的關係圖:
通過給每個子環境分配一個分組來實現各個子環境在一個組裏面,從而實現各個環境的隔離。具體操作如下:
服務消費方
<!--lang:xml-->
<dubbo:reference interface="com.alibaba.dubbo.demo.HelloWorldService" check="false" id="helloWorldService"/>
針對上面的接口只能調用指定的分組,可以在dubbo.properties中添加dubbo.reference.helloWorldService.group=test
,那麼該接口只會從test
分組中發現對應接口的服務了。也可以將所有服務都指向某個分組dubbo.reference.group=test
。
服務提供方
<!--lang:xml-->
<dubbo:service interface="com.alibaba.dubbo.demo.HelloWorldService" id="helloWorldRemote" ref="helloWorld"/>
針對上面的接口發佈到指定的分組,也是在dubbo.properties中添加dubbo.service.helloWorldRemote.group=test
,那麼該服務就發佈到了test
分組,同樣也可以將當前系統所有服務發佈到指定分組dubbo.service.group=test
。
而通過版本號也是類似的方案,只是配置的屬性不是group
而是version
,這裏就不贅述了。
這個方案看上去很好,不需要再配置直連的地址了,而是通過分組的方案來實現環境的隔離。但是如果你看過dubbo的官方文檔,你可能知道group
在dubbo的定義是服務接口有多種實現而進行分組的(version
也是類似),不是進行環境上面隔離的,所以雖然dubbo提供了這種功能,但是設計的目的不是做這種事情的,那麼就不能這麼硬拉過來,不然會導致團隊開發理解不一直出現問題。另外這種方案會導致註冊中心比較混亂,因爲註冊中心是所以環境公用的,那麼會導致一個註冊中心中存在多個環境的接口,也不便於維護。
說了這麼多,那麼有沒有一個比較合理的方案來實現環境的隔離呢?據我瞭解dubbo的原生並沒有提供,需要對dubbo進行小小的改造。下面談談這個小小的改造怎麼個改造法!
註冊中心分組實現隔離
細心的童鞋可能知道dubbo在配置註冊中心的時候有group
字段,可以通過dubbo.registry.group=test
來實現註冊中心的分組,但是這有個問題,如果配置了這個,那麼當前系統的服務發現和服務註冊都會到這個組裏面來進行,不能分別對服務發現和服務註冊單獨配置,也不能對某個接口進行配置。所以沿着這個想法,我對dubbo進行了小小的改造,在dubbo的服務發佈和服務消費添加了註冊中心分組的概念。既然要對註冊中心進行分組配置,那麼就需要了解怎麼將分組告訴註冊中心,以及分組在註冊中心是如何體現的,這裏我就以Zookeeper註冊中心爲例,看看它是怎麼實現的。
dubbo中zookeeper的註冊中心由ZookeeperRegistry
類實現的,看看它的構造函數就你就清楚了:
<!--lang:java-->
public ZookeeperRegistry(URL url, ZookeeperTransporter zookeeperTransporter) {
super(url);
if (url.isAnyHost()) {
throw new IllegalStateException("registry address == null");
}
String group = url.getParameter(Constants.GROUP_KEY, DEFAULT_ROOT);
if (! group.startsWith(Constants.PATH_SEPARATOR)) {
group = Constants.PATH_SEPARATOR + group;
}
this.root = group;
zkClient = zookeeperTransporter.connect(url);
zkClient.addStateListener(new StateListener() {
public void stateChanged(int state) {
if (state == RECONNECTED) {
try {
recover();
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
}
});
}
可以看到註冊中心接受的是一個URL
對象(dubbo內部和外部同學都是通過URL來實現的),並且從其中獲取group
參數,如果沒有則是默認的dubbo
。那麼你就不難理解爲什麼dubbo發佈到zookeeper的根節點是dubbo
了,這個其實是組名。那麼不同組服務,將會在zookeeper不同的根節點下面。
在談這些之前先看看dubbo中發佈服務,關聯遠程服務和註冊中心的關係。
上圖是服務引用和註冊中心關係圖,服務發佈也是類似,他發佈服務的時候會向制定的註冊中心發佈服務。基於上面我在這兩個類中添加了一個registryGroup
屬性,由於ReferenceBean
和ServiceBean
都繼承了AbstractInterfaceConfig
抽象類,那麼在這個抽象類中加入字段registryGroup
那麼服務消費和服務發佈裏面都可以讀取到該字段,添加完該字段之後,就可以通過dubbo.properties文件配置該屬性,在《DUBBO配置規則詳解》有講過怎麼配置。我這裏列舉一下對於屬性registryGroup
怎麼來配置:
服務消費端:
在dubbo.properties文件中添加dubbo.reference.registry-group=test
那麼當前系統的所有服務應用都會從註冊中心的test
組中去發現服務,當然也可以通過dubbo.reference.beanId.registry-group=test
來指定某個服務從test
組中發現服務。
服務提供端:
也是在dubbo.properties文件中添加類似的內容,只是將上面的reference
改成service
即可。
這裏是配置,在服務發佈和服務發現都讀到這個配置之後,怎麼體現到註冊中心裏的分組中呢?因爲這裏畢竟不是直接配置註冊中心的分組(dubbo.registry.group
),所以需要調整一下dubbo的代碼來將這個屬性添加到服務發現和服務註冊的註冊中心中。這裏主要調整了三個類,其中一個是常量類中添加了一個常量。總共對dubbo的代碼修改不超過10行。 下面列舉一下我的代碼調整:
顯示添加一個常量,在Constants
中添加了public static final String REGISTRY_GROUP_KEY = "registry.group";
主要是爲了避免字符串硬編碼。
上面有說過AbstractInterfaceConfig
類,在該類中添加了一個字段private String registryGroup;
並且生成get/set
方法,好讓dubbo幫我們注入這個屬性(見《DUBBO配置規則詳解》)。
服務消費端代碼調整:
對ReferenceConfig
(ReferenceBean
的父類)的createProxy
方法進行了調整,這個方法入參的map
是ReferenceBean
的所有參數K/V對。對該方法下面一段進行了調整:
<!--lang:java-->
else { // 通過註冊中心配置拼裝URL
List<URL> us = loadRegistries(false);
if (us != null && us.size() > 0) {
for (URL u : us) {
URL monitorUrl = loadMonitor(u);
if (monitorUrl != null) {
map.put(Constants.MONITOR_KEY, URL.encode(monitorUrl.toFullString()));
}
u=u.addParameterAndEncoded(Constants.REFER_KEY, StringUtils.toQueryString(map));
if(map.containsKey(Constants.REGISTRY_GROUP_KEY)){
u=u.addParameter(Constants.GROUP_KEY,map.get(Constants.REGISTRY_GROUP_KEY));
}
urls.add(u);
}
}
if (urls == null || urls.size() == 0) {
throw new IllegalStateException("No such any registry to reference " + interfaceName + " on the consumer " + NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion() + ", please config <dubbo:registry address=\"...\" /> to your spring config.");
}
}
就是判斷當前類有沒有配置registryGroup
,如果配置了添加到註冊中心的分組屬性中,那麼這個服務就會從這個分組的註冊中心去發現服務了。
服務提供方調整:
這部分是對AbstractInterfaceConfig
的方法loadRegistries
進行了調整,該方法是加載發佈服務的註冊中心URL,所以只需要在其返回的URL裏面添加group
參數即可。具體代碼如下:
<!--lang:java-->
protected List<URL> loadRegistries(boolean provider) {
checkRegistry();
List<URL> registryList = new ArrayList<URL>();
if (registries != null && registries.size() > 0) {
for (RegistryConfig config : registries) {
String address = config.getAddress();
if (address == null || address.length() == 0) {
address = Constants.ANYHOST_VALUE;
}
String sysaddress = System.getProperty("dubbo.registry.address");
if (sysaddress != null && sysaddress.length() > 0) {
address = sysaddress;
}
if (address != null && address.length() > 0
&& ! RegistryConfig.NO_AVAILABLE.equalsIgnoreCase(address)) {
Map<String, String> map = new HashMap<String, String>();
appendParameters(map, application);
appendParameters(map, config);
map.put("path", RegistryService.class.getName());
map.put("dubbo", Version.getVersion());
map.put(Constants.TIMESTAMP_KEY, String.valueOf(System.currentTimeMillis()));
if (ConfigUtils.getPid() > 0) {
map.put(Constants.PID_KEY, String.valueOf(ConfigUtils.getPid()));
}
if (! map.containsKey("protocol")) {
if (ExtensionLoader.getExtensionLoader(RegistryFactory.class).hasExtension("remote")) {
map.put("protocol", "remote");
} else {
map.put("protocol", "dubbo");
}
}
List<URL> urls = UrlUtils.parseURLs(address, map);
for (URL url : urls) {
url = url.addParameter(Constants.REGISTRY_KEY, url.getProtocol());
url = url.setProtocol(Constants.REGISTRY_PROTOCOL);
if ((provider && url.getParameter(Constants.REGISTER_KEY, true))
|| (! provider && url.getParameter(Constants.SUBSCRIBE_KEY, true))) {
if(!StringUtils.isEmpty(this.getRegistryGroup())){
url=url.addParameter(Constants.GROUP_KEY,this.getRegistryGroup());
}
registryList.add(url);
}
}
}
}
}
return registryList;
}
到此,關於這方案的介紹基本完畢。這種就可以使得每個環境在一個獨立的註冊中心的分組中,可以很好的維護,並且發佈服務不會凌亂,對服務的配置即可以全局設置,也可以對單個服務進行配置。基本上滿足了環境隔離的需要。