Disconf-client详细设计文档
=======
本文档主要阐述了版本 Disconf-Client 的设计。、
## 程序运行流程图 ##
### 版本2.0的设计 ###
![](http://ww3.sinaimg.cn/bmiddle/60c9620fjw1eqi7tnuic8j20l50g7acs.jpg)
[点击查看大图 ](http://ww3.sinaimg.cn/mw1024/60c9620fjw1eqi7tnuic8j20l50g7acs.jpg)
**运行流程详细介绍:**
- **启动事件A**:以下按顺序发生。
- A1:扫描静态注解类数据,并注入到配置仓库里。
- A2:根据仓库里的配置文件、配置项,到 disconf-web 平台里下载配置数据。
- A3:将下载得到的配置数据值注入到仓库里。
- A4:根据仓库里的配置文件、配置项,去ZK上监控结点。
- A5:根据XML配置定义,到 disconf-web 平台里下载配置文件,放在仓库里,并监控ZK结点。
- A6:A1-A5均是处理静态类数据。A6是处理动态类数据,包括:实例化配置的回调函数类;将配置的值注入到配置实体里。
- **更新配置事件B**:以下按顺序发生。
- B1:管理员在 Disconf-web 平台上更新配置。
- B2:Disconf-web 平台发送配置更新消息给ZK指定的结点。
- B3:ZK通知 Disconf-cient 模块。
- B4:与A2一样。唯一不同的是它只处理一个配置文件或者一个配置项,而事件A2则是处理所有配置文件和配置项。下同。
- B5:与A3一样。
- B6:基本与A4一样,区别是,这里还会将配置的新值注入到配置实体里。
### 完全版的设计 ###
![](http://ww3.sinaimg.cn/bmiddle/60c9620fjw1eqj81no7shj20l50h2q65.jpg)
[点击查看大图 ](http://ww3.sinaimg.cn/mw1024/60c9620fjw1eqj81no7shj20l50h2q65.jpg
**运行流程详细介绍:**
与2.0版本的主要区别是支持了:主备分配功能/主备切换事件。
- **启动事件A**:以下按顺序发生。
- A3:扫描静态注解类数据,并注入到配置仓库里。
- A4+A2:根据仓库里的配置文件、配置项,去 disconf-web 平台里下载配置数据。这里会有主备竞争
- A5:将下载得到的配置数据值注入到仓库里。
- A6:根据仓库里的配置文件、配置项,去ZK上监控结点。
- A7+A2:根据XML配置定义,到 disconf-web 平台里下载配置文件,放在仓库里,并监控ZK结点。这里会有主备竞争。
- A8:A1-A6均是处理静态类数据。A7是处理动态类数据,包括:实例化配置的回调函数类;将配置的值注入到配置实体里。
- **更新配置事件B**:以下按顺序发生。
- B1:管理员在 Disconf-web 平台上更新配置。
- B2:Disconf-web 平台发送配置更新消息给ZK指定的结点。
- B3:ZK通知 Disconf-cient 模块。
- B4:与A4一样。
- B5:与A5一样。
- B6:基本与A4一样,唯一的区别是,这里还会将配置的新值注入到配置实体里。
- **主备机切换事件C**:以下按顺序发生。
- C1:发生主机挂机事件。
- C2:ZK通知所有被影响到的备机。
- C4:与A2一样。
- C5:与A4一样。
- C6:与A5一样。
- C7:与A6一样。
## 类设计图 ##
![](http://ww4.sinaimg.cn/bmiddle/60c9620fgw1ej0ycv2fjbj21ao0u8441.jpg)
[查看大图](http://ww4.sinaimg.cn/mw1024/60c9620fgw1ej0ycv2fjbj21ao0u8441.jpg)
**Disconf-client包括的大模块有:**
- scan 配置扫描模块
- core 配置核心处理模块
- fetch 配置抓取模块
- watch 配置监控模块
- store 配置仓库模块
- addons 配置reload模块
**各个模块均采用以下设计模式来进设计:**
- 各个模块均以接口的方式对外暴露,松耦合,强内聚
- 各个模块均提供工厂类由其它模块来进行获取实例,实例的操纵方式均采用接口方式。
- 对于配置文件和配置项,采用类扩展的方法来避免if else判断。
## Disconf-client 的启动 ##
启动分成两步,由两个Bean来实现
这里 com.baidu.disconf.dem 是要扫描的类。
第一步由Bean com.baidu.disconf.client.DisconfMgrBean 来控制。第二步由 com.baidu.disconf.client.DisconfMgrBeanSecond 控制。
### 第一步:com.baidu.disconf.client.DisconfMgrBean ###
此Bean实现了BeanFactoryPostProcessor和PriorityOrdered接口。它的Bean初始化Order是最高优先级的。
因此,当Spring扫描了所有的Bean信息后,在所有Bean初始化(init)之前,DisconfMgrBean的postProcessBeanFactory方法将被调用,在这里,Disconf-Client会进行第一次扫描。
扫描按顺序做了以下几个事情:
1. 初始化Disconf-client自己的配置模块。
2. 初始化Scan模块。
3. 初始化Core模块,并极联初始化Watch,Fetcher,Restful模块。
4. 扫描用户类,整合分布式配置注解相关的静态类信息至配置仓库里。
5. 执行Core模块,从disconf-web平台上下载配置数据:配置文件下载到本地,配置项直接下载。
6. 配置文件和配置项的数据会注入到配置仓库里。
7. 使用watch模块为所有配置关联ZK上的结点。
其中对配置的处理详细为:
![](http://ww3.sinaimg.cn/bmiddle/60c9620fgw1ej1x5tfvzgj20pj141421.jpg)
[查看大图](http://ww3.sinaimg.cn/mw1024/60c9620fgw1ej1x5tfvzgj20pj141421.jpg)
### 第二步:com.baidu.disconf.client.DisconfMgrBeanSecond ###
DisconfMgrBean的扫描主要是静态数据的初始化,并未涉及到动态数据。DisconfMgrBeanSecond Bean则是将一些动态的数据写到仓库里。
本次扫描按顺序做了以下几个事情:
1. 将配置更新回调实例放到配置仓库里
2. 为配置实例注入值。
![](http://ww3.sinaimg.cn/bmiddle/60c9620fgw1ej1x5ve5w6j20pj11xwj4.jpg)
[查看大图](http://ww3.sinaimg.cn/mw1024/60c9620fgw1ej1x5ve5w6j20pj11xwj4.jpg)
## 分布式配置的实现 ##
下面将 分别详细阐述 分布式配置文件 和 分布式配置项 的实现方式。
由于目前版本只支持 Spring编程方式,因此,以下均只阐述Spring编程下的实现方式。
### 注解式实现 ###
#### 分布式配置文件的实现 ####
**定义分布式配置文件类**
对于配置文件,我们必须实现一个Java类来表示此 分布式配置文件。如:
package com.example.disconf.demo.config;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import com.baidu.disconf.client.common.annotations.DisconfFile;
import com.baidu.disconf.client.common.annotations.DisconfFileItem;
/**
* Redis配置文件
*
* @author liaoqiqi
* @version 2014-6-17
*/
@Service
@Scope("singleton")
@DisconfFile(filename = "redis.properties")
public class JedisConfig {
// 代表连接地址
private String host;
// 代表连接port
private int port;
/**
* 地址, 分布式文件配置
*
* @return
*/
@DisconfFileItem(name = "redis.host", associateField = "host")
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
/**
* 端口, 分布式文件配置
*
* @return
*/
@DisconfFileItem(name = "redis.port", associateField = "port")
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
}
对于此Java类,它必须是Spring托管的。此配置文件是redis.properties。
此配置类必须标注为 @DisconfFile,标识它是一个分布式配置文件。且必须指定文件名。
此配置类含有两个配置项,分别是host和port。这两个变量必须有 get 方法。且get方法名必须是符合JavaBean规范的。
我们通过在这两个变量的 get 方法上添加 @DisconfFileItem 注解来标注它是分布式配置文件里的配置项。必须指定name参数,表示配置文件里的KEY值。associateField值是可选的,表示此get方法相对应的域的名字。
**Disconf-client优先启动,并从平台上下载配置文件:**
应用程序启动时,当Spring容器扫描了所有Java Bean却还未初始化这些Bean时,disconf-client 模块会优先开始初始化(最高优先级)。它会将 配置文件名、配置项名记录在配置仓库里,并去 disconf-web 平台下载配置文件至classpath目录下。并且,还会到ZK上生成相应的结点。
接着Spring开始初始化用户定义的SpringBean。由于配置文件已经被正确下载至Classpath路径下,因此,JavaBean的配置文件使用的是分布式配置文件,而非本地的配置文件。
**待SpringBean初始化后,Disconf-client会获取配置更新回调类实例:**
此时,Spring上的所有Bean均已被init。Disconf-client模块会再次运行,这时它会去获取用户撰写的配置更新回调函数类实例。
一个配置更新回调函数通常是这样撰写的:
package com.example.disconf.demo.service.callbacks;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import com.baidu.disconf.client.common.annotations.DisconfUpdateService;
import com.baidu.disconf.client.common.update.IDisconfUpdate;
import com.example.disconf.demo.config.Coefficients;
import com.example.disconf.demo.config.JedisConfig;
import com.example.disconf.demo.service.SimpleRedisService;
/**
* 更新Redis配置时的回调函数
*
* @author liaoqiqi
* @version 2014-6-17
*/
@Service
@Scope("singleton")
@DisconfUpdateService(classes = {JedisConfig.class}, itemKeys = {Coefficients.key})
public class SimpleRedisServiceUpdateCallback implements IDisconfUpdate {
protected static final Logger LOGGER = LoggerFactory.getLogger(SimpleRedisServiceUpdateCallback.class);
@Autowired
private SimpleRedisService simpleRedisService;
/**
*
*/
public void reload() throws Exception {
simpleRedisService.changeJedis();
}
}
此类必须实现接口IDisconfUpdate,它可以不必是Java托管的。如果是SpringBean,则disconf-client会从Spring容器里获取此Bean。如果它不是SpringBean,disconf-client就会new一个实例出来。
使用SpringBean来定义此类的好处是,我们可以在此类中使用@Autowired来使用其它SpringBean。比较方便些。
disconf-client根据注解@DisconfUpdateService 以配置文件为Key,将回调函数实例列表放在此Key的Map里。当配置文件更新时,这些回调函数实例就会被按顺序执行。
**配置文件更新时,分布式配置文件会重新被下载:**
当配置文件更新时,disconf-client便会重新从 disconf-web 平台下载配置文件,并重新将值放在配置仓库里。并按顺序进行调用回调函数类的 reload() 方法。
**如何使用分布式配置文件类:**
在上面我们说到,配置文件类中的配置项必须有 get 方法,并且必须有 @DisconfFileItem 注解。
在 get 上面添加注解的原因就是为了做切面。
disconf-cient使用Spring AOP拦截 系统里所有含有@DisconfFileItem注解的 get 方法,把所有此类请求都定向到用户程序的配置仓库中去获取。
通过这种方式,我们可以实现统一的、集中式的在配置仓库里去获取配置文件数据。这是一种简洁的实现方式。
#### 分布式配置项的实现 ####
配置项相对于配置文件,比较灵活。我们可以在任何SpringBean里添加配置项。
如以下是在一个配置文件类里添加配置项:
package com.example.disconf.demo.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import com.baidu.disconf.client.common.annotations.DisconfFile;
import com.baidu.disconf.client.common.annotations.DisconfFileItem;
import com.baidu.disconf.client.common.annotations.DisconfItem;
/**
* 金融系数文件
*/
@Service
@DisconfFile(filename = "coefficients.properties")
public class Coefficients {
public static final String key = "discountRate";
@Value(value = "2.0d")
private Double discount;
private double baiFaCoe;
private double yuErBaoCoe;
/**
* 阿里余额宝的系数, 分布式文件配置
*
* @return
*/
@DisconfFileItem(name = "coe.baiFaCoe")
public double getBaiFaCoe() {
return baiFaCoe;
}
public void setBaiFaCoe(double baiFaCoe) {
this.baiFaCoe = baiFaCoe;
}
/**
* 百发的系数, 分布式文件配置
*
* @return
*/
@DisconfFileItem(name = "coe.yuErBaoCoe")
public double getYuErBaoCoe() {
return yuErBaoCoe;
}
public void setYuErBaoCoe(double yuErBaoCoe) {
this.yuErBaoCoe = yuErBaoCoe;
}
/**
* 折扣率,分布式配置
*
* @return
*/
@DisconfItem(key = key)
public Double getDiscount() {
return discount;
}
public void setDiscount(Double discount) {
this.discount = discount;
}
}
或者,我们也可以在一个Service类里添加配置项:
package com.example.disconf.demo.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import com.baidu.disconf.client.common.annotations.DisconfItem;
import com.example.disconf.demo.config.Coefficients;
/**
* 金融宝服务,计算一天赚多少钱
*
* @author liaoqiqi
* @version 2014-5-16
*/
@Service
public class BaoBaoService {
protected static final Logger LOGGER = LoggerFactory.getLogger(BaoBaoService.class);
public static final String key = "moneyInvest";
@Value(value = "2000d")
private Double moneyInvest;
@Autowired
private Coefficients coefficients;
/**
* 计算百发一天赚多少钱
*
* @return
*/
public double calcBaiFa() {
return coefficients.getBaiFaCoe() * coefficients.getDiscount() * getMoneyInvest();
}
/**
* k 计算余额宝一天赚多少钱
*
* @return
*/
public double calcYuErBao() {
return coefficients.getYuErBaoCoe() * coefficients.getDiscount() * getMoneyInvest();
}
/**
* 投资的钱,分布式配置
*
* 这里切面无法生效,因为SpringAOP不支持。
* 但是这里还是正确的,因为我们会将值注入到Bean的值里.
*
* @return
*/
@DisconfItem(key = key)
public Double getMoneyInvest() {
return moneyInvest;
}
public void setMoneyInvest(Double moneyInvest) {
this.moneyInvest = moneyInvest;
}
}
采用哪种方式,由用户选择。
值得注意的是,在第二种实现中,它的方法calcBaiFa() 时调用了 getMoneyInvest() 方法。 getMoneyInvest() 是配置项的get方法,它添加了@DisconfItem注解,表明它是一个配置项,并且会被切面拦截,moneyInvest的值会在配置仓库里获取。但是,可惜的是,SpringAOP是无法拦截"Call myself"方法的。也就是说getMoneyInvest()是无法被切面拦截到的。
为了解决此问题,在实现中,我们不仅将它的值 注入到配置仓库中,而且还注入到配置项所在类的实例里。因此,在上面第二种实现中,虽然 getMoneyInvest() 方法无法被拦截,但是它返回的还是正确的分布式值的。
配置文件也一样,配置值亦会注入到配置文件类实体中。
#### 非Spring编程的实现 ####
在非Spring方式下,无法使用AOP切面编程,因此无法统一的拦截配置数据请求。
在这种情况下,用户配置类的实现有两种方式:
1. 配置类的域是static。用户直接访问这些域便可以获取得到配置类数据。
2. 配置类使用单例。用户通过单例访问配置获取配置类数据。
注意:此两种方式均无法自动避免“配置读取不一致问题”。
当事件发生时,用户程序处理配置的方式是:
1. 配置文件更新时,系统会自动去下载配置文件存储到本地,并存储到配置仓库。对于static变量,系统会自动注入到配置类中。对于使用单例实现方式,用户必须在回调函数中进行用户配置类的更新。
2. 配置项更新时,与配置文件更新一样。
#### Zookeeper的目录存储结构 ####
|----disconf
|----app1_version1_env1
|----file
|----confA.properties
|----item
|----keyA
|----app2_version2_env2
|----file
|----conf2.properties
|----item
|----key2
### 基于XML的实现
虽然注解式编程简单、直观,易维护,但是,它是具有一定的代码侵入性的。
disconf考虑到有些用户不想写代码,只想通过XML配置(可能是在旧项目中使用disconf)来实现分布式配置的需求。因此,disconf亦实现了基于XML分布式的实现方式。
#### ReloadablePropertiesFactoryBean实现了配置文件的disconf托管
ReloadablePropertiesFactoryBean继承了PropertiesFactoryBean类,它主要做到:
- 托管配置文件至disconf仓库,并下载至本地。
- 解析配置数据传递到 ReloadingPropertyPlaceholderConfigurer
#### ReloadingPropertyPlaceholderConfigurer实现了配置数据至Bean的映射
ReloadingPropertyPlaceholderConfigurer继承自Spring的配置类PropertyPlaceholderConfigurer,它会在Spring启动时将配置数据与Bean做映射,以便在检查到配置文件更改时,可以实现Bean相关域值的自动注入。
#### ReloadConfigurationMonitor 定时校验配置是否更新
它是一个Timer类,定时校验配置是否有更改,进而促发 ReloadingPropertyPlaceholderConfigurer 类来分析要对哪些 Bean实例进行重新注入。
## 系统配置 ##
配置项 |
说明 |
是否必填 |
默认值 |
conf_server_store_action |
仓库 URL |
是 |
/api/config |
conf_server_zoo_action |
zoo URL |
是 |
/api/zoo |
conf_server_master_num_action |
获取远程主机个数的URL |
是 |
/api/getmasterinfo |
zookeeper_url_prefix |
zookeeper的前缀路径名 |
是 |
/disconfserver2 |
local_dowload_dir |
下载文件夹, 远程文件下载后会放在这里 |
是 |
./disconf/download |
## 局限性和注意事项 ##
[局限性和注意事项](局限性和注意事项.html)
## 异构系统主备控制实现
disconf将会为所有配置提供主备功能的开关,对于一个配置,多台实例机器可以进行竞争成为主机(使用主配置),竞争失败的实例将会成为备机(使用备配置)。基于zookeeper提供的分布式一致性锁,可以非常容易的达到此目的。