Dubbo 動態配置中心
一、參考文檔
http://dubbo.apache.org/zh-cn/docs/user/configuration/config-center.html
三大中心指的:註冊中心,元數據中心,配置中心。
在 2.7 之前的版本,Dubbo 只配備了註冊中心,主流使用的註冊中心爲 zookeeper。新增加了元數據中心和配置中心,自然是爲了解決對應的痛點,下面我們來詳細闡釋三大中心改造的原因。
元數據改造
元數據是什麼?元數據定義爲描述數據的數據,在服務治理中,例如服務接口名,重試次數,版本號等等都可以理解爲元數據。在 2.7 之前,元數據一股腦丟在了註冊中心之中,這造成了一系列的問題:
推送量大 -> 存儲數據量大 -> 網絡傳輸量大 -> 延遲嚴重
生產者端註冊 30+ 參數,有接近一半是不需要作爲註冊中心進行傳遞;消費者端註冊 25+ 參數,只有個別需要傳遞給註冊中心。有了以上的理論分析,Dubbo 2.7 進行了大刀闊斧的改動,只將真正屬於服務治理的數據發佈到註冊中心之中,大大降低了註冊中心的負荷。
同時,將全量的元數據發佈到另外的組件中:元數據中心。元數據中心目前支持 redis(推薦),zookeeper。這也爲 Dubbo 2.7 全新的 Dubbo Admin 做了準備,關於新版的 Dubbo Admin,我將會後續準備一篇獨立的文章進行介紹。
示例:使用 zookeeper 作爲元數據中心
<dubbo:metadata-report address=“zookeeper://127.0.0.1:2181”/>
Dubbo 2.6 元數據
dubbo://30.5.120.185:20880/com.alibaba.dubbo.demo.DemoService?
anyhost=true&
application=demo-provider&
interface=com.alibaba.dubbo.demo.DemoService&
methods=sayHello&
bean.name=com.alibaba.dubbo.demo.DemoService&
dubbo=2.0.2&
executes=4500&
generic=false&
owner=kirito&
pid=84228&
retries=7&
side=provider&
timestamp=1552965771067
從本地的 zookeeper 中取出一條服務數據,通過解碼之後,可以看出,的確有很多參數是不必要。
Dubbo 2.7 元數據
在 2.7 中,如果不進行額外的配置,zookeeper 中的數據格式仍然會和 Dubbo 2.6 保持一致,這主要是爲了保證兼容性,讓 Dubbo 2.6 的客戶端可以調用 Dubbo 2.7 的服務端。如果整體遷移到 2.7,則可以爲註冊中心開啓簡化配置的參數:
<dubbo:registry address=“zookeeper://127.0.0.1:2181” simplified=“true”/>
Dubbo 將會只上傳那些必要的服務治理數據,一個簡化過後的數據如下所示:
dubbo://30.5.120.185:20880/org.apache.dubbo.demo.api.DemoService?
application=demo-provider&
dubbo=2.0.2&
release=2.7.0&
timestamp=1552975501873
對於那些非必要的服務信息,仍然全量存儲在元數據中心之中:
元數據中心的數據可以被用於服務測試,服務 MOCK 等功能。目前註冊中心配置中 simplified 的默認值爲 false,因爲考慮到了遷移的兼容問題,在後續迭代中,默認值將會改爲 true。
配置中心支持
衡量配置中心的必要性往往從三個角度出發:
- 分佈式配置統一管理
- 動態變更推送
- 安全性
Spring Cloud Config, Apollo, Nacos 等分佈式配置中心組件都對上述功能有不同程度的支持。在 2.7 之前的版本中,在 zookeeper 中設置了部分節點:configurators,routers,用於管理部分配置和路由信息,它們可以理解爲 Dubbo 配置中心的雛形。在 2.7 中,Dubbo 正式支持了配置中心,目前支持的幾種註冊中心 Zookeeper,Apollo,Nacos(2.7.1-release 支持)。
在 Dubbo 中,配置中心主要承擔了兩個作用
- 外部化配置。啓動配置的集中式存儲
- 服務治理。服務治理規則的存儲與通知
示例:使用 Zookeeper 作爲配置中心
<dubbo:config-center address=“zookeeper://127.0.0.1:2181”/>
引入配置中心後,需要注意配置項的覆蓋問題。
二、動態配置中心
何爲配置中心?配置中心即就是我們平常經常見的比如註冊中心的地址,服務的版本,服務的分組,反正在dubbo中能夠看到的一些信息都可以作爲配置中心中去存儲。官方說的這兩點比較的明確。就是一些外部化的參數配置,其實這個在其他的配置中心組件中非常常見,比如diamond 從remote獲取配置信息,然後覆蓋本地的信息。
1、直接下載官方demo工程
git clone https://github.com/apache/dubbo-samples.git
dubbo-samples-zookeeper
spring/dubbo-provider.properties
將本地的配置修改爲配置中心的地址
dubbo.application.name=zookeeper-demo-provider
dubbo.config-center.address=zookeeper://127.0.0.1:2181
#dubbo.registry.address=zookeeper://${zookeeper.address:localhost}:2181
#dubbo.protocol.name=dubbo
#dubbo.protocol.port=20880
#dubbo.application.qosEnable=true
#dubbo.application.qosPort=33333
dubbo.application.qosAcceptForeignIp=false
2、配置管理
Dubbo admin 配置中
將一些基礎配置信息配置進去
dubbo.registry.address=zookeeper://127.0.0.1:2181
dubbo.metadata-report.address=zookeeper://127.0.0.1:2181
dubbo.protocol.name=dubbo
dubbo.protocol.port=20880
dubbo.application.qosEnable=true
dubbo.application.qosPort=33333
dubbo.application.qosAcceptForeignIp=false
3、查看 zookeeper
4、啓動demo服務即可。
發現demo 發現日誌監聽配置的變化
12/07/19 10:35:23:023 CST] main-EventThread INFO state.ConnectionStateManager: State change: CONNECTED
[12/07/19 10:35:23:023 CST] ZookeeperDynamicConfiguration-thread-1 INFO curator.CuratorZookeeperClient: [DUBBO] listen the zookeeper changed. The changed data:ChildData{path='/dubbo/config', stat=427,427,1562936763476,1562936763476,0,1,0,0,0,1,428
, data=[]}, dubbo version: 2.7.2, current host: 192.168.199.112
[12/07/19 10:35:23:023 CST] ZookeeperDynamicConfiguration-thread-1 INFO curator.CuratorZookeeperClient: [DUBBO] listen the zookeeper changed. The changed data:ChildData{path='/dubbo/config/dubbo', stat=428,428,1562936763478,1562936763478,0,1,0,0,0,1,429
, data=[]}, dubbo version: 2.7.2, current host: 192.168.199.112
[12/07/19 10:35:23:023 CST] ZookeeperDynamicConfiguration-thread-1 INFO curator.CuratorZookeeperClient: [DUBBO] listen the zookeeper changed. The changed data:ChildData{path='/dubbo/config/dubbo/dubbo.properties', stat=429,465,1562936763479,1562940541881,4,0,0,0,267,0,429
, data=[100, 117, 98, 98, 111, 46, 114, 101, 103, 105, 115, 116, 114, 121, 46, 97, 100, 100, 114, 101, 115, 115, 61, 122, 111, 111, 107, 101, 101, 112, 101, 114, 58, 47, 47, 49, 50, 55, 46, 48, 46, 48, 46, 49, 58, 50, 49, 56, 49, 32, 10, 100, 117, 98, 98, 111, 46, 109, 101, 116, 97, 100, 97, 116, 97, 45, 114, 101, 112, 111, 114, 116, 46, 97, 100, 100, 114, 101, 115, 115, 61, 122, 111, 111, 107, 101, 101, 112, 101, 114, 58, 47, 47, 49, 50, 55, 46, 48, 46, 48, 46, 49, 58, 50, 49, 56, 49, 10, 100, 117, 98, 98, 111, 46, 112, 114, 111, 116, 111, 99, 111, 108, 46, 110, 97, 109, 101, 61, 100, 117, 98, 98, 111, 10, 100, 117, 98, 98, 111, 46, 112, 114, 111, 116, 111, 99, 111, 108, 46, 112, 111, 114, 116, 61, 50, 48, 56, 56, 48, 10, 100, 117, 98, 98, 111, 46, 97, 112, 112, 108, 105, 99, 97, 116, 105, 111, 110, 46, 113, 111, 115, 69, 110, 97, 98, 108, 101, 61, 116, 114, 117, 101, 10, 100, 117, 98, 98, 111, 46, 97, 112, 112, 108, 105, 99, 97, 116, 105, 111, 110, 46, 113, 111, 115, 80, 111, 114, 116, 61, 51, 51, 51, 51, 51, 10, 100, 117, 98, 98, 111, 46, 97, 112, 112, 108, 105, 99, 97, 116, 105, 111, 110, 46, 113, 111, 115, 65, 99, 99, 101, 112, 116, 70, 111, 114, 101, 105, 103, 110, 73, 112, 61, 102, 97, 108, 115, 101]}, dubbo version: 2.7.2, current host: 192.168.199.112
[12/07/19 10:35:23:023 CST] ZookeeperDynamicConfiguration-thread-1 INFO curator.CuratorZookeeperClient: [DUBBO] listen the zookeeper changed. The changed data:null, dubbo version: 2.7.2, current host: 192.168.199.112
全局的額陪孩子和官方文檔相同處理邏輯。
5、實現原理
三、源碼解析
1、服務導出
監聽到spring 啓動完畢,然後服務進行導出
org.apache.dubbo.config.spring.ServiceBean#onApplicationEvent
org.apache.dubbo.config.spring.ServiceBean#export
public void onApplicationEvent(ContextRefreshedEvent event) {
if (!isExported() && !isUnexported()) {
if (logger.isInfoEnabled()) {
logger.info("The service ready on spring started. service: " + getInterface());
}
export();
}
}
org.apache.dubbo.config.ServiceConfig#export
這裏是同步的進行導出服務,先檢查配置項信息。
public synchronized void export() {
checkAndUpdateSubConfigs();
if (!shouldExport()) {
return;
}
if (shouldDelay()) {
delayExportExecutor.schedule(this::doExport, getDelay(), TimeUnit.MILLISECONDS);
} else {
doExport();
}
}
2、獲取配置流程
org.apache.dubbo.config.ServiceConfig#checkAndUpdateSubConfigs
- 獲取本地的全局配置,比如應用、註冊中心、協議、配置中心地址等等
- 從配置中心獲取信息
public void checkAndUpdateSubConfigs() {
// Use default configs defined explicitly on global configs
completeCompoundConfigs();
// Config Center should always being started first.
startConfigCenter();
checkDefault();
checkProtocol();
checkApplication();
// if protocol is not injvm checkRegistry
if (!isOnlyInJvm()) {
checkRegistry();
}
this.refresh();
checkMetadataReport();
if (StringUtils.isEmpty(interfaceName)) {
throw new IllegalStateException("<dubbo:service interface=\"\" /> interface not allow null!");
}
if (ref instanceof GenericService) {
interfaceClass = GenericService.class;
if (StringUtils.isEmpty(generic)) {
generic = Boolean.TRUE.toString();
}
} else {
try {
interfaceClass = Class.forName(interfaceName, true, Thread.currentThread()
.getContextClassLoader());
} catch (ClassNotFoundException e) {
throw new IllegalStateException(e.getMessage(), e);
}
checkInterfaceAndMethods(interfaceClass, methods);
checkRef();
generic = Boolean.FALSE.toString();
}
if (local != null) {
if ("true".equals(local)) {
local = interfaceName + "Local";
}
Class<?> localClass;
try {
localClass = ClassUtils.forNameWithThreadContextClassLoader(local);
} catch (ClassNotFoundException e) {
throw new IllegalStateException(e.getMessage(), e);
}
if (!interfaceClass.isAssignableFrom(localClass)) {
throw new IllegalStateException("The local implementation class " + localClass.getName() + " not implement interface " + interfaceName);
}
}
if (stub != null) {
if ("true".equals(stub)) {
stub = interfaceName + "Stub";
}
Class<?> stubClass;
try {
stubClass = ClassUtils.forNameWithThreadContextClassLoader(stub);
} catch (ClassNotFoundException e) {
throw new IllegalStateException(e.getMessage(), e);
}
if (!interfaceClass.isAssignableFrom(stubClass)) {
throw new IllegalStateException("The stub implementation class " + stubClass.getName() + " not implement interface " + interfaceName);
}
}
checkStubAndLocal(interfaceClass);
checkMock(interfaceClass);
}
3、獲取全局配置信息
org.apache.dubbo.config.ServiceConfig#completeCompoundConfigs
private void completeCompoundConfigs() {
if (provider != null) {
if (application == null) {
setApplication(provider.getApplication());
}
if (module == null) {
setModule(provider.getModule());
}
if (registries == null) {
setRegistries(provider.getRegistries());
}
if (monitor == null) {
setMonitor(provider.getMonitor());
}
if (protocols == null) {
setProtocols(provider.getProtocols());
}
if (configCenter == null) {
setConfigCenter(provider.getConfigCenter());
}
}
if (module != null) {
if (registries == null) {
setRegistries(module.getRegistries());
}
if (monitor == null) {
setMonitor(module.getMonitor());
}
}
if (application != null) {
if (registries == null) {
setRegistries(application.getRegistries());
}
if (monitor == null) {
setMonitor(application.getMonitor());
}
}
3、獲取配置中心
org.apache.dubbo.config.AbstractInterfaceConfig#startConfigCenter
環境信息準備是重點。
void startConfigCenter() {
## 配置中心配置是否存在
if (configCenter == null) {
ConfigManager.getInstance().getConfigCenter().ifPresent(cc -> this.configCenter = cc);
}
if (this.configCenter != null) {
// TODO there may have duplicate refresh
this.configCenter.refresh();
## 準備獲環境信息
prepareEnvironment();
}
ConfigManager.getInstance().refreshAll();
}
4、環境信息準備
org.apache.dubbo.config.AbstractInterfaceConfig#prepareEnvironment
動態獲取配置中心的實現,然後獲取到全局配置內容,獲取應用配置內容,然後刷新到本地的環境變量中去。
private void prepareEnvironment() {
if (configCenter.isValid()) {
if (!configCenter.checkOrUpdateInited()) {
return;
}
## 獲取配置中心實現類
DynamicConfiguration dynamicConfiguration = getDynamicConfiguration(configCenter.toUrl());
## 獲取常量
String configContent = dynamicConfiguration.getConfigs(configCenter.getConfigFile(), configCenter.getGroup());
String appGroup = application != null ? application.getName() : null;
String appConfigContent = null;
if (StringUtils.isNotEmpty(appGroup)) {
appConfigContent = dynamicConfiguration.getConfigs
(StringUtils.isNotEmpty(configCenter.getAppConfigFile()) ? configCenter.getAppConfigFile() : configCenter.getConfigFile(),
appGroup
);
}
try {
Environment.getInstance().setConfigCenterFirst(configCenter.isHighestPriority());
Environment.getInstance().updateExternalConfigurationMap(parseProperties(configContent));
Environment.getInstance().updateAppExternalConfigurationMap(parseProperties(appConfigContent));
} catch (IOException e) {
throw new IllegalStateException("Failed to parse configurations from Config Center.", e);
}
}
}
5、SPI 動態獲取配置工廠
org.apache.dubbo.config.AbstractInterfaceConfig#getDynamicConfiguration
private DynamicConfiguration getDynamicConfiguration(URL url) {
DynamicConfigurationFactory factories = ExtensionLoader
.getExtensionLoader(DynamicConfigurationFactory.class)
.getExtension(url.getProtocol());
DynamicConfiguration configuration = factories.getDynamicConfiguration(url);
Environment.getInstance().setDynamicConfiguration(configuration);
return configuration;
}
6、ZookeeperDynamicConfigurationFactory 工廠的實現
org.apache.dubbo.configcenter.support.zookeeper.ZookeeperDynamicConfigurationFactory
給予了註冊的地址
public class ZookeeperDynamicConfigurationFactory extends AbstractDynamicConfigurationFactory {
private ZookeeperTransporter zookeeperTransporter;
public void setZookeeperTransporter(ZookeeperTransporter zookeeperTransporter) {
this.zookeeperTransporter = zookeeperTransporter;
}
@Override
protected DynamicConfiguration createDynamicConfiguration(URL url) {
return new ZookeeperDynamicConfiguration(url, zookeeperTransporter);
}
}
7、 ZookeeperDynamicConfiguration連接
org.apache.dubbo.configcenter.support.zookeeper.ZookeeperDynamicConfiguration#ZookeeperDynamicConfiguration
這裏監聽的路徑啓動
ZookeeperDynamicConfiguration(URL url, ZookeeperTransporter zookeeperTransporter) {
this.url = url;
rootPath = "/" + url.getParameter(CONFIG_NAMESPACE_KEY, DEFAULT_GROUP) + "/config";
initializedLatch = new CountDownLatch(1);
this.cacheListener = new CacheListener(rootPath, initializedLatch);
this.executor = Executors.newFixedThreadPool(1, new NamedThreadFactory(this.getClass().getSimpleName(), true));
zkClient = zookeeperTransporter.connect(url);
zkClient.addDataListener(rootPath, cacheListener, executor);
try {
// Wait for connection
this.initializedLatch.await();
} catch (InterruptedException e) {
logger.warn("Failed to build local cache for config center (zookeeper)." + url);
}
}
8、環境信息準備–>獲取到配置信息
org.apache.dubbo.config.AbstractInterfaceConfig#prepareEnvironment
配置文件、分組都是URL中的參數
private void prepareEnvironment() {
if (configCenter.isValid()) {
if (!configCenter.checkOrUpdateInited()) {
return;
}
DynamicConfiguration dynamicConfiguration = getDynamicConfiguration(configCenter.toUrl());
String configContent = dynamicConfiguration.getConfigs(configCenter.getConfigFile(), configCenter.getGroup());
String appGroup = application != null ? application.getName() : null;
String appConfigContent = null;
if (StringUtils.isNotEmpty(appGroup)) {
appConfigContent = dynamicConfiguration.getConfigs
(StringUtils.isNotEmpty(configCenter.getAppConfigFile()) ? configCenter.getAppConfigFile() : configCenter.getConfigFile(),
appGroup
);
}
try {
Environment.getInstance().setConfigCenterFirst(configCenter.isHighestPriority());
Environment.getInstance().updateExternalConfigurationMap(parseProperties(configContent));
Environment.getInstance().updateAppExternalConfigurationMap(parseProperties(appConfigContent));
} catch (IOException e) {
throw new IllegalStateException("Failed to parse configurations from Config Center.", e);
}
}
}
9 讀取配置文件
org.apache.dubbo.common.config.ConfigurationUtils#parseProperties
public static Map<String, String> parseProperties(String content) throws IOException {
Map<String, String> map = new HashMap<>();
if (StringUtils.isEmpty(content)) {
logger.warn("You specified the config centre, but there's not even one single config item in it.");
} else {
Properties properties = new Properties();
properties.load(new StringReader(content));
properties.stringPropertyNames().forEach(
k -> map.put(k, properties.getProperty(k))
);
}
return map;
}
整個流程結束.
四、總結
整個源碼的分析可以看出,整個流程鏈路比較的清爽、功能劃分比較清楚,什麼時候該幹什麼,學習了一波。