1. 程式人生 > >實現一個支援執行時併發修改配置生效的Configuration類

實現一個支援執行時併發修改配置生效的Configuration類

可配置性是一個好的應用程式的重要指標。我們常常需要實現類似能夠執行時修改配置的功能。最近在開發一箇中間層的服務程式,最終釋出的方式是把程式碼打成jar包交給呼叫方使用。這個中間層服務需要一些配置資訊,考慮了一下有幾個基本的需求:

1. 在ja包中提供一個service-defalut.properties配置檔案來提供全部的預設配置。這樣的好處是儘量減少對呼叫方的侵入。呼叫方可以不提供額外的配置。

2. 呼叫方也可以提供一個service-site.properties配置檔案來提供自定義的配置資訊,可以覆蓋預設配置

3. 在分散式系統中,希望提供一個在叢集中全域性可見的配置資訊,比如可以在ZooKeeper中設定配置資訊

4. 支援併發環境下執行時修改配置,並且可以立刻生效

5. 高效能訪問配置資訊


之前看過Hadoop的程式碼,Hadoop的org.apache.hadoop.conf.Configuration實現了1,2,4項需求,但是它訪問配置資訊的效能不高,原因是為了支援併發訪問,對讀寫配置都採用了加鎖的方式,鎖的粒度是方法級的,會影響併發的效能。


大致說一下org.apache.hadoop.conf.Configuration的實現

1. 採用Properties來儲存K-V的配置資訊

2. 採用CopyOnWriteArrayList來儲存預設的配置檔案列表

3. 採用ArrayList來儲存自定義的配置檔案列表

4. 對上述3個共享物件的訪問都採用了加鎖的方式來訪問,保證併發情況下的正確性


public class Configuration implements Iterable<Map.Entry<String,String>>,
                                      Writable {
      private ArrayList<Object> resources = new ArrayList<Object>();
      
      private static final CopyOnWriteArrayList<String> defaultResources =
    new CopyOnWriteArrayList<String>();
    
      private Properties properties;

       public static synchronized void addDefaultResource(String name) {
    if(!defaultResources.contains(name)) {
      defaultResources.add(name);
      for(Configuration conf : REGISTRY.keySet()) {
        if(conf.loadDefaults) {
          conf.reloadConfiguration();
        }
      }
    }
  }

      public synchronized void reloadConfiguration() {
    properties = null;                            // trigger reload
    finalParameters.clear();                      // clear site-limits
  }
  
  private synchronized void addResourceObject(Object resource) {
    resources.add(resource);                      // add to resources
    reloadConfiguration();
  }


  private synchronized Properties getProps() {
    if (properties == null) {
      properties = new Properties();
      loadResources(properties, resources, quietmode);
      if (overlay!= null) {
        properties.putAll(overlay);
        for (Map.Entry<Object,Object> item: overlay.entrySet()) {
          updatingResource.put((String) item.getKey(), UNKNOWN_RESOURCE);
        }
      }
    }
    return properties;
  }

  public String getRaw(String name) {
    return getProps().getProperty(name);
  }

  public void set(String name, String value) {
    getOverlay().setProperty(name, value);
    getProps().setProperty(name, value);
    this.updatingResource.put(name, UNKNOWN_RESOURCE);
  }

}


org.apache.hadoop.conf.Configuration 在配置的資料來源上是靈活地,可以動態的新增。它的主要問題是鎖太多,讀寫都加鎖,嚴重影響了併發訪問的效能。


簡單分析一下這個需求的場景:

1. 基於配置檔案的配置資訊一般是在啟動服務時已經配置好了,可以用static載入的方式一次載入,是執行緒安全的

2. 基於執行時修改配置資訊,即寫配置的情況非常小,比如把配置設定在ZooKeeper中,只有在需要修改時才會執行時修改,非常少的機會會去修改配置

3. 99%以上的場景是讀配置資訊,最好不要加鎖


基於這幾個需求,我寫了一個簡單的Configuration實現,可以實現一下功能:

1. 靈活支援多種配置資訊來源

2. 支援執行時修改配置資訊,並立刻生效

3. 寫配置操作保證強一致性,不會丟失寫的內容。寫操作需要加鎖。

4. 讀配置操作保證最終一致性,減少了鎖的粒度,在沒有寫配置的情況下是無鎖的。這樣大大地提高了併發情況下效能


程式碼如下,簡單測試了一下,考慮不周全的地方歡迎來拍。主要考慮併發的地方有:

1. 採用ConcurrentHashMap來取代Properties儲存配置內容,提高併發下的效能和保證正確性

2. 採用volatile標識properties屬性,這樣保證了在reloadConfiguration時設定properties = null的操作對讀操作get()立刻可見

3. get()讀操作時,使用了一個引用指向properties,而不是直接使用properties,這樣可以在properties被設定成null時,get操作還能讀取到舊的配置,保證下一次讀時能讀到最新內容,這裡保證了最終一致性。只有在properties == null的情況下(有配置修改),get操作才有可能加鎖去載入配置

