實現一個支援執行時併發修改配置生效的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");
}
}
}