Disconf原理解析
阅读原文时间:2021年04月23日阅读:1

1. 介绍
Disconf是一套完整的基于zookeeper的分布式配置统一解决方案。它支持配置(配置项+配置文件)的分布式化管理。

2. 安装
依赖Mysql, Tomcat, Nginx, Zookeeeper, Redis。
路径配置:
将你的配置文件放到此地址目录下(以下地址可自行设定):

/home/work/dsp/disconf-rd/online-resources

如果不确定如何配置,可以拷贝/disconf-web/profile/rd/目录下的文件,拷贝过去后,依次修改配置文件内容即可。配置文件包括:
[list]
[*]jdbc-mysql.properties (数据库配置)
[*]redis-config.properties (Redis配置,主要用于web登录使用)
[*]zoo.properties (Zookeeper配置)
[*]application.properties (应用配置)
[/list]
*注意,记得执行将application-demo.properties复制成application.properties。
*注意,即使只有一个redis,也应该配置两个redis client,否则将造成内部错误。

设置War包将要被部署的地址(以下地址可自行设定):

/home/work/dsp/disconf-rd/war

构建

ONLINE_CONFIG_PATH=/home/work/dsp/disconf-rd/online-resourcesWAR_ROOT_PATH=/home/work/dsp/disconf-rd/warexport ONLINE_CONFIG_PATHexport WAR_ROOT_PATHcd disconf-websh deploy/deploy.sh

这样会在 /home/work/dsp/disconf-rd/war 生成以下结果:

-disconf-web.war-html-META-INF-WEB-INF

Mysql:
参考disconf-web/sql的readme.md进行sql脚本初始化。
Tomcat:
修改server.xml文件,在Host结点下设定Context:

并设置端口为 8015(端口号与Nginx的upstream模块中server的端口号匹配就行),启动Tomcat,即可。
Nginx:
修改 nginx.conf

