統一配置中心2


統一配置中心方案

統一配置中心1中記錄了我之前項目中如何處理多系統中的配置問題,對於統一配置中心組件一般分為兩種做法:

自建

它的好外與缺點都非常明確。

好處

  • 設計以及代碼實現都由自己把控,可形成自己的知識積累
  • 設計可以足夠簡化,無需考慮過多場景
  • 能夠快速適應項目的需求,無需考慮開源的是否支持

缺點

  • 需要人力的投入,而且有重復造輪子的嫌疑,需要有足夠的說服力
  • 對技術有一定要求,如果寫出來的組件問題多或者通用性不強會形成浪費的局面
  • 缺少權威性,需要長時間的推進

開源

開源的配置中心,我之前有提到過百度的disconf,還有當當的config-toolkit,這些產品都有很多應用案例,功能也非常全。

好處

  • 權威性相對好,成功案例多的組件往往在技術推進上很比較容易
  • 不需要過多的人力投入,往往只需要一步一下按步驟來
  • 通用性比較強,適用的場景相對廣泛

缺點

  • 可能並不完全與實際需求相符,需要通過一些擴展來支持
  • 可能會是重量級組件,而你的需求只使用其中一小部分
  • 可能存在BUG
  • 組件升級不受使用者控制

自建的方案

場景

當系統越來越多后,每個系統的配置如果沒有統一的管理機制那么會非常難管理,需要有一種侵入性小的方案來將這些原本在單項目中維護的配置工作統一管理起來。

配置存儲

推薦使用zookeeper來存儲項目中的配置項,也可以是其它的存儲。下面設計的配置中心組件是可擴展,但實現暫時只實現了zookeeper。

核心思路

在系統加載時,想辦法將存儲在zookeeper中的配置內容加載到系統變量中,然后程序就可以像調用本地配置文件一樣了,不需要改變現有系統引用變量的行為。至於本地配置文件直接使用Spring自帶功能即可,當然也可以統一在組件中。

實際需求

  • 配置存儲在zookeeper
  • 支持本地配置
  • 有本地配置的情況下,以本地配置為准(本地調試)
  • 支持zookeeper熱更新,即在運行時當zookeeper信息發生變更時能夠實時同步到程序中

不需要支持本地文件的熱更新

設計實現

時序圖

內容有點多,先看下配置加載的時序圖,不包含事件通知以及熱更新。

啟動組件入口

PropertyPlaceholderConfigurer,可以利用它來啟動組件的初始化進而從zookeeper中加載數據到系統中,即先加載zookeeper數據然后加載本地配置文件。

@Bean
public static PropertyPlaceholderConfigurer properties() {
    SpringPropertyInjectSupport springPropertyInjectSupport=new SpringPropertyInjectSupport();
    springPropertyInjectSupport.setConfigNameSpaces("configcenter/"+System.getProperty("env"));
    springPropertyInjectSupport.init();
    PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer();
    Resource[] resources = new ClassPathResource[]
            { new ClassPathResource( "application.properties" ) };
    ppc.setLocations( resources );
    ppc.setIgnoreUnresolvablePlaceholders( true );
    return ppc;
}

SpringPropertyInjectSupport,是自定義的組件啟動類,它負責加載zookeeper中的數據到系統:核心方法是init。

public void init() {
    if (this.configNameSpaces != null) {
        this.setSystemPropertiesFromConfigCenter();
    }
}

configNameSpaces是統一配置中心管理所有節點的父結點,比如configcenter/test是指測試環境的配置,系統中的配置節點名稱不包含這些與框架相關的信息,只需要配置dataSource=XXX即代表configcenter/test/dataSource這個配置項。

ConfigCenterFactory是個單例用來返回統一配置中心實例,ConfigCenterService是配置接口,然后由配置接口獲取所有的配置項(類型是一個Map),最后將Map中的信息導入到系統變量中。

private void setSystemPropertiesFromConfigCenter() {
    if (StringUtils.isBlank(this.configNameSpaces)) {
        return;
    }
    ConfigCenterFactory.getInstance().setSystemNameSpace(this.configNameSpaces);
    ConfigCenterService cc = ConfigCenterFactory.getInstance().getConfig(this.configNameSpaces);
    Map<String, Object> config = cc.getConfig();
    setSystemProperys(cc, config);

}

private void setSystemProperys(ConfigCenterService cc, Map<String, Object> config) {
    for (String key : config.keySet()) {
        String value = cc.get(key);
        if (key.contains(".")) {
            key = key.substring(1);
        }
        if (value == null) {
            value = "";
        }
        System.setProperty(key, value);
    }
}

ConfigCenterFactory

實例化配置組件,為了支持同時加載多個不同節點下的數據,所以以nameSpace做為key將實例放在Map中,比如想同時訪問商品以及訂單的配置,它們的namespace分別為

  • configcenter/order/test
  • configcenter/product/test
