Apollo(阿波罗)是携程框架部门研发的分布式配置中心,能够集中化管理应用的不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性,适用于微服务配置管理场景。
Apollo包括服务端和客户端两部分:
服务端基于Spring Boot和Spring Cloud开发,打包后可以直接运行,不需要额外安装Tomcat等应用容器。
Java客户端不依赖任何框架,能够运行于所有Java运行时环境,同时对Spring/Spring Boot环境也有较好的支持。
github:https://github.com/ctripcorp/apollo
基于配置的特殊性,所以Apollo从设计之初就立志于成为一个有治理能力的配置发布平台,目前提供了以下的特性:
统一管理不同环境、不同集群的配置
提供了一个统一界面集中式管理不同环境(environment)、不同集群(cluster)、不同命名空间(namespace)的配置。
同一份代码部署在不同的集群,可以有不同的配置,比如zookeeper的地址等
通过命名空间(namespace)可以很方便地支持多个不同应用共享同一份配置,同时还允许应用对共享的配置进行覆盖
配置修改实时生效(热发布)
用户在Apollo修改完配置并发布后,客户端能实时(1秒)接收到最新的配置,并通知到应用程序
版本发布管理
所有的配置发布都有版本概念,从而可以方便地支持配置的回滚
灰度发布
支持配置的灰度发布,比如点了发布后,只对部分应用实例生效,等观察一段时间没问题后再推给所有应用实例
权限管理、发布审核、操作审计
应用和配置的管理都有完善的权限管理机制,对配置的管理还分为了编辑和发布两个环节,从而减少人为的错误。
所有的操作都有审计日志,可以方便地追踪问题
客户端配置信息监控
可以在界面上方便地看到配置在被哪些实例使用
提供Java和.Net原生客户端
提供了Java和.Net的原生客户端,方便应用集成
支持Spring Placeholder, Annotation和Spring Boot的ConfigurationProperties,方便应用使用(需要Spring 3.1.1+)
同时提供了Http接口,非Java和.Net应用也可以方便地使用
提供开放平台API
Apollo自身提供了比较完善的统一配置管理界面,支持多环境、多数据中心配置管理、权限、流程治理等特性。不过Apollo出于通用性考虑,不会对配置的修改做过多限制,只要符合基本的格式就能保存,不会针对不同的配置值进行针对性的校验,如数据库用户名、密码,Redis服务地址等
对于这类应用配置,Apollo支持应用方通过开放平台API在Apollo进行配置的修改和发布,并且具备完善的授权和权限控制
操作流程如下:
2)、应用程序通过Apollo客户端从配置中心拉取配置信息
用户通过Apollo配置中心修改或发布配置后,会有两种机制来保证应用程序来获取最新配置:一种是Apollo配置中心会向客户端推送最新的配置;另外一种是Apollo客户端会定时从Apollo配置中心拉取最新的配置,通过以上两种机制共同来保证应用程序能及时获取到配置。
运行时环境:
Java
Apollo服务端:1.8+
Apollo客户端:1.7+
由于需要同时运行服务端和客户端,所以建议安装Java 1.8+。
MySQL
版本要求:5.6.5+
Apollo的表结构对timestamp使用了多个default声明,所以需要5.6.5以上版本。
访问Apollo的官方主页获取安装包(本次使用1.3版本):https://github.com/ctripcorp/apollo/tags
打开1.3发布链接,下载必须的安装包:https://github.com/ctripcorp/apollo/releases/tag/v1.3.0
解压安装包后将apollo-configservice-1.3.0.jar, apollo-adminservice-1.3.0.jar, apollo-portal-1.3.0.jar放置于apollo目录下
Apollo服务端共需要两个数据库:ApolloPortalDB和ApolloConfigDB,ApolloPortalDB只需要在生产环境部署一个即可,而ApolloConfigDB需要在每个环境部署一套。
a)创建ApolloPortalDB,sql脚本下载地址:https://github.com/ctripcorp/apollo/blob/v1.3.0/scripts/db/migration/configdb/V1.0.0__initialization.sql
以MySQL原生客户端为例:source apollo/ApolloPortalDB__initialization.sql
验证ApolloPortalDB:导入成功后,可以通过执行以下sql语句来验证:
select Id
, Key
, Value
, Comment
from ApolloPortalDB
.ServerConfig
limit 1;
注:ApolloPortalDB只需要在生产环境部署一个即可
b)创建ApolloConfigDB,sql脚本下载地址:https://github.com/ctripcorp/apollo/blob/v1.3.0/scripts/db/migration/configdb/V1.0.0__initialization.sql
以MySQL原生客户端为例: source apollo/ApolloConfigDB__initialization.sql
验证ApolloConfigDB:导入成功后,可以通过执行以下sql语句来验证:
select `Id`, `Key`, `Value`, `Comment` from `ApolloConfigDB`.`ServerConfig` limit 1;
在bin目录下创建批量启动脚本runApollo.bat
-Dserver.port=7070 修改默认端口
configService:7070, adminService:7080, ApolloPortal:7090
注意修改ApolloPortal中的 -Ddev_meta=http://localhost:7070
echo
set url="localhost"
set username="root"
set password="root"
start "configService" java -Xms256m -Xmx256m -Dserver.port=7070 -Dapollo_profile=github -Dspring.datasource.url=jdbc:mysql://%url%/ApolloConfigDB?characterEncoding=utf8 -Dspring.datasource.username=%username% -Dspring.datasource.password=%password% -Dlogging.file=.\logs\apollo-configservice.log -jar .\apollo-configservice-1.3.0.jar
start "adminService" java -Xms256m -Xmx256m -Dserver.port=7080 -Dapollo_profile=github -Dspring.datasource.url=jdbc:mysql://%url%/ApolloConfigDB?characterEncoding=utf8 -Dspring.datasource.username=%username% -Dspring.datasource.password=%password% -Dlogging.file=.\logs\apollo-adminservice.log -jar .\apollo-adminservice-1.3.0.jar
start "ApolloPortal" java -Xms256m -Xmx256m -Dapollo_profile=github,auth -Ddev_meta=http://localhost:7070/ -Dserver.port=7090 -Dspring.datasource.url=jdbc:mysql://%url%/ApolloPortalDB?characterEncoding=utf8 -Dspring.datasource.username=%username% -Dspring.datasource.password=%password% -Dlogging.file=.\logs\apollo-portal.log -jar .\apollo-portal-1.3.0.jar
**修改ApolloConfigDB库中的ServerConfig表,将地址修改为configService的地址。**这步很重要,否则启动会一直报错
访问ApolloPortal的地址:http://localhost:7090/ 账号默认为apollo admin
创建apollo-config-center-6100
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>com.ctrip.framework.apollogroupId>
<artifactId>apollo-clientartifactId>
<version>1.5.0version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-contextartifactId>
<version>2.2.7.RELEASEversion>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-datasource-apolloartifactId>
<version>1.6.3version>
dependency>
dependencies>
server:
port: 6100
app:
id: apollo-config-center-6100 # 应用的身份信息,同apollo后台配置的AppId
apollo:
meta: http://127.0.0.1:7070
cacheDir: /opt/data/apollo‐config # 指定配置文件的本地缓存路径
bootstrap:
enabled: true # 在应用启动阶段,向spring容器注入被托管的application.properties文件的配置信息
eagerLoad:
enabled: true # 将apollo配置加载提前到初始化日志系统之前
namespaces: application # 指定namespace
dev:
meta: http://localhost:7070\
env: DEV
spring:
application:
name: apollo-config-center-6100
cloud:
sentinel:
transport:
dashboard: localhost:8080 #配置Sentinel dashboard地址
port: 8719
management:
endpoints:
web:
exposure:
include: '*'
启动类:添加 @EnableApolloConfig 注解启用Apollo
@SpringBootApplication
@EnableApolloConfig
@Slf4j
public class ApolloMain6100 {
public static void main(String[] args) {
SpringApplication.run(ApolloMain6100.class,args);
log.info("==========ApolloMain6100启动成功===============");
}
}
业务类:
@RestController
@Slf4j
public class ApolloController {
@Value("${test.url}")
private String url;
@Resource
private SystemConfig systemConfig;
@GetMapping("/test/apollo")
public String getUrl(){
log.info("url:"+url);
log.info("systemConfig.name:"+systemConfig.getName());
return systemConfig.getName();
}
@GetMapping("/testB")
@SentinelResource(value = "testB",fallbackClass = FlowLimitFallback.class,fallback = "testB")
public String testB() {
// int a = 10/0;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "------testB";
}
}
@ConfigurationProperties(prefix = "system")
@Component
@Data
public class SystemConfig {
private String name;
}
public class FlowLimitFallback {
public static String testB() {
return "服务不可用";
}
}
test.url = http://localhost:6662
修改其值,发现可以动态更新。
但是对于bean中的值,apollo无法直接做到动态更新。需要新增配置类。
导入springcloud的依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-contextartifactId>
<version>2.2.7.RELEASEversion>
dependency>
@Slf4j
@Component
public class ApolloConfigChanged implements ApplicationContextAware {
@Resource
private ApplicationContext applicationContext;
@ApolloConfigChangeListener("application.properties")
private void apolloChangedHandler(ConfigChangeEvent event){
Set<String> set = event.changedKeys();
for (String key : set) {
ConfigChange change = event.getChange(key);
log.info("application.properties changed:"+change.toString());
this.applicationContext.publishEvent(new EnvironmentChangeEvent(set));
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
导入依赖:
血泪教训:sentinel-datasource-apollo的版本需要和spring-cloud-starter-alibaba-sentinel中对应jar包版本一致,否则可能导致流控规则无法注入。
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-datasource-apolloartifactId>
<version>1.6.3version>
dependency>
@Component
@Slf4j
public class ApolloSentinelRuleConfig implements InitializingBean {
@ApolloConfig
private Config config;
@Override
public void afterPropertiesSet() throws Exception {
// System.setProperty("csp.sentinel.log.dir","logs/csp/");
String namespace = "application.properties";
Set<String> propertyNames = config.getPropertyNames();
for (String propertyName : propertyNames) {
if(propertyName.contains("sentinel")){
log.info("限流规则发生改变:"+propertyName);
String[] split = propertyName.split("\\.");
loadParam(namespace,split[2],propertyName);
}
}
}
private void loadParam(String namespace,String ruleType,String ruleKey){
switch (ruleType){
case "flow":
ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new ApolloDataSource<>(namespace,
ruleKey, "[]", source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {
}));
log.info("namespace:{},rule:{}",namespace,flowRuleDataSource.getProperty().toString());
FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
break;
case "degrade":
ReadableDataSource<String, List<DegradeRule>> degradeRuleDataSource = new ApolloDataSource<>(namespace,
ruleKey, "[]", source -> JSON.parseObject(source, new TypeReference<List<DegradeRule>>() {
}));
log.info("namespace:{},rule:{}",namespace,degradeRuleDataSource.getProperty());
DegradeRuleManager.register2Property(degradeRuleDataSource.getProperty());
break;
case "param":
ReadableDataSource<String, List<ParamFlowRule>> paramFlowRuleDataSource = new ApolloDataSource<>(namespace,
ruleKey, "[]", source -> JSON.parseObject(source, new TypeReference<List<ParamFlowRule>>() {
}));
log.info("namespace:{},rule:{}",namespace,paramFlowRuleDataSource.getProperty());
ParamFlowRuleManager.register2Property(paramFlowRuleDataSource.getProperty());
break;
}
}
}
以上实现参考官方文档:https://github.com/alibaba/Sentinel/wiki/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%99%E6%89%A9%E5%B1%95
在apollo新增配置:
sentinel.rule.flow = [{“resource”: “testB”,“grade”: 1,“count”: 2}]
启动apollo,启动sentinel,启动6100。
登录sentinel后台,请求http://localhost:6100/testB,刷新sentinel,发现限流规则已经注入。快速刷新testB,发现流控生效。去apollo后台更新流控规则,发现sentinel后台也同步更新。
应用场景:Apollo是基于AppID来区分不同实例配置,那如何在不改变AppID的情况下使用不同的配置实例呢?那就可以使用Apollo集群
目前需求:通过docker搭建一个项目的多个实例,而项目的配置是依赖于Apollo的,在不改项目AppID的情况下使得每个实例使用不同的Apollo配置。
配置步骤:
1)登录Apollo配置页面,添加集群
添加一个集群,eg:test1 然后选择项目环境,eg:pro
2)修改项目 server.properties 配置文件,添加如下配置
env=pro
idc=test1
使用pro环境,集群为test1,这样此项目实例就会去读取test1集群的配置
sudo docker run -dit -m 3G --net=host --privileged=true --cap-add=SYS_PTRACE --name=study-spring-test -v /usr/java/jdk1.8.0_191:/usr/java/jdk1.8.0_191 -v /TEST/study-spring-test/logs:/TEST/study-spring-test/logs -v /opt:/opt -e JAVA_MAJOR_VERSION=8 -e JAVA_INIT_MEM_RATIO=25 -e ENV= -e APOLLO_META= -e IDC=test1 docker.study.net/test/study-spring-test:TSET_V9.1.24_20200807_B01;
Config Service提供配置的读取、推送等功能,服务对象是Apollo客户端
Admin Service提供配置的修改、发布等功能,服务对象是Apollo Portal(管理界面)
Eureka提供服务注册和发现,为了简单起见,目前Eureka在部署时和Config Service是在一个JVM进程中的
Config Service和Admin Service都是多实例、无状态部署,所以需要将自己注册到Eureka中并保持心跳
在Eureka之上架了一层Meta Server用于封装Eureka的服务发现接口
Client通过域名访问Meta Server获取Config Service服务列表(IP+Port),而后直接通过IP+Port访问服务, 同时在Client侧会做load balance、错误重试
Portal通过域名访问Meta Server获取Admin Service服务列表(IP+Port),而后直接通过IP+Port访问服务, 同时在Portal侧会做load balance、错误重试
为了简化部署,我们实际上会把Config Service、Eureka和Meta Server三个逻辑角色部署在同一个JVM进程 中
1)Apollo启动后,Config/Admin Service会自动注册到Eureka服务注册中心,并定期发送保活心跳。
Apollo Client和Portal管理端通过配置的Meta Server的域名地址经由Software Load Balancer(软件负载均衡器)进行负载均衡后分配到某一个Meta Server
Meta Server从Eureka获取Config Service和Admin Service的服务信息,相当于是一个Eureka Client
4.)Meta Server获取Config Service和Admin Service(IP+Port)失败后会进行重试
1)application (应用) 这个很好理解,就是实际使用配置的应用,Apollo客户端在运行时需要知道当前应用是谁,从而可以去获取 对应的配置 关键字:appId
2)environment (环境) 配置对应的环境,Apollo客户端在运行时需要知道当前应用处于哪个环境,从而可以去获取应用的配置 关键字:env
3)cluster (集群) 一个应用下不同实例的分组,比如典型的可以按照数据中心分,把上海机房的应用实例分为一个集群,把北京机房的应用实例分为另一个集群。 关键字:cluster
4)namespace (命名空间) 一个应用下不同配置的分组,可以简单地把namespace类比为文件,不同类型的配置存放在不同的文件中, 如数据库配置文件,RPC配置文件,应用自身的配置文件等 关键字:namespaces
Admin Service在配置发布后,需要通知所有的Config Service有配置发布,从而Config Service可以通知对应的客户端来拉取最新的配置。 从概念上来看,这是一个典型的消息使用场景,Admin Service作为producer(生产者)发出消息,各个Config Service作为consumer(消费者)消费消息。通过一个消息队列组件(Message Queue)就能很好的实现Admin Service和Config Service的解耦。 在实现上,考虑到Apollo的实际使用场景,以及为了尽可能减少外部依赖,我们没有采用外部的消息中间件,而是 通过数据库实现了一个简单的消息队列。
具体实现方式如下:
1).Admin Service在配置发布后会往ReleaseMessage表插入一条消息记录,消息内容就是配置发布的 AppId+Cluster+Namespace
SELECT * FROM ApolloConfigDB.ReleaseMessage
2).Config Service有一个线程会每秒扫描一次ReleaseMessage表,看看是否有新的消息记录
3).Config Service如果发现有新的消息记录,那么就会通知到所有的消息监听器
4).NotificationControllerV2得到配置发布的AppId+Cluster+Namespace后,会通知对应的客户端
实现方式如下:
1).客户端会发起一个Http请求到Config Service的 notifications/v2 接口NotificationControllerV2
2).NotificationControllerV2不会立即返回结果,而是把请求挂起。考虑到会有数万客户端向服务端发起长连, 因此在服务端使用了async servlet(Spring DeferredResult)来服务Http Long Polling请求。
3).如果在60秒内没有该客户端关心的配置发布,那么会返回Http状态码304给客户端。
4).如果有该客户端关心的配置发布,NotificationControllerV2会调用DeferredResult的setResult方法,传入有 配置变化的namespace信息,同时该请求会立即返回。客户端从返回的结果中获取到配置变化的namespace 后,会立即请求Config Service获取该namespace的最新配置。
除了之前介绍的客户端和服务端保持一个长连接,从而能第一时间获得配置更新的推送外,客户端还会定时从 Apollo配置中心服务端拉取应用的最新配置。 这是一个备用机制,为了防止推送机制失效导致配置不更新
客户端定时拉取会上报本地版本,所以一般情况下,对于定时拉取的操作,服务端都会返回304 - Not Modified 定时频率默认为每5分钟拉取一次,客户端也可以通过在运行时指定System Property: apollo.refreshInterval 来覆盖,单位为分钟