随着业务的发展、微服务架构的升级,服务的数量、程序的配置日益增多(各种微服务、各种服务器地址、各种参数),传统的配置文件方式和数据库的方式已无法满足开发人员对配置管理的要求:配置修改后实时生效,灰度发布,分环境、分集群管理配置,完善的权限、审核机制。分布式环境下,这些配置更加复杂。
因此,我们需要配置中心来统一管理配置!把业务开发者从复杂以及繁琐的配置中解脱出来,只需专注于业务代码本身,从而能够显著提升开发以及运维效率。同时将配置和发布包解藕也进一步提升发布的成功率,并为运维的细力度管控、应急处理等提供强有力的支持。
在之前的文章中,我们介绍过 Spring Cloud 中的分布式配置中心组件:Spring Cloud Config。本文将会介绍功能更为强大的 Apollo。
在一个分布式环境中,同类型的服务往往会部署很多实例。这些实例使用了一些配置,为了更好地维护这些配置就产生了配置管理服务。通过这个服务可以轻松地管理成千上百个服务实例的配置问题。配置中心的特点:
现有的配置中心组件有:Spring Cloud Config、Apollo、Disconf、Diamond 等等,这些组件在功能上有或多或少的差异,但是都具有基本的配置中心的功能。
Apollo(阿波罗)是携程框架部门研发的开源配置管理中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性。目前的有超过 14k 的 star,使用广泛。Apollo基于开源模式开发,开源地址:https://github.com/ctripcorp/apollo。
首先用户在配置中心对配置进行修改并发布;配置中心通知Apollo客户端有配置更新;Apollo客户端从配置中心拉取最新的配置、更新本地配置并通知到应用。
Apollo 支持4个维度管理 Key-Value 格式的配置:
我们在集成 Apollo 时,可以根据需要创建相应的维度。
下面我们搭建一个基于 Spring Boot 的微服务,集成 Apollo。
Apollo配置中心包括:Config Service、Admin Service 和 Portal。
官网准备好了一个Quick Start安装包,大家只需要下载到本地,就可以直接使用,免去了编译、打包过程。也可以自行编译,较为繁琐。
Apollo服务端共需要两个数据库:ApolloPortalDB和ApolloConfigDB。创建的语句见安装包,创建好之后需要配置启动的脚本,即 demo.sh
脚本:
#apollo config db info
apollo_config_db_url=jdbc:mysql://localhost:3306/ApolloConfigDB?characterEncoding=utf8
apollo_config_db_username=用户名
apollo_config_db_password=密码(如果没有密码,留空即可)
# apollo portal db info
apollo_portal_db_url=jdbc:mysql://localhost:3306/ApolloPortalDB?characterEncoding=utf8
apollo_portal_db_username=用户名
apollo_portal_db_password=密码(如果没有密码,留空即可)
脚本会在本地启动3个服务,分别使用8070, 8080, 8090端口,请确保这3个端口当前没有被使用。执行
./demo.sh start
看到输出如下的日志信息:
==== starting service ====
Service logging file is ./service/apollo-service.log
Started [10768]
Waiting for config service startup.......
Config service started. You may visit http://localhost:8080 for service status now!
Waiting for admin service startup....
Admin service started
==== starting portal ====
Portal logging file is ./portal/apollo-portal.log
Started [10846]
Waiting for portal startup......
Portal started. You can visit http://localhost:8070 now!
Apollo 服务端启动成功。
搭建好 Apollo 服务器之后,接下来将我们的应用接入 Apollo。
<dependency>
<groupId>com.ctrip.framework.apollogroupId>
<artifactId>apollo-clientartifactId>
<version>1.1.0version>
dependency>
在依赖中只需要增加 apollo-client
的引用。
@SpringBootApplication
@EnableApolloConfig("TEST1.product")
public class ApolloApplication {
public static void main(String[] args) {
SpringApplication.run(ApolloApplication.class, args);
}
}
我们通过 @EnableApolloConfig("TEST1.product")
注解开启注册到 Apollo 服务端,并指定了 namespace 为 TEST1.product
。
app.id: spring-boot-logger
# set apollo meta server address, adjust to actual address if necessary
apollo.meta: http://localhost:8080
server:
port: 0
配置文件中指定了appid 和 Apollo 服务器的地址。
我们通过动态设置输出的日志等级来测试接入的配置中心。
@Service
public class LoggerConfiguration {
private static final Logger logger = LoggerFactory.getLogger(LoggerConfiguration.class);
private static final String LOGGER_TAG = "logging.level.";
@Autowired
private LoggingSystem loggingSystem;
@ApolloConfig
private Config config;
@ApolloConfigChangeListener
// 监听 Apollo 配置中心的刷新事件
private void onChange(ConfigChangeEvent changeEvent) {
refreshLoggingLevels();
}
@PostConstruct
// 设置刷新之后的日志级别
private void refreshLoggingLevels() {
Set<String> keyNames = config.getPropertyNames();
for (String key : keyNames) {
if (containsIgnoreCase(key, LOGGER_TAG)) {
String strLevel = config.getProperty(key, "info");
LogLevel level = LogLevel.valueOf(strLevel.toUpperCase());
loggingSystem.setLogLevel(key.replace(LOGGER_TAG, ""), level);
logger.info("{}:{}", key, strLevel);
}
}
}
private static boolean containsIgnoreCase(String str, String searchStr) {
if (str == null || searchStr == null) {
return false;
}
int len = searchStr.length();
int max = str.length() - len;
for (int i = 0; i <= max; i++) {
if (str.regionMatches(true, i, searchStr, 0, len)) {
return true;
}
}
return false;
}
}
如上的配置类用于根据 Apollo 配置中心的日志等级配置,设置本地服务的日志等级,并监听刷新事件,将刷新后的配置及时应用到本地服务,其中 @PostConstruct
注解用于在完成依赖项注入以执行任何初始化之后需要执行的方法。
@Service
public class PrintLogger {
private static Logger logger = LoggerFactory.getLogger(PrintLogger.class);
@ApolloJsonValue("${kk.v}")
private String v;
@PostConstruct
public void printLogger() throws Exception {
Executors.newSingleThreadExecutor().submit(() -> {
while (true) {
logger.error("=========" + v);
logger.info("我是info级别日志");
logger.error("我是error级别日志");
logger.warn("我是warn级别日志");
logger.debug("我是debug级别日志");
TimeUnit.SECONDS.sleep(1);
}
});
}
}
起一个线程,输出不同级别的日志。根据配置的日志等级,过滤后再打印。我们在如上的程序中,还自定义了一个字段,同样用以测试随机打印最新的值。
我们在 Apollo 的配置界面中,增加如下的配置:
并将配置发布,启动我们本地的 SpringBoot 服务:
2019-05-28 20:31:36.688 ERROR 44132 --- [pool-1-thread-1] com.blueskykong.apollo.PrintLogger : =========log-is-error-level.
2019-05-28 20:31:36.688 ERROR 44132 --- [pool-1-thread-1] com.blueskykong.apollo.PrintLogger : 我是error级别日志
将编辑好的配置发布,应用服务将会收到刷新事件。
可以看到,服务刷新了日志的级别,打印 warn 的日志信息。
2019-05-28 20:35:56.819 WARN 44132 --- [pool-1-thread-1] com.blueskykong.apollo.PrintLogger : 我是warn级别日志
2019-05-28 20:36:06.823 ERROR 44132 --- [pool-1-thread-1] com.blueskykong.apollo.PrintLogger : =========log-is-warn-level.
在体验了 Apollo 作为配置中心之后,我们将了解下 Apollo 的总体设计和实现的原理。
上图简要描述了 Apollo 的总体设计,从下往上看:
ConfigService、AdminService、Portal 属于 Apollo 服务端的模块,其中提到的 Eureka 是为了保证高可用,Config 和 Admin 都是无状态以集群方式部署的,Client 怎么找到 Config?Portal 怎么找到 Admin?为了解决这个问题,Apollo在其架构中引入了Eureka服务注册中心组件,实现微服务间的服务注册和发现用于服务发现和注册,Config 和 Admin Service注册实例并定期报心跳, Eureka 与 ConfigService 一起部署。
MetaServer 其实是一个Eureka的Proxy,将Eureka的服务发现接口以更简单明确的HTTP接口的形式暴露出来,方便 Client/Protal 通过简单的 HTTPClient 就可以查询到 Config/Admin 的地址列表。获取到服务实例地址列表之后,再以简单的客户端软负载(Client SLB)策略路由定位到目标实例,并发起调用。
在配置中心中,一个重要的功能就是配置发布后实时推送到客户端。下面我们简要看一下这块是怎么设计实现的。
上图简要描述了配置发布的大致过程:用户在Portal操作配置发布;Portal调用Admin Service的接口操作发布;Admin Service发布配置后,发送ReleaseMessage给各个Config Service;Config Service收到ReleaseMessage后,通知对应的客户端。
如何通知客户端呢?我们看到 Apollo 的实现步骤如下:
本文首先介绍分布式配置中心的概念和 Apollo 接入的实践,然后深入介绍了 Apollo 的总体架构和实现的一些细节。总得来说, Apollo 是现有配置中心组件中,功能最全的一个。能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性,适用于微服务配置管理场景。
本文对应的代码地址: https://github.com/keets2012/Spring-Boot-Samples/tree/master/apollo-demo