private static final Object lockObj = new Object();
private ConcurrentHashMap<String, ConfigCenterService> configCenterCache = null;
public ConfigCenterService getConfig(final String hosts, final String nameSpace) {

    Preconditions.checkNotNull(hosts);
    Preconditions.checkNotNull(nameSpace);

    StringBuilder sb = new StringBuilder(hosts);
    sb.append(nameSpace);

    final String key = sb.toString().intern();

    ConfigCenterService config = this.configCenterCache.get(key);
    if (config == null) {
        synchronized (lockObj) {
            if (!this.configCenterCache.containsKey(key)) {
                ConfigOption co = new ConfigOption(nameSpace, hosts);
                ConfigCenterService cc = new ConfigCenterServiceImpl(co);
                this.configCenterCache.put(key, cc);
            }
        }
    } else {
        return config;
    }

    return this.configCenterCache.get(key);
}

ConfigCenterService

配置管理接口,主要包含三部分內容:

  • 返回所有數據為一個Map
  • 數據更新通知,當某個配置變更后需要觸發的功能。

不是更新系統中的變量,是指系統中的變量發生改變之后需要做什么?比如數據庫的配置發生變化,此時需要重新刷新數據庫連接池的信息等。
一些獲取配置的協助類,即強類型化返回配置,減少調用端的類型轉換。

public interface ConfigCenterService {
    public Map<String,Object> getConfig();
    public String get(String key);
    public Long getLongValue(String key);
    public Integer getIntegerValue(String key);
    public Double getDoubleValue(String key);
    public Boolean getBooleanValue(String key);
    public void notify(DataChangeEvent event);
    public void colse();
}

AbstractConfigCenterService

一些通用的邏輯實現放在這里。

ZkConfigCenterServiceImpl

是具體的配置接口實現類,繼承AbstractConfigCenterService,實現ConfigCenterService接口。

  • zk客戶端,選用curator-recipes,它很好的解決了重試等問題。
  • 連接zk
private void startClient() {
    if (this.client == null) {
        try {
            this.client = CuratorFrameworkFactory.builder()
                    .connectString(configOption.getZkUrls())
                    .namespace(configOption.getNameSpace())
                    .retryPolicy(configOption.getRetryPolicy())
                    .connectionTimeoutMs(20000)
                    .build();
            this.client.start();
            logger.info("zkclient start " + configOption.getNameSpace());

        } catch (Throwable e) {
            if(null!=this.client){
                this.client.close();
            }
            throw new RuntimeException("CuratorFrameworkFactory start error",e);
        }

    }
}
  • 加載配置
    從根目錄開始一層一層住下加載,同時啟用節點監控。
private void loadConfig(String nodePath) {

    try {
        if (StringUtils.isNotEmpty(nodePath)) {
            loadData(nodePath);
        }
        GetChildrenBuilder childrenBuilder = client.getChildren();

        try {
            List<String> children = null;
            if (StringUtils.isEmpty(nodePath)) {
                children = childrenBuilder.watched().forPath(null);
            } else {
                children = childrenBuilder.watched().forPath(nodePath);
            }

            loadChildsConfig(children, nodePath);
        } catch (Exception e) {
            throw Throwables.propagate(e);
        }

    } catch (Exception e) {
        logger.error("load zk config error namespace={}", configOption.getNameSpace());
        logger.error("load config error", e);
        this.client.close();
        throw new RuntimeException("load zk error");
    }
}
  • 監控配置變更
    當zookeeper的節點數據發生刪除更新時會發起通知,我們根據通知信息來做相應的數據變更。
private Watcher getPathWatcher() {
    return new Watcher() {
        @Override
        public void process(WatchedEvent event) {
            if (event != null) {
                try {
                    boolean isDelete = false;
                    if (event.getState() == Event.KeeperState.SyncConnected) {

                        String path = event.getPath();
                        if (path == null || path.equals("/")) return;
                        switch (event.getType()) {
                            case NodeDeleted:
                                postRemovePath(event.getPath());
                                isDelete = true;
                                break;
                            case NodeDataChanged:
                                postDataChangeEvent(event);

                                break;
                            default:
                                break;
                        }

                        if (!isDelete) {
                            watchPathDataChange(event.getPath());
                        }
                    }

                } catch (Exception e) {
                    logger.info("zk data changed error:",e);
                }
            }
        }
    };
}

節點數據變更后的事件通知,采用guava的eventbus來實現。

private void postDataChangeEvent(WatchedEvent event) throws Exception {
    byte[] data = client.getData().forPath(event.getPath());
    String value = new String(data, Charsets.UTF_8);
    String key = event.getPath().replace("/", ".");
    postDataChangeKeyValue(key, value);
}

private void postDataChangeKeyValue(String key, String value) {
    this.config.put(key, value);

    Map<String, Object> map = new HashMap<String, Object>();
    map.put(key, value);
    DataChangeEvent dataChangeEvent = new DataChangeEvent(map);

    configOption.getEnventBus().post(dataChangeEvent);

}

源碼

統一配置源碼

源碼參考了網上開源的項目,由於時間久遠原項目已經找到地址了。
我公布的源碼是經過我重新整理后的結果,並非全部出自於自己。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM