分布式配置中心
分布式配置中心主要是解决在分布式的场景下,一个服务可能会有多台实例,在传统的单体架构下是每一台服务器上都会有一个配置文件,我们可以通过修改配置文件来实现配置的更改,但在分布式架构下,我们要实现该效果就必须每台实例都更改一遍,这样不仅增加了很高的运维成本,而且还可能会因为遗漏导致一系列别的问题发生。因此分布式配置中心就应运而生,通过分布式配置中心的方式来解决该问题。
常见的配置中心
配置中心对比
功能点 | SpringCloud Config | Apollo | Nacos |
---|---|---|---|
开源时间 | 2014.9 | 2016.5 | 2018.6 |
配置实时推送 | 支持(SpringCloud Bus) | 支持(HTTP长轮训1s内) | 支持(HTTP长轮训1s内) |
版本管理 | 支持(GIt) | 支持 | 支持 |
配置回滚 | 支持(GIt) | 支持 | 支持 |
灰度发布 | 支持 | 支持 | 待支持 |
权限管理 | 支持 | 支持 | 待支持 |
多集群 | 支持 | 支持 | 支持 |
监控查询 | 支持 | 支持 | 支持 |
语言 | Java | go/c++/java/python/php/.net/openApi | python/java/node.js/openApi |
单机部署 | config-server+git+springcloud bus(支持配置实时推送) | apollo-quikstart+mysql | nacos单节点 |
集群部署 | config-server+git+mq(部署复杂) | config+admin+portal+mysql(部署复杂) | nacos+mysql(部署简单) |
配置格式校验 | 不支持 | 支持 | 支持 |
通信协议 | HTTP/AMQP | HTTP | HTTP |
对于目前SpringCloud生态来讲选型比较简单,Alibaba使用Nacos,NetFlix使用SpringCloud Config
基于Nacos实现
- pom中引入jar包:
<dependency>
<groupId>io.terminus.common</groupId>
<artifactId>terminus-spring-boot-starter-configcenter</artifactId>
</dependency>
- 增加bootstrap.yml配置文件,注意必须在该配置文件中声明nacos的config配置:
spring:
cloud:
nacos:
config:
server-addr: ${NACOS_ADDRESS:127.0.0.1:8848} #配置中心地址
group: ${CONFIGCENTER_GROUP_NAME:}
namespace: ${CONFIGCENTER_TENANT_ID:}
file-extension: yaml
profiles:
active: dev
- 在Nacos对应的namespace下创建基于以下规则的配置文件
${prefix}-${spring.profiles.active}.${file-extension}
如图所示:
prefix
默认为spring.application.name
的值,也可以通过配置项spring.cloud.nacos.config.prefix
来配置。spring.profiles.active
即为当前环境对应的 profile,详情可以参考 Spring Boot文档。 注意:当spring.profiles.active
为空时,对应的连接符-
也将不存在,dataId 的拼接格式变成${prefix}.${file-extension}
file-exetension
为配置内容的数据格式,可以通过配置项spring.cloud.nacos.config.file-extension
来配置。目前只支持properties
和yaml
类型。- 通过 Spring Cloud 原生注解
@RefreshScope
实现配置自动更新:
@RestController
@RequestMapping("/config")
@RefreshScope
public class ConfigController {
@Value("${useLocalCache:false}")
private boolean useLocalCache;
@RequestMapping("/get")
public boolean get() {
return useLocalCache;
}
}
- 手动修改Nacos中的配置文件,可以发现Java代码中获取到的值也发生变更
日志配置
当前配置了Nacos作为配置中心后,会发现后台不断的在打印日志,因此我们需要修改配置中心的日志打印级别
logging
level:
com:
alibaba:
nacos:
client:
naming: error
自定义处理
如果我们有需求要摒弃,Nacos默认的配置规则,比如我们自定义自己的配置文件名称的,那么此时我们能够怎么实现呢?
- 首先重写Nacos的配置文件类SdkPropertySourceLocator加载过程:
@Order(0)
public class SdkPropertySourceLocator implements PropertySourceLocator {
public SdkPropertySourceLocator() {
}
public PropertySource<?> locate(Environment environment) {
this.initNacosConfigProperties(environment);
return null;
}
@Bean
private NacosConfigProperties initNacosConfigProperties(Environment environment) {
NacosConfigProperties properties = new NacosConfigProperties();
properties.setServerAddr(environment.getProperty("CONFIGCENTER_ADDRESS"));
properties.setNamespace(environment.getProperty("CONFIGCENTER_TENANT_NAME"));
properties.setGroup(environment.getProperty("CONFIGCENTER_GROUP_NAME"));
List<Config> extConfig = new ArrayList();
Config config1 = new Config();
config1.setDataId("default_project.yml");
config1.setGroup("default_project");
config1.setRefresh(true);
extConfig.add(config1);
Config config2 = new Config();
config2.setDataId("application.yml");
config2.setGroup(environment.getProperty("CONFIGCENTER_GROUP_NAME"));
config2.setRefresh(true);
extConfig.add(config2);
Config config3 = new Config();
config3.setDataId("extra.yml");
config3.setGroup(environment.getProperty("CONFIGCENTER_GROUP_NAME"));
config3.setRefresh(true);
extConfig.add(config3);
properties.setExtConfig(extConfig);
return properties;
}
}
- 增加自动转配类ConfigCenterAutoConfiguration
@Configuration
@ConditionalOnClass({ConfigCenterClient.class})
public class ConfigCenterAutoConfiguration {
private static final Logger log = LoggerFactory.getLogger(ConfigCenterAutoConfiguration.class);
@Autowired
private NacosConfigProperties nacosConfigProperties;
public ConfigCenterAutoConfiguration() {
}
@Bean
public ConfigCenterClient configCenterClient() {
if (!StringUtils.isEmpty(this.nacosConfigProperties.getServerAddr())) {
return new NacosConfigCenterClient(this.nacosConfigProperties);
} else {
log.info("no config-center client find!");
return null;
}
}
}
- 增加配置中心客户端文件
public class NacosConfigCenterClient implements ConfigCenterClient {
private static final Logger log = LoggerFactory.getLogger(NacosConfigCenterClient.class);
private final ConfigService configService;
private final NacosConfigProperties configProperties;
private final Map<String, Listener> listenerMap = new HashMap();
public NacosConfigCenterClient(NacosConfigProperties configProperties) {
this.configService = configProperties.configServiceInstance();
this.configProperties = configProperties;
}
public synchronized boolean publishConfig(String key, String value) {
try {
if ("application.yml".equals(key)) {
log.error("publishConfig key cannot be \"application.yml\"!");
return false;
} else {
return this.configService.publishConfig(key, this.configProperties.getGroup(), value);
}
} catch (NacosException var4) {
log.error("publishConfig error, key:" + key + ", e:" + var4.getMessage(), var4);
return false;
}
}
public synchronized boolean deleteConfig(String key) {
try {
return this.configService.removeConfig(key, this.configProperties.getGroup());
} catch (NacosException var3) {
log.error("deleteConfig error, key:" + key + ", e:" + var3.getMessage(), var3);
return false;
}
}
public String getConfig(String key, String defaultValue) {
try {
String content = this.configService.getConfig(key, this.configProperties.getGroup(), 3000L);
return StringUtils.isEmpty(content) ? defaultValue : content;
} catch (NacosException var4) {
log.error("getConfig error, key:" + key + ", e:" + var4.getMessage(), var4);
return defaultValue;
}
}
public synchronized boolean updateSpringConfig(String key, String value) {
try {
String configs = this.getConfig("application.yml", "");
if (StringUtils.isEmpty(configs)) {
log.info("updateSpringConfig can't find key:" + key);
return false;
} else {
Map<String, Object> propertyMap = (Map)(new Yaml()).load(configs);
if (!CollectionUtils.isEmpty(propertyMap) && propertyMap.containsKey(key)) {
propertyMap.put(key, value);
return this.configService.publishConfig("application.yml", this.configProperties.getGroup(), map2Yaml(propertyMap));
} else {
log.info("updateSpringConfig can't find key:" + key);
return true;
}
}
} catch (Exception var5) {
log.error("updateSpringConfig error, key:" + key + ", e:" + var5.getMessage(), var5);
return false;
}
}
public boolean saveSpringConfig(String key, String value) {
try {
String configs = this.getConfig("application.yml", "");
if (StringUtils.isEmpty(configs)) {
log.info("saveSpringConfig can't find key:application.yml");
return false;
} else {
Map<String, Object> propertyMap = (Map)(new Yaml()).load(configs);
if (propertyMap == null) {
propertyMap = new HashMap();
}
((Map)propertyMap).put(key, value);
return this.configService.publishConfig("application.yml", this.configProperties.getGroup(), map2Yaml((Map)propertyMap));
}
} catch (Exception var5) {
log.error("saveSpringConfig error, key:" + key + ", e:" + var5.getMessage(), var5);
return false;
}
}
public void addListener(String key, io.terminus.common.configcenter.component.Listener listener) {
if ("application.yml".equals(key)) {
log.error("addListener key cannot be \"application.yml\"!");
} else {
try {
this.configService.addListener(key, this.configProperties.getGroup(), this.getListener(listener));
} catch (NacosException var4) {
log.error("addListener error, key:" + key + ", e:" + var4.getMessage(), var4);
}
}
}
public void removeListener(String key, io.terminus.common.configcenter.component.Listener listener) {
this.configService.removeListener(key, this.configProperties.getGroup(), this.getListener(listener));
}
private static String map2Yaml(Map<String, Object> map) {
if (CollectionUtils.isEmpty(map)) {
return "##";
} else {
StringBuilder sb = new StringBuilder();
sb.append("##");
map.entrySet().forEach((entry) -> {
sb.append("\n" + String.valueOf(entry.getKey()) + ": " + entry.getValue());
});
return sb.toString();
}
}
private Listener getListener(final io.terminus.common.configcenter.component.Listener listener) {
String hashCode = String.valueOf(listener.hashCode());
if (this.listenerMap.containsKey(hashCode)) {
return (Listener)this.listenerMap.get(hashCode);
} else {
Listener nacosListener = new Listener() {
public void receiveConfigInfo(String configInfo) {
listener.receiveConfigInfo(configInfo);
}
public Executor getExecutor() {
return listener.getExecutor();
}
};
this.listenerMap.put(hashCode, nacosListener);
return nacosListener;
}
}
public boolean saveSpringConfig(Map<String, String> paramMap) {
try {
String configs = this.getConfig("application.yml", "");
if (StringUtils.isEmpty(configs)) {
log.info("saveSpringConfig can't find key:application.yml");
return false;
} else {
Map<String, Object> propertyMap = (Map)(new Yaml()).load(configs);
if (propertyMap == null) {
propertyMap = new HashMap();
}
((Map)propertyMap).putAll(paramMap);
return this.configService.publishConfig("application.yml", this.configProperties.getGroup(), map2Yaml((Map)propertyMap));
}
} catch (Exception var4) {
log.error("saveSpringConfig error, e:" + var4.getMessage(), var4);
return false;
}
}
}
- 增加spring.factories配置文件
org.springframework.boot.autoconfigure.EnableAutoConfiguration = io.terminus.common.configcenter.autoconfigure.ConfigCenterAutoConfiguration
org.springframework.cloud.bootstrap.BootstrapConfiguration = io.terminus.common.configcenter.autoconfigure.SdkPropertySourceLocator
- 原理说明
主要实现原理就是在NacosConfigCenterClient类中,给当前的configservice注册增加了一个监听器,文件发生变化就会通知到监听器,然后进行相应的处理
public void addListener(String key, io.terminus.common.configcenter.component.Listener listener) {
if ("application.yml".equals(key)) {
log.error("addListener key cannot be \"application.yml\"!");
} else {
try {
this.configService.addListener(key, this.configProperties.getGroup(), this.getListener(listener));
} catch (NacosException var4) {
log.error("addListener error, key:" + key + ", e:" + var4.getMessage(), var4);
}
}
}