張開濤:降級特技之配置中心
降級開關(guān)我們需要通過配置方式來動態(tài)開啟/關(guān)閉,在應(yīng)用時,首先要封裝一套應(yīng)用層API方便業(yè)務(wù)邏輯使用,對于開關(guān)數(shù)據(jù)的存儲如果涉及的服務(wù)器/系統(tǒng)較少,則初期可以考慮使用配置文件配置。如果涉及的服務(wù)器/系統(tǒng)較多,則應(yīng)該使用配置中心進行配置。實現(xiàn)時要做到不需要修改代碼,不需要重啟應(yīng)用可動態(tài)配置開關(guān)。
1. 應(yīng)用層API封裝
如下是我們抽象并封裝的開關(guān)API。
- USER(
- "用戶信息",
- "user.not.call.backend", "是否不調(diào)用后端服務(wù)",
- "user. call.backend.rate.limit", "調(diào)用后端服務(wù)的限流",
- "user.redis.expire.seconds", "redis緩存過期時間"),
里邊涉及到一兩個配置。
- user.not.call.backend:是否回源調(diào)用后端用戶服務(wù)。如果不開啟,那么只會訪問緩存,不會將流量打到后端。
- user.call.backend.ratio:調(diào)用后端服務(wù)的限流,比如配置100,即一秒只有100個請求會打到后端服務(wù),剩余請求如果緩存沒用命中時,則直接返回空數(shù)據(jù)或錯誤。
- user.redis.expire.seconds:后端返回的用戶數(shù)據(jù)在緩存中緩存多久。
通過封裝后,我們可以很簡單地使用這些API。
- if (Switches.USER.notCall()) {
- retur nnull;
- }
或者
- cacheService.set(CacheKeys.getUserKey(pin), info, Switches.USER.getExpiresInSeconds());
API實現(xiàn)是從配置文件獲取相關(guān)配置,如果沒有,則返回一個默認值。
- public boolean notCall() {
- return DynamicConfigurer.getBoolean(callKey, false);
- }
或者
- public int getExpiresInSeconds() {
- return DynamicConfigurer.getInt(expiresKey, DEFAULT_EXPIRES_IN_ SECONDS);
- }
2. 配置文件實現(xiàn)開關(guān)配置
使用properties文件作為配置文件,借助JDK 7 WatchService實現(xiàn)文件變更監(jiān)聽,實現(xiàn)代碼如下所示。
- static {
- try {
- filename= "application.properties";
- resource= new ClassPathResource(filename);
- //監(jiān)聽filename所在目錄下的文件修改、刪除事件
- watchService = FileSystems.getDefault().newWatchService();
- Paths.get(resource.getFile().getParent()).register(watchService,StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE);
- properties= PropertiesLoaderUtils.loadProperties(resource);
- } catch(IOException e) {e.printStackTrace();}
- //啟動一個線程監(jiān)聽內(nèi)容變化,并重新載入配置
- Thread watchThread = new Thread(() -> {
- while(true) {
- try{
- WatchKey watchKey = watchService.take();
- for (WatchEvent<?> event : watchKey.pollEvents()) {
- if (Objects.equal(event.context().toString(), filename)){
- properties =PropertiesLoaderUtils.loadProperties (resource);
- break;
- }
- }
- watchKey.reset();
- } catch (Exception e){e.printStackTrace();}
- }
- });
- watchThread.setDaemon(true);
- watchThread.start();
- Runtime.getRuntime().addShutdownHook(newThread(() -> {
- try{
- watchService.close();
- } catch(IOException e) {e.printStackTrace();}
- }));
- }
- 使用WatchService監(jiān)聽“application.properties”文件所在目錄內(nèi)容變化,包括修改、刪除事件。
- 通過后臺線程實現(xiàn)阻塞等待內(nèi)容變化事件,一旦發(fā)現(xiàn)有內(nèi)容變更,如果是“application.properties”文件發(fā)生變更,則重新裝載配置文件。
- JVM停止時記得關(guān)閉WatchService。
整體實現(xiàn)比較簡單,然后就可以封裝Properties實現(xiàn)自己的開關(guān)API了。通過配置文件的方式缺點是每次配置文件內(nèi)容變更需要將配置文件同步到服務(wù)器上,這點算是比較麻煩的,如果自動部署系統(tǒng)支持動態(tài)更改配置文件并同步用這種方式,那么也并不麻煩。只是如果要維護多個項目時,則需要切換多個界面來操作。
3. 配置中心實現(xiàn)開關(guān)配置
統(tǒng)一配置中心,或者叫分布式配置中心,目的是實現(xiàn)配置開關(guān)的集中管理,要有配置后臺方便開關(guān)的配置,對于一般公司來說配置中心的維護要簡單,不需要投入過多的人力來做這件事情,配置中心不管是采用拉取模式還是推送模式,要考慮到連接數(shù)和網(wǎng)絡(luò)帶寬可能帶來的風(fēng)險和問題。目前有一些開源方案可以選擇,如Zookeeper、Diamond、Disconf、Etcd3、Consul。本文選擇了Consul,其支持多數(shù)據(jù)中心、服務(wù)發(fā)現(xiàn)、KV存儲等特性,而且使用簡單,提供了簡單的Web UI方便管理,更多介紹可以參考Nginx負載均衡部分。我們借助Consul的KV存儲特性來實現(xiàn)配置管理。
啟動Consul Server
- ./consul agent -server -bootstrap-expect 1-data-dir /tmp/consul -bind 0.0.0.0-client 0.0.0.0 -ui-dir ./ui/
HTTP API CRUD
● 新增/修改
- curl -X PUT -d 'true' http://localhost:8500/v1/kv/item_tomcat/user.not.call.backend
- curl -X PUT -d '30' http://localhost:8500/v1/kv/item_tomcat / user.redis.expire.seconds
item_tomcat是我們系統(tǒng)名,后邊是我們的配置名,Consul可以通過目錄層次實現(xiàn)多級配置。
● 查詢某個開關(guān)
curl
http://localhost:8500/v1/kv/item_tomcat/user.not.call.backend
● 查詢某個系統(tǒng)的開關(guān)
curl
http://localhost:8500/v1/kv/item_tomcat?recurse
通過添加recurse參數(shù)實現(xiàn)目錄樹遞歸查詢,可以得到如下結(jié)果。
- [{"LockIndex":0,"Key":"item_tomcat/user.not.call.backend","Flags":0,"Value":"ZmFsc2U=","CreateIndex":13009,"ModifyIndex":13192},{"LockIndex":0,"Key":"item_tomcat/user.redis.expire.seconds","Flags":0,"Value":"MzA=","CreateIndex":13015,"ModifyIndex":13144}]
● 阻塞查詢某個系統(tǒng)的開關(guān)
- curl “http://192.168.61.129:8500/v1/kv/item_tomcat?t=10s&recurse= true&index=13192”
此處的index取列表ModifyIndex最大值,當其中的修改值大于此index,則返回數(shù)據(jù)。也可以添加“wait=10s”設(shè)置超時時間,超時后阻塞返回。
● 刪除某個開關(guān)
curl -X DELETE
http://localhost:8500/v1/kv/item_tomcat/user.not. call. backend
● 刪除某個系統(tǒng)開關(guān)
curl -X DELETE
http://localhost:8500/v1/kv/item_tomcat?recurse
整體使用比較簡單,Consul Web UI提供了可視化配置,在啟動時,通過ui-dir指定下載的Web UI目錄即可,配置界面如下圖所示。
配置界面簡潔,目前存在的一個缺點是沒有配置項的描述功能,在定義配置時,要起一個好理解且清晰的名字。
4. 應(yīng)用代碼
接下來就需要在應(yīng)用代碼中引入配置中心了,代碼如下所示。
- private static transient Properties properties =null;
- private static transient String system ="item_tomcat";
- static {
- Consul consul = Consul.builder()
- .withHostAndPort(HostAndPort.fromString("192.168.61.129:8500"))
- .withConnectTimeoutMillis(1000)
- .withReadTimeoutMillis(30 * 1000)
- .withWriteTimeoutMillis(5000).build();
- final KeyValueClient keyValueClient = consul.keyValueClient();
- final AtomicBoolean needBreak = new AtomicBoolean(true);
- Thread thread = new Thread(() -> {
- BigInteger index = BigInteger.ZERO;
- while(true){
- Properties _properties = new Properties();
- try{
- //阻塞獲取item_tomcat下的數(shù)據(jù)(阻塞30秒),index是item_tomcat下的最后修改數(shù)據(jù)的修改index,為了實現(xiàn)阻塞
- //此處阻塞時間受readTimeoutMillis影響
- List<Value> values =keyValueClient.getValues(system, QueryOptions.blockSeconds(30,index).build());
- for(Value value : values) {
- _properties.put(value.getKey().substring(system.length()+ 1), value. getValueAsString().orNull());
- //獲取最大的一個最后修改index,實現(xiàn)KeyValueClient #getValues的阻塞訪問
- indexindex = index.max(BigInteger.valueOf(value.getModifyIndex ()));
- }
- properties = _properties;
- } catch (ConsulException e) {
- e.printStackTrace();
- if(e.getCode() == 404) { //如果key不存在,休眠下
- try { Thread.sleep(5000L); } catch(Exception e1) {}
- }
- }
- if(needBreak.get()== true) {break;}
- }
- });
- thread.run();//先運行一次
- needBreak.set(false);
- thread.setDaemon(true);
- thread.start();
- }
● 在配置Consul時,目前我們使用的IP/PORT,實際應(yīng)用時建議使用域名/VIP,記得配置相關(guān)的超時時間。
● KeyValueClient在獲取數(shù)據(jù)時使用拉取模式(長輪詢),可以設(shè)置合理的阻塞時間(次時間受限于Consul配置的超時時間),選擇最大的ModifyIndex進行阻塞等待。
● 當ConsulException的code=404表示system在配置中心沒有任何配置。
● 在加載該類時先運行一次拉取配置,然后啟動后臺線程阻塞拉取最新配置。
Consul的一個缺點是無法進行增量配置更新,如果訂閱配置的應(yīng)用很多,那么每次配置更新下發(fā)的量就非常大,如果有增量配置更新的話,則只需要把變化的下發(fā)即可。
到此集成Consul配置中心就完成了,此處只列出了核心代碼,還有一些異常情況需要大家處理,使得配置中心在應(yīng)用中做到高可用。
【本文是51CTO專欄作者張開濤的原創(chuàng)文章,作者微信公眾號:開濤的博客( kaitao-1234567)】