欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

disconf原理 “入坑”指南

程序员文章站 2022-05-18 14:28:39
之前有了解过disconf,也知道它是基于zookeeper来做的,但是对于其运行原理不太了解,趁着周末,debug下源码,也算是不枉费周末大好时光哈 :) 。关于这篇文章,笔者主要是参考disconf源码和官方文档,若有不正确地方,感谢评论区指正交流~ disconf是一个分布式配置管理平台(Di ......

之前有了解过disconf,也知道它是基于zookeeper来做的,但是对于其运行原理不太了解,趁着周末,debug下源码,也算是不枉费周末大好时光哈 :) 。关于这篇文章,笔者主要是参考disconf源码和官方文档,若有不正确地方,感谢评论区指正交流~

disconf是一个分布式配置管理平台(distributed configuration management platform),专注于各种 分布式系统配置管理 的通用组件/通用平台, 提供统一的配置管理服务,是一套完整的基于zookeeper的分布式配置统一解决方案。disconf目前已经被多个公司在使用,包括百度、滴滴出行、银联、网易、拉勾网、苏宁易购、顺丰科技 等知名互联网公司。disconf源码地址 ,官方文档 https://disconf.readthedocs.io/zh_cn/latest/

目前disconf包含了 客户端disconf-client和 管理端disconf-web两个模块,均由java实现。服务依赖组件包括nginx、tomcat、mysql、zookeeper,nginx提供反向代理(disconf-web是前后端分离的),tomcat是后端web容器,配置存储在mysql上,基于zookeeper的wartch模型,实时推送。注意,disconf优先读取本地文件,disconf只支持应用对配置的读操作,通过在disconf-web上更新配置,然后由zookeeper通知到服务实例,最后服务实例去disconf-web端获取最新配置并更新到本地。

 

disconf 功能特点:

  • 支持配置(配置项/配置文件)分布式管理
  • 配置发布统一化
    • 配置发布、更新统一化,同一个上线包 无须改动配置 即可在 多个环境中(rd/qa/production) 上线
    • 配置更新自动化:用户在平台更新配置,使用该配置的系统会自动发现该情况,并应用新配置。特殊地,如果用户为此配置定义了回调函数类,则此函数类会被自动调用
  • 上手简单,基于注解或者xml配置方式

功能特点描述图

disconf原理 “入坑”指南

 

disconf 架构图

disconf原理 “入坑”指南

 

分析disconf,最好是在本地搭建一个disconf-web环境,方便调试代码,具体步骤可参考官方文档,使用disconf-client,只需要在pom引入依赖即可:

<dependency>
    <groupid>com.baidu.disconf</groupid>
    <artifactid>disconf-client</artifactid>
    <version>2.6.36</version>
</dependency>