upstream disconf {    server 127.0.0.1:8015;}server {    listen   8081;    server_name disconf.com;    access_log /home/work/var/logs/disconf/access.log;    error_log /home/work/var/logs/disconf/error.log;    location / {        root /home/work/dsp/disconf-rd/war/html;        if ($query_string) {            expires max;        }    }    location ~ ^/(api|export) {        proxy_pass_header Server;        proxy_set_header Host $http_host;        proxy_redirect off;        proxy_set_header X-Real-IP $remote_addr;        proxy_set_header X-Scheme $scheme;        proxy_pass http://disconf;    }}

关于host:
这里的 host 设置成 disconf.com (可以自定义),但它必须与application.properties 里的domain一样。然后浏览器的访问域名也是这个。
通过Nginx(处理静态请求) + Tomcat(处理动态请求)达到处理请求的逻辑。
在浏览器输入:http://localhost:8081后表明安装成功,默认登录用户名admin/admin。
[img]http://dl2.iteye.com/upload/attachment/0124/5858/3521992a-d441-3f4e-862d-3fe3aeab6c05.jpg[/img]

3. 架构设计
[img]http://dl2.iteye.com/upload/attachment/0124/5840/214a5e7e-5e82-3974-9c05-4cae54ea333d.png[/img]
Disconf通过disconf-web管理配置信息,然后将配置的key在Zookeeper上建立节点,disconf-client启动后拉取自身需要的配置信息并监听Zookeeper的节点。在web上更新配置信息会触发zk节点状态的变动,client可以实时感知到变化,然后从web上拉取最新配置信息。

4. 使用-基于注解
由于最新版Disconf(2.6.36)只支持Spring项目集成,非spring项目暂时无法使用。添加Maven依赖:

<dependency>            <groupId>com.baidu.disconf</groupId>            <artifactId>disconf-client</artifactId>         <version>2.6.36</version>       </dependency>

在需要进行配置的类通过DisconfFile注解指名配置从哪个文件获取,在get方法通过DisconfFileItem注解的name属性指定文件中的key,associateField指类的属性,即当前值设置到当前类的哪个属性中。

@Service@DisconfFile(filename = "redis.properties")public class JedisConfig {    private String host;    private int port;   @DisconfFileItem(name = "redis.host", associateField = "host")  public String getHost() {       return host;    }   public void setHost(String host) {      this.host = host;   }   @DisconfFileItem(name = "redis.port", associateField = "port")  public int getPort() {      return port;    }   public void setPort(int port) {     this.port = port;   }}



@Servicepublic class SimpleRedisService implements InitializingBean, DisposableBean{    protected static final Logger LOGGER = LoggerFactory.getLogger(SimpleRedisService.class);    private Jedis jedis = null;    @Autowired    private JedisConfig jedisConfig;  @Override   public void destroy() throws Exception {        if (jedis != null) {            jedis.disconnect();        }    }    public String getKey(String key) {        if (jedis != null) {            return jedis.get(key);        }        return null;    }    public void changeJedis() {        LOGGER.info("start to change jedis hosts to: " + jedisConfig.getHost() + " : " + jedisConfig.getPort());        jedis = JedisUtil.createJedis(jedisConfig.getHost(), jedisConfig.getPort());        LOGGER.info("change ok.");    }   @Override   public void afterPropertiesSet() throws Exception {     jedis = JedisUtil.createJedis(jedisConfig.getHost(), jedisConfig.getPort());    }}

通过DisconfUpdateService注解指定哪些配置更新时,进行配置的更新。注解了DisconfUpdateService的类同时要实现IDisconfUpdate方法。

@Service@DisconfUpdateService(classes = {JedisConfig.class})public class SimpleRedisServiceUpdateCallback implements IDisconfUpdate{    @Autowired  private SimpleRedisService simpleRedisService;  @Override   public void reload() throws Exception {     simpleRedisService.changeJedis();   }}

添加配置spring的扫描类:

<bean id="disconfMgrBean" class="com.baidu.disconf.client.DisconfMgrBean" destroy-method="destroy">        <property name="scanPackage" value="com.dfire.missile" />   </bean><bean id="disconfMgrBean2" class="com.baidu.disconf.client.DisconfMgrBeanSecond" init-method="init"      destroy-method="destroy"></bean>

最后,在classpath下添加disconf.properties文件:

#为上面nginx地址disconf.conf_server_host=localhost:8081#disconf需要指定应用的app名disconf.app=test#版本号,推荐x_x_x_x形式disconf.version=1_0_0_0#是否开启从远程仓库获取配置disconf.enable.remote.conf=true#指定获取环境,rd,qa,local,online四个值disconf.env=rd#忽略的分布式配置,用空格分隔disconf.ignore=#调试模式。调试模式下,ZK超时或断开连接后不会重新连接(常用于client单步debug)。非调试模式下,ZK超时或断开连接会自动重新连接。disconf.debug=false#获取远程配置 重试次数,默认是3次disconf.conf_server_url_retry_times=1#获取远程配置 重试时休眠时间,默认是2秒disconf.conf_server_url_retry_sleep_seconds=1#用户定义的下载文件夹, 远程文件下载后会放在这里。注意,此文件夹必须有有权限,否则无法下载到这里,默认./disconf/downloaddisconf.user_define_download_dir=/disconf/download#下载的文件会被迁移到classpath根路径下,强烈建议将此选项置为 true(默认是true)    disconf.enable_local_download_dir_in_class_path=false

假如,先前我们在web上创建了redis.properties文件,且设置了redis.host,redis.port值,当client启动后,我们就可以在/disconf/download目录下,看到redis.properties文件已经下载下来了。如果我们在web上进行redis.host,redis.port修改,可以看到如下log:

LOGGER.info("start to change jedis hosts to: " + ***+ " : " + ***);LOGGER.info("change ok.");

以上,我们通过了@DisconfFile和@DisconfFileItem组合,进行了文件配置获取。还可以通过@DisconfItem进行KV值的配置获取。DisconfItem的key为web端设置的"配置项目"

@Servicepublic class Host {    private int size;   private int threshold;  @DisconfItem(key = "missile_gateway_msg_threshold", associateField = "threshold")   public int getThreshold() {     return threshold;   }   public void setThreshold(int threshold) {       this.threshold = threshold; }   @DisconfItem(key = "missile_gateway_msg_size", associateField = "size") public int getSize() {      return size;    }   public void setSize(int size) {     this.size = size;   }}

然后再配置@DisconfUpdateService并实现IDisconfUpdate接口即可:

@Service@DisconfUpdateServicepublic class HostCallcack implements IDisconfUpdatePipeline, IDisconfUpdate{    protected static final Logger LOGGER = LoggerFactory.getLogger(HostCallcack.class); @Autowired  private Host host;  @Override   public void reloadDisconfFile(String key, String filePath) throws Exception {       LOGGER.info("reload file key: " + key + " , filePath : " + filePath);   }   @Override   public void reloadDisconfItem(String key, Object content) throws Exception {        LOGGER.info("reload item key: " + key + " , content : " + content);     LOGGER.info("threshold : " + host.getThreshold());      LOGGER.info("size : " + host.getSize());    }   public void reload() throws Exception {     LOGGER.info("threshold : " + host.getThreshold());      LOGGER.info("size : " + host.getSize());    }}

这一次,我们还实现了IDisconfUpdatePipeline接口,它和IDisconfUpdate的区别在于,前者可以同时监听到DisconfFile和DisconfItem的变化。

5. 使用-基于xml
xml方式除了需要添加上面的spring bean外,还需要指定哪些配置文件需要更新,哪些对应的bean需要从这些文件中加载数据:

<!--需要拉取更新的文件 --><bean id="configproperties_disconf" class="com.baidu.disconf.client.addons.properties.ReloadablePropertiesFactoryBean"> <property name="locations"> <list> <value>file:${disconf.user_define_download_dir}/data.properties</value>  <value>file:${disconf.user_define_download_dir}/dubbo.properties</value> <value>file:${disconf.user_define_download_dir}/dynamic.properties</value> </list> </property></bean><!--内容有变动,重新reload --><bean id="propertyConfigurer" class="com.baidu.disconf.client.addons.properties.ReloadingPropertyPlaceholderConfigurer"> <property name="ignoreResourceNotFound" value="true"/> <property name="ignoreUnresolvablePlaceholders" value="true"/> <property name="propertiesArray"> <list> <ref bean="configproperties_disconf"/> </list> </property></bean><!--配置的bean --><bean id="autoService" class="com.example.disconf.demo.service.AutoService">    <property name="auto" value="${auto=100}"/></bean><bean id="autoService2" class="com.example.disconf.demo.service.AutoService2">    <property name="auto2" value="${auto2=100}"/></bean>

6. 原理-client
[img]http://dl2.iteye.com/upload/attachment/0124/5860/508650a2-97d8-3fe4-aeeb-cfca4a639021.png[/img]
我们主要关注第三块-系统正常运行时请求配置数据:对配置数据进行AOP拦截。这里可以保证对于数据总是从仓库获取值,且对于更新数据也能立即生效。com.baidu.disconf.client.store.aspect.DisconfAspectJ为AOP类,分别对File和Item进行了切面:

@Around("anyPublicMethod() && @annotation(disconfFileItem)")    public Object decideAccess(ProceedingJoinPoint pjp, DisconfFileItem disconfFileItem) throws Throwable {        if (DisClientConfig.getInstance().ENABLE_DISCONF) {            MethodSignature ms = (MethodSignature) pjp.getSignature();            Method method = ms.getMethod();            //            // 文件名            //            Class<?> cls = method.getDeclaringClass();            DisconfFile disconfFile = cls.getAnnotation(DisconfFile.class);            //            // Field名            //            Field field = MethodUtils.getFieldFromMethod(method, cls.getDeclaredFields(), DisConfigTypeEnum.FILE);            if (field != null) {                //                // 请求仓库配置数据                //                DisconfStoreProcessor disconfStoreProcessor =                        DisconfStoreProcessorFactory.getDisconfStoreFileProcessor();                Object ret = disconfStoreProcessor.getConfig(disconfFile.filename(), disconfFileItem.name());                if (ret != null) {                    LOGGER.debug("using disconf store value: " + disconfFile.filename() + " ("                            + disconfFileItem.name() +                            " , " + ret + ")");                    return ret;                }            }        }        Object rtnOb;        try {            // 返回原值            rtnOb = pjp.proceed();        } catch (Throwable t) {            LOGGER.info(t.getMessage());            throw t;        }        return rtnOb;    }    /**     * 获取配置项数据, 只有开启disconf远程才会进行切面     *     * @throws Throwable     */    @Around("anyPublicMethod() && @annotation(disconfItem)")    public Object decideAccess(ProceedingJoinPoint pjp, DisconfItem disconfItem) throws Throwable {        if (DisClientConfig.getInstance().ENABLE_DISCONF) {            //            // 请求仓库配置数据            //            DisconfStoreProcessor disconfStoreProcessor = DisconfStoreProcessorFactory.getDisconfStoreItemProcessor();            Object ret = disconfStoreProcessor.getConfig(null, disconfItem.key());            if (ret != null) {                LOGGER.debug("using disconf store value: (" + disconfItem.key() + " , " + ret + ")");                return ret;            }        }        Object rtnOb;        try {            // 返回原值            rtnOb = pjp.proceed();        } catch (Throwable t) {            LOGGER.info(t.getMessage());            throw t;        }        return rtnOb;    }

7. 原理-web
[img]http://dl2.iteye.com/upload/attachment/0124/5868/f4e3f2ba-7a13-3659-b3ce-a50fe676acfa.png[/img]
web主要用于管理配置项,当新建配置项后,会保存到DB的config及config-history表,并邮件通知app表对应的邮件地址。
对更新的配置项,会更新config表并新增记录到config-history表,邮件通知,同时添加zk节点(/disconf/app_version_env/file(或item)/配置项),当client端更新配置后,会在此节点下写入client标识,所以就有了web端的“实例列表”。
client启动后会从web拉取最新的配置文件信息,并监听相应的zk节点,当有数据变化时,zk会通知client,然后client重新从web拉取最新数据。
web的另外一个功能是配置项检查,每30分钟将DB config表中配置值与zk相比,发现不一致会邮件通知。

8. DB-table
Disconf只有如下7张表,功能描述如下:
[list]
[*]app: app表,配置以app为核心。
[*]config: 配置表。
[*]config_history: 配置记录表,每更新每新增。
[*]env: 环境表。
[*]role: 角色表,用户有哪些角色。
[*]role_resource: 角色权限对应的功能表。
[*]user: 用户表,登录使用。
[/list]

9. Disconf VS Diamond VS Apollo
[table]
| | Diamond| Disconf| Apollo|
| 数据持久性| 存储在mysql上| mysql| mysql|
| 推拉模型| 拉模型,http长轮询|基于ZK,实时 |http长轮询,1s |
| 配置读写| 支持实例对配置读写|读|读 |
| 容灾| 多级容灾模式|多级 |多级 |
| 配置数据模型| 只支持KV结构的数据| 文件和KV| KV|
| 是否支持灰度| 是|否 |是 |
| 客户端配置信息监控|是 | 是|是 |
[/table]
解释下,Apollo的长轮询为啥是1s。首先Apollo为了减少对外中间件的依赖,将消息消费中间件改为依赖DB的扫表操作。当用户通过Portal更新配置时,向ReleaseMessage表插一条记录,其Config Service有个线程会每秒扫表,检查到有数据后,会通知注册的监听服务即client,然后进行拉取更新操作。

10. 注意事项
[list]
[*]注解是放在get方法之上的,对于”call self”的方法调用,AOP无法拦截得到(此时读取对应bean属性的默认值),这样就无法统一处理这些配置。一旦出现这种情况,“非一致性读问题”就会产生。
[*]web在zk连接异常的情况下,对配置项的修改也会成功。这将导致client端无法实时获取最新数据以及zk节点数据与db数据不一致问题。当修改配置项时,发现web端提示zk异常情况,请多次执行相同值修改操作直到无异常提示。对于后者,web端提供了定时任务每30m检测两者间的数据一致性问题。
[/list]

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器

你可能感兴趣的文章