降级开关我们需要通过配置方式来动态开启/关闭,在应用时,首先要封装一套应用层API方便业务逻辑使用,对于开关数据的存储如果涉及的服务器/系统较少,则初期可以考虑使用配置文件配置。如果涉及的服务器/系统较多,则应该使用配置中心进行配置。实现时要做到不需要修改代码,不需要重启应用可动态配置开关。
1. 应用层API封装
如下是我们抽象并封装的开关API。
- USER(
- "用户信息",
- "user.not.call.backend", "是否不调用后端服务",
- "user. call.backend.rate.limit", "调用后端服务的限流",
- "user.redis.expire.seconds", "redis缓存过期时间"),
里边涉及到一两个配置。
- user.not.call.backend:是否回源调用后端用户服务。如果不开启,那么只会访问缓存,不会将流量打到后端。
- user.call.backend.ratio:调用后端服务的限流,比如配置100,即一秒只有100个请求会打到后端服务,剩余请求如果缓存没用命中时,则直接返回空数据或错误。
- user.redis.expire.seconds:后端返回的用户数据在缓存中缓存多久。
通过封装后,我们可以很简单地使用这些API。
- if (Switches.USER.notCall()) {
- retur nnull;
- }
或者
- cacheService.set(CacheKeys.getUserKey(pin), info, Switches.USER.getExpiresInSeconds());
API实现是从配置文件获取相关配置,如果没有,则返回一个默认值。
- public boolean notCall() {
- return DynamicConfigurer.getBoolean(callKey, false);
- }
或者
- public int getExpiresInSeconds() {
- return DynamicConfigurer.getInt(expiresKey, DEFAULT_EXPIRES_IN_ SECONDS);
- }
2. 配置文件实现开关配置
使用properties文件作为配置文件,借助JDK 7 WatchService实现文件变更监听,实现代码如下所示。
- static {
- try {
- filename= "application.properties";
- resource= new ClassPathResource(filename);
- //监听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();}
- //启动一个线程监听内容变化,并重新载入配置
- 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监听“application.properties”文件所在目录内容变化,包括修改、删除事件。
- 通过后台线程实现阻塞等待内容变化事件,一旦发现有内容变更,如果是“application.properties”文件发生变更,则重新装载配置文件。
- JVM停止时记得关闭WatchService。
整体实现比较简单,然后就可以封装Properties实现自己的开关API了。通过配置文件的方式缺点是每次配置文件内容变更需要将配置文件同步到服务器上,这点算是比较麻烦的,如果自动部署系统支持动态更改配置文件并同步用这种方式,那么也并不麻烦。只是如果要维护多个项目时,则需要切换多个界面来操作。
3. 配置中心实现开关配置
统一配置中心,或者叫分布式配置中心,目的是实现配置开关的集中管理,要有配置后台方便开关的配置,对于一般公司来说配置中心的维护要简单,不需要投入过多的人力来做这件事情,配置中心不管是采用拉取模式还是推送模式,要考虑到连接数和网络带宽可能带来的风险和问题。目前有一些开源方案可以选择,如Zookeeper、Diamond、Disconf、Etcd3、Consul。本文选择了Consul,其支持多数据中心、服务发现、KV存储等特性,而且使用简单,提供了简单的Web UI方便管理,更多介绍可以参考Nginx负载均衡部分。我们借助Consul的KV存储特性来实现配置管理。
启动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是我们系统名,后边是我们的配置名,Consul可以通过目录层次实现多级配置。
● 查询某个开关
curl
http://localhost:8500/v1/kv/item_tomcat/user.not.call.backend
● 查询某个系统的开关
curl
http://localhost:8500/v1/kv/item_tomcat?recurse
通过添加recurse参数实现目录树递归查询,可以得到如下结果。
- [{"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}]
● 阻塞查询某个系统的开关
- curl “http://192.168.61.129:8500/v1/kv/item_tomcat?t=10s&recurse= true&index=13192”
此处的index取列表ModifyIndex最大值,当其中的修改值大于此index,则返回数据。也可以添加“wait=10s”设置超时时间,超时后阻塞返回。
● 删除某个开关
curl -X DELETE
http://localhost:8500/v1/kv/item_tomcat/user.not. call. backend
● 删除某个系统开关
curl -X DELETE
http://localhost:8500/v1/kv/item_tomcat?recurse
整体使用比较简单,Consul Web UI提供了可视化配置,在启动时,通过ui-dir指定下载的Web UI目录即可,配置界面如下图所示。
配置界面简洁,目前存在的一个缺点是没有配置项的描述功能,在定义配置时,要起一个好理解且清晰的名字。
4. 应用代码
接下来就需要在应用代码中引入配置中心了,代码如下所示。
- 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下的数据(阻塞30秒),index是item_tomcat下的最后修改数据的修改index,为了实现阻塞
- //此处阻塞时间受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,实现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,实际应用时建议使用域名/VIP,记得配置相关的超时时间。
● KeyValueClient在获取数据时使用拉取模式(长轮询),可以设置合理的阻塞时间(次时间受限于Consul配置的超时时间),选择最大的ModifyIndex进行阻塞等待。
● 当ConsulException的code=404表示system在配置中心没有任何配置。
● 在加载该类时先运行一次拉取配置,然后启动后台线程阻塞拉取最新配置。
Consul的一个缺点是无法进行增量配置更新,如果订阅配置的应用很多,那么每次配置更新下发的量就非常大,如果有增量配置更新的话,则只需要把变化的下发即可。
到此集成Consul配置中心就完成了,此处只列出了核心代码,还有一些异常情况需要大家处理,使得配置中心在应用中做到高可用。
【本文是51CTO专栏作者张开涛的原创文章,作者微信公众号:开涛的博客( kaitao-1234567)】