对于开发人员来说,最多接触的就是disconf-web配置和disconf-client了,disconf-web配置官方文档已经很详细了,这里就来不及解释了,抓紧上车,去分析disconf-client的实现,disconf-client最重要的内容就是disconf-client初始化流程配置动态更新机制。disconf的功能是基于spring的(初始化是在spring的beandefinitionregistrypostprocessor#postprocessbeandefinitionregistry开始的,配置动态更新也是要更新到spring ioc中对应的bean),所以使用disconf,项目必须基于spring。

 

1 disconf-client 初始化流程

关于disconf-client的初始化,联想到spring ioc流程,我们先不看代码,可以猜想一下其大致流程,disconf-client首先需要从disconf服务端获取配置,然后等到ioc流程中创建好对应的bean之后,将对应的配置值设置到bean中,这样基本上就完成了初始化流程,其实disconf的初始化实现就是这样的。
 
disconf-client的初始化开始于beandefinitionregistrypostprocessor#postprocessbeandefinitionregistry(spring ioc初始化时,对于beandefinitionregistrypostprocessor的实现类,会调用其postprocessbeandefinitionregistry方法),disconf的disconfmgrbean类就是beandefinitionregistrypostprocessor的实现类,disconfmgrbean类的bean配置在哪里呢?其实就是disconf.xml中的配置,该配置是必须的,示例如下:
<!-- 使用disconf必须添加以下配置 -->
<bean id="disconfmgrbean" class="com.baidu.disconf.client.disconfmgrbean"
      destroy-method="destroy">
    <property name="scanpackage" value="com.luo.demo"/>
</bean>
<bean id="disconfmgrbean2" class="com.baidu.disconf.client.disconfmgrbeansecond"
      init-method="init" destroy-method="destroy">
</bean>

 
disconfmgrbean#postprocessbeandefinitionregistry方法主要做的3件事就是扫描(firstscan)、注册disconfaspectj 和 bean属性注入。

public void postprocessbeandefinitionregistry(beandefinitionregistry registry) throws beansexception {
    // scanpacklist包括disconf.xml中disconfmgrbean.scanpackage
    list<string> scanpacklist = stringutil.parsestringtostringlist(scanpackage, scan_split_token);

    // 1. 进行扫描
    disconfmgr.getinstance().setapplicationcontext(applicationcontext);
    disconfmgr.getinstance().firstscan(scanpacklist);

    // 2. register java bean
    registeraspect(registry);
}

 

1.1 firstscan

firstscan操作主要是加载系统配置和用户配置(disconf.properties),进行包扫描并入库,然后获取获取数据/注入/watch。
protected synchronized void firstscan(list<string> scanpackagelist) {
    // 导入配置
    configmgr.init();
    
    // registry
    registry registry = registryfactory.getspringregistry(applicationcontext);

    // 扫描器
    scanmgr = scanfactory.getscanmgr(registry);

    // 第一次扫描并入库
    scanmgr.firstscan(scanpackagelist);

    // 获取数据/注入/watch
    disconfcoremgr = disconfcorefactory.getdisconfcoremgr(registry);
    disconfcoremgr.process();
}

进行包扫描是使用reflections来完成的,获取路径下(比如xxx/target/classes)某个包下符合条件(比如com.luo.demo)的资源(reflections),然后从reflections获取某些符合条件的资源列表,如下:

disconf原理 “入坑”指南
/**
 * 扫描基本信息
 */
private scanstaticmodel scanbasicinfo(list<string> packnamelist) {
    scanstaticmodel scanmodel = new scanstaticmodel();

    // 扫描对象
    reflections reflections = getreflection(packnamelist);
    scanmodel.setreflections(reflections);

    // 获取disconffile class
    set<class<?>> classdata = reflections.gettypesannotatedwith(disconffile.class);
    scanmodel.setdisconffileclassset(classdata);

    // 获取disconffileitem method
    set<method> af1 = reflections.getmethodsannotatedwith(disconffileitem.class);
    scanmodel.setdisconffileitemmethodset(af1);

    // 获取disconfitem method
    af1 = reflections.getmethodsannotatedwith(disconfitem.class);
    scanmodel.setdisconfitemmethodset(af1);

    // 获取disconfactivebackupservice
    classdata = reflections.gettypesannotatedwith(disconfactivebackupservice.class);
    scanmodel.setdisconfactivebackupserviceclassset(classdata);

    // 获取disconfupdateservice
    classdata = reflections.gettypesannotatedwith(disconfupdateservice.class);
    scanmodel.setdisconfupdateservice(classdata);
    
    return scanmodel;
}
view code
获取到资源信息(比如disconffile 和disconffileitem )之后,读取disconffile类及其对应的disconffileitem信息,将它们放到disconffileitemmap中,最后将这些信息存储到仓库disconfcenterstore。这部分逻辑在scanmgrimpl.firstscan方法中,整体逻辑还是比较清晰的,这里就不贴代码了。
 
扫描入库之后,就该获取数据/注入/watch(disconfcorefactory.getdisconfcoremgr()中逻辑)了。
public static disconfcoremgr getdisconfcoremgr(registry registry) throws exception {
    fetchermgr fetchermgr = fetcherfactory.getfetchermgr();
    
    // 不开启disconf,则不要watch了
    watchmgr watchmgr = null;
    if (disclientconfig.getinstance().enable_disconf) {
        // watch 模块
        watchmgr = watchfactory.getwatchmgr(fetchermgr);
    }
    return new disconfcoremgrimpl(watchmgr, fetchermgr, registry);
}
public static watchmgr getwatchmgr(fetchermgr fetchermgr) throws exception {

    synchronized(hostssync) {
        // 从disconf-web端获取 zoo hosts信息,及zookeeper host和zk prefix信息(默认 /disconf)
        hosts = fetchermgr.getvaluefromserver(disconfwebpathmgr.getzoohostsurl(disclientsysconfig
                                                                                   .getinstance()
                                                                                   .conf_server_zoo_action));
        zooprefix = fetchermgr.getvaluefromserver(disconfwebpathmgr.getzooprefixurl(disclientsysconfig
                                                                                        .getinstance
                                                                                             ()
                                                                                        .conf_server_zoo_action));

        /**
         * 初始化watchmgr,这里会与zookeeper建立连接,如果/disconf节点不存在会新建
         */
        watchmgr watchmgr = new watchmgrimpl();
        watchmgr.init(hosts, zooprefix, disclientconfig.getinstance().debug);

        return watchmgr;
    }

    return null;
}
从disconf-web端获取zk host和 zk prefix之后,会建立与zk的连接,然后就该从disconf-web端下载配置和watcher了,也就是disconfcoremgr.process()逻辑。下载配置时disconf-client从disconf-web端获取配置的全量数据(http连接),并存放到本地,然后解析数据,生成datamap,datamap是全量数据。然后将数据注入到仓库中(disconfcenterstore.conffilemap,类型为map<string, disconfcenterfile>)。注意:这里还没有将配置的值设置到bean中,设置bean值是在spring的finishbeanfactoryinitialization流程做的,准确来说是在初始化bean disconfmgrbeansecond(bean配置在disconf.xml中),调用其init方法中做的。
 
在将值设置到仓库之后,就该监听对应配置了,这样才能使用zk的watch机制,在zk上监听的url格式为 /disconf/boot-demo_1_0_0_0_rd/file/specific.properties ,如果该url对应的node不存在则新建,注意该node是persistent类型的。然后在该node下新建临时节点,节点名字是discon-client的签名,格式为host_port_uuid,节点data为针对该配置文件,disconf-client需要的配置项的json格式数据,比如"{"port":9998,"host":"192.168.1.104"}"。
 

1.2 注册disconfaspectj

 往spring中注册一个aspect类disconfaspectj,该类会对@disconffileitem注解修饰的方法做切面,功能就是当获取bean属性值时,如果开启了disclientconfig.getinstance().enable_disconf,则返回disconf仓库中对应的属性值,否则返回bean实际值。注意:目前版本的disconf在更新仓库中属性值后会将bean的属性值也一同更改,所以,目前disconfaspectj类作用已不大,不必理会,关于该类的讨论可参考issue disconfaspectj 拦截的作用?

1.3 bean属性注入

bean属性注入是从disconfmgr.secondscan开始的:

protected synchronized void secondscan() {
    // 扫描回调函数,也就是注解@disconfupdateservice修饰的配置更新回调类,该类需实现idisconfupdate
    if (scanmgr != null) {
        scanmgr.secondscan();
    }

    // 注入数据至配置实体中
    if (disconfcoremgr != null) {
        disconfcoremgr.inject2disconfinstance();
    }
}
 
bean属性注入通过获取仓库中对应的属性值,然后调用setmethod.invoke或者field.set方法来设置,bean对应的boject是通过spring来获取的,也就是说,在获取后bean已经初始化完成,只不过对应的属性值还不是配置文件中配置的而已。如果程序中有2个类的@disconffile都是同一个配置文件,那么这个时候获取的bean是哪个类的bean呢?关于这个可以点击issue disconffile用法咨询,disconf目前只支持一个配置文件一个类的方式,不给两个class同时使用同一个 "resources.properties",否则第二个是不生效的。

 

2 配置动态更新机制

disconf的配置动态更新借助于zk的watch机制(watch机制是zk 3大重要内容之一,其余两个是zk协议和node存储模型)实现的,初始化流程会中会对配置文件注册watch,这样当配置文件更新时,会通知到discnof-client,然后disconf-client再从disconf-web中获取最新的配置并更新到本地,这样就完成了配置动态更新。
 
配置动态更新动作开始于disconffilecoreprocessorimpl.updateoneconfandcallback()方法:
/**
 * 更新消息: 某个配置文件 + 回调
 */
@override
public void updateoneconfandcallback(string key) throws exception {

    // 更新 配置
    updateoneconf(key);

    // 回调
    disconfcoreprocessutils.calloneconf(disconfstoreprocessor, key);
    callupdatepipeline(key);
}
更新配置时,首先更新仓库中值,然后更新bean属性值,配置更新回调是用户自定义的回调方法,也就是@disconfupdateservice修饰的类。配置更新时流程是:
开发人员在前端更新配置 -> disconf-web保存数据并更新zookeeper -> zookeeper通知disconf-client -> discnof-client 从 disconf-web下载对应配置 -> 更新仓库和bean属性 -> 调用回调 -> 更新配置完成。
 

小结

disconf 作为一个分布式的配置管理平台,文档详细,易于上手,动态配置更新,满足大多数场景的配置更新需求。美中不足的是,代码有的地方有点臃余,获取配置时还是全量获取方式,目前还不支持多个类共用同一个 "resources.properties",多余的disconfaspectj操作等。
 
使用disconf,最好的使用方式是基于它,将其改造成适合本公司或者项目组的工具,比如更方便的注解方式和回调方式等。