4. set()寫操作時加鎖,這樣保證同意時候只能一個執行緒去修改配置。set不會重新載入配置,ConcurrentHashMap保證了set的值能立刻被get讀取到。

import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;

/**
 *	Configuration用來讀寫配置資訊,支援執行時配置的修改,支援多種資料來源的配置 
 *  可以通過addResource()來修改配置,也可以通過set()來修改配置,保證強一致性
 *  get()保證最終一致性,通過減小鎖的粒度來提高效能,執行時如果不呼叫addResource(),是無鎖的
 */
public class Configuration {
	private static Configuration instance = new Configuration();
	private Configuration(){}
	public static Configuration getInstance(){
		return instance;
	}
	
	public static final String DEFAULT_CONFIG_FILE = "service-default.properties";
	public static final String SITE_CONFIG_FILE = "service-site.properties";
	
	private static List<String> defaultResources = new ArrayList<String>();
	private static void addDefaultResource(String name){
		if(!defaultResources.contains(name)){
			defaultResources.add(name);
			instance.reloadConfiguration();
		}
	}
	
	static{
		addDefaultResource(DEFAULT_CONFIG_FILE);
		addDefaultResource(SITE_CONFIG_FILE);
	}

	private List<Object> resources = new ArrayList<Object>();
	
	private volatile ConcurrentHashMap<String, String> properties;
	
	private synchronized void reloadConfiguration(){
		properties = null;
	}
	
	private synchronized ConcurrentHashMap<String, String> getProperites(){
        // 減小鎖粒度,提高效能
            if(properties == null){
                // 不直接使用properties,防止 properties = new ConcurrentHashMap<String, String>();之後被get()直接獲取到未設定的properties
                ConcurrentHashMap<String, String> props = new ConcurrentHashMap<String, String>();
                loadResources(props, resources);
                properties = props;
            }
            return properties;
        }

	// 最常用的方法, 保證最終一致性
	public String get(String key){
		// 如果get時另外執行緒在addResource,將指向老的properties物件,取老的配置
		ConcurrentHashMap<String, String> p = properties;
		if(p == null){
			p = getProperites();
		}
	    return p.get(key);
    }

	// set保證強一致性
    public synchronized void set(String key, String value){
		getProperites().put(key, value);
    }
	
	private void loadResources(ConcurrentHashMap<String, String> props, List<Object> resources){
		// 先載入default
		for(String resource: defaultResources){
			loadResource(props, resource);
		}
		// 再載入自定義
		for(Object resource: resources){
			loadResource(props, resource);
		}
	}
	
	private void loadResource(ConcurrentHashMap<String, String> props, Object resource){
		if(props == null){
			return;
		}
		
		Properties newProps = new Properties();
		if(resource instanceof String){
			URL url = ResourceLoader.getResource((String)resource);
            if(url == null){
                return;
            }
			try {
				newProps.load(url.openStream());
			} catch (Exception e) {
				// quiet
			}
		}else if(resource instanceof InputStream){
			try {
				newProps.load((InputStream)resource);
			} catch (Exception e) {
				// quiet
			}
		}else if(resource instanceof URL){
			try {
				newProps.load(((URL)resource).openStream());
			} catch (Exception e) {
				// quiet
			}
		}else if(resource instanceof Properties){
			newProps = (Properties)resource;
		}
		
		for(Map.Entry<Object, Object> entry: newProps.entrySet()){
			props.put(entry.getKey().toString(), entry.getValue().toString());
		}
	}
	
	public void addResource(String obj){
		addResourceObject(obj);
	}
	
	public void addResource(URL obj){
		addResourceObject(obj);
	}
	
	public void addResource(InputStream obj){
		addResourceObject(obj);
	}
	
	public void addResource(Properties obj){
		addResourceObject(obj);
	}
	
	private synchronized void addResourceObject(Object obj){
		if(!resources.contains(obj)){
			resources.add(obj);
		}
		
		reloadConfiguration();
	}
	
}


簡單的測試程式碼

import java.util.Properties;

public class ConfigTest {
	public static void main(String[] args){
		final Consumer obj = new Consumer();
		
		new Thread(new Runnable(){

			@Override
			public void run() {
				while(true){
					System.out.println(obj.getLogFile());
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
			
		}).start();
		
		
		new Thread(new Runnable(){

			@Override
			public void run() {
				try {
					Thread.sleep(2000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				Configuration config = Configuration.getInstance();
				config.set("log.file", "/data/A-service.log");
			}
			
		}).start();
		
		new Thread(new Runnable(){

			@Override
			public void run() {
				try {
					Thread.sleep(4000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				Configuration config = Configuration.getInstance();
				Properties p = new Properties();
				p.put("log.file", "/data/B-service.log");
				
				config.addResource(p);
			}
			
		}).start();
	}
	
	private static class Consumer{
		Configuration config = Configuration.getInstance();
		
		public String getLogFile(){
			return config.get("log.file");
		}
	}
}