参考:appollo官方
文章目录
- 如何启动源码
- 一、apollo整体架构
- 二、portal service发布配置后,如何通知client
- 1、配置发布后的实时推送过程
- 步骤一、用户在Portal操作配置发布
- 步骤二、Portal调用Admin Service的接口操作发布
- 步骤三、Admin Service发布配置后,发送ReleaseMessage给各个Config Service
- 步骤四、Config Service收到ReleaseMessage后,通知对应的客户端
- Config Service通知客户端的实现方式:
- 1、客户端会发起一个Http请求到Config Service的notifications/v2接口,也就是NotificationControllerV2,参见RemoteConfigLongPollService
- 2、NotificationControllerV2不会立即返回结果,而是通过Spring DeferredResult把请求挂起
- 3、如果在60秒内没有该客户端关心的配置发布,那么会返回Http状态码304给客户端
- 4、如果有该客户端关心的配置发布,NotificationControllerV2会调用DeferredResult的setResult方法,传入有配置变化的namespace信息,同时该请求会立即返回。客户端从返回的结果中获取到配置变化的namespace后,会立即请求Config Service获取该namespace的最新配置。
- 三、客户端监听配置原理
- 1、客户端和服务端保持了一个长连接,从而能第一时间获得配置更新的推送。(通过Http Long Polling实现)
- 2、客户端还会定时从Apollo配置中心服务端拉取应用的最新配置。
- 3、客户端从Apollo配置中心服务端获取到应用的最新配置后,会保存在内存中
- 4、客户端会把从服务端获取到的配置在本地文件系统缓存一份
- 5、应用程序可以从Apollo客户端获取最新的配置、订阅配置更新通知
- 四、整个流程
- 1、通知客户端配置有更新:是通过 deferredResults 这个对象实现的。
- 2、核心方法如下:
如何启动源码
1、源码结构
2、初始化库
3、启动apollo-assembly
application启动配置如下:
VM options:
-Denv=dev
-Dspring.profiles.active=github
-Deureka.service.url=http://localhost:8080/eureka/
-Dspring.datasource.url=jdbc:mysql://localhost:3309/ApolloConfigDB?characterEncoding=utf8
-Dspring.datasource.username=root
-Dspring.datasource.password=123456
-Dlogging.file=D:/logs/apollo-assembly.log
Program arguments:
–configservice --adminservice
这些配置其实可以参考官方的项目apollo-build-scripts项目
这个项目有一个deom.sh脚本如下:不需要完全看懂所有意思,只需看重点如下:
# meta server url定义的一些变量
config_server_url=http://localhost:8080
admin_server_url=http://localhost:8090
eureka_service_url=$config_server_url/eureka/
portal_url=http://localhost:8070
# JAVA OPTS BASE_JAVA_OPTS:基础变量
BASE_JAVA_OPTS="-Denv=dev"
CLIENT_JAVA_OPTS="$BASE_JAVA_OPTS -Dapollo.meta=$config_server_url"
#config service和admin service所需的VM options配置参数
SERVER_JAVA_OPTS="$BASE_JAVA_OPTS -Dspring.profiles.active=github -Deureka.service.url=$eureka_service_url"
#portal service所需的VM options配置参数
PORTAL_JAVA_OPTS="$BASE_JAVA_OPTS -Ddev_meta=$config_server_url -Dspring.profiles.active=github,auth -Deureka.client.enabled=false -Dhibernate.query.plan_cache_max_size=192"
#启动config service和admin service时所带的Program arguments参数
$SERVICE_JAR start --configservice --adminservice
启动报错
注意apollo-assembly这个项目是将config 和admin一起启动的, 并且是先启动config 再启动admin。因为config这个服务中整合了eureka 服务端,所以要先把eureka服务启动,然后config 和admin再注册到eureka。
注:在启动apollo-configservice的过程中会在日志中输出eureka注册失败的信息,如com.sun.jersey.api.client.ClientHandlerException: java.net.ConnectException: Connection refused。需要注意的是,这个是预期的情况,因为apollo-configservice需要向Meta Server(它自己)注册服务,但是因为在启动过程中,自己还没起来,所以会报这个错。后面会进行重试的动作,所以等自己服务起来后就会注册正常了。
4、启动apollo-portal
application启动配置如下:
VM options:
-Denv=dev
-Ddev_meta=http://localhost:8080
-Dspring.profiles.active=github,auth
-Deureka.client.enabled=false
-Dhibernate.query.plan_cache_max_size=192
-Dserver.port=8170
-Dspring.datasource.url=jdbc:mysql://localhost:3309/ApolloPortalDB?characterEncoding=utf8
-Dspring.datasource.username=root
-Dspring.datasource.password=123456
-Dlogging.file=D:/logsh/apollo-portal.log
Program arguments:
–portal
这些配置也同样参考官方的项目apollo-build-scripts项目的deom.sh脚本如下:不需要完全看懂所有意思,只需看重点如下:
#portal service所需的VM options配置参数
PORTAL_JAVA_OPTS="$BASE_JAVA_OPTS -Ddev_meta=$config_server_url -Dspring.profiles.active=github,auth -Deureka.client.enabled=false -Dhibernate.query.plan_cache_max_size=192"
echo "==== starting portal ===="
echo "Portal logging file is $PORTAL_LOG"
# 导入VM options配置参数
export JAVA_OPTS="$PORTAL_JAVA_OPTS -Dlogging.file=./apollo-portal.log -Dserver.port=8070 -Dspring.datasource.url=$apollo_portal_db_url -Dspring.datasource.username=$apollo_portal_db_username -Dspring.datasource.password=$apollo_portal_db_password"
# 启动portal service 的参数
$PORTAL_JAR start --portal
一、apollo整体架构
apollo服务端大概有3个服务:
- Config Service提供配置的读取、推送等功能,服务对象是Apollo客户端
- Admin Service提供配置的修改、发布等功能,服务对象是Apollo Portal(管理界面)
- 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进程中
二、portal service发布配置后,如何通知client
1、配置发布后的实时推送过程
步骤一、用户在Portal操作配置发布
步骤二、Portal调用Admin Service的接口操作发布
步骤三、Admin Service发布配置后,发送ReleaseMessage给各个Config Service
Admin Service发送ReleaseMessage方式:
1、 Admin Service在配置发布后会往ReleaseMessage表插入一条消息记录,消息内容就是配置发布的AppId+Cluster+Namespace,参见DatabaseMessageSender
2、Config Service有一个线程会每秒扫描一次ReleaseMessage表,看看是否有新的消息记录,参见ReleaseMessageScanner
3、Config Service如果发现有新的消息记录,那么就会通知到所有的消息监听器(ReleaseMessageListener),如NotificationControllerV2,消息监听器的注册过程参见ConfigServiceAutoConfiguration
4、NotificationControllerV2得到配置发布的AppId+Cluster+Namespace后,会通知对应的客户端
步骤四、Config Service收到ReleaseMessage后,通知对应的客户端
Config Service通知客户端的实现方式:
1、客户端会发起一个Http请求到Config Service的notifications/v2接口,也就是NotificationControllerV2,参见RemoteConfigLongPollService
2、NotificationControllerV2不会立即返回结果,而是通过Spring DeferredResult把请求挂起
3、如果在60秒内没有该客户端关心的配置发布,那么会返回Http状态码304给客户端
4、如果有该客户端关心的配置发布,NotificationControllerV2会调用DeferredResult的setResult方法,传入有配置变化的namespace信息,同时该请求会立即返回。客户端从返回的结果中获取到配置变化的namespace后,会立即请求Config Service获取该namespace的最新配置。
三、客户端监听配置原理
1、客户端和服务端保持了一个长连接,从而能第一时间获得配置更新的推送。(通过Http Long Polling实现)
2、客户端还会定时从Apollo配置中心服务端拉取应用的最新配置。
- 这是一个fallback机制,为了防止推送机制失效导致配置不更新
- 客户端定时拉取会上报本地版本,所以一般情况下,对于定时拉取的操作,服务端都会返回304 - Not Modified
- 定时频率默认为每5分钟拉取一次,客户端也可以通过在运行时指定System Property: apollo.refreshInterval来覆盖,单位为分钟。
3、客户端从Apollo配置中心服务端获取到应用的最新配置后,会保存在内存中
4、客户端会把从服务端获取到的配置在本地文件系统缓存一份
- 在遇到服务不可用,或网络不通的时候,依然能从本地恢复配置
5、应用程序可以从Apollo客户端获取最新的配置、订阅配置更新通知
四、整个流程
1、通知客户端配置有更新:是通过 deferredResults 这个对象实现的。
deferredResults 对象被定成NotificationControllerV2类的成员变量。
客户端会发起一个长连接到config service服务,url对应了NotificationControllerV2.#pollNotification 方法,并返回DeferredResult对象, 返回这个对象 ,那么请求并不会立即返回,直到DeferredResult.setResult 被设置值 ,请求才会返回。
那么这个对象DeferredResult.setResult 何时调用?
- 当配置发布—》
- 插入消息表----》
- config service 定时描消息表-----》
- 有新的发布消息-----》
- 调用监听方法NotificationControllerV2.#handleMessage-----》
- 设置DeferredResult.setResult ------->
- 客户端调用的NotificationControllerV2.#pollNotification方法------》
- 感受到DeferredResult.setResult 设置,立即返回,否则60s没有配置更新 ,那么超时返回304
2、核心方法如下:
@RestController
@RequestMapping("/notifications/v2")
public class NotificationControllerV2 implements ReleaseMessageListener {
//定义deferredResults 成员变量,设置成同步集合,防止并发
//DeferredResultWrapper类型是DeferredResult的包装类,里面持有DeferredResult类型的变量
private final Multimap<String, DeferredResultWrapper> deferredResults =
Multimaps.synchronizedSetMultimap(TreeMultimap.create(String.CASE_INSENSITIVE_ORDER, Ordering.natural()));
}
//客户端定时获取 最新配置url方法
@GetMapping
public DeferredResult<ResponseEntity<List<ApolloConfigNotification>>> pollNotification(
@RequestParam(value = "appId") String appId,
@RequestParam(value = "cluster") String cluster,
@RequestParam(value = "notifications") String notificationsAsString,
@RequestParam(value = "dataCenter", required = false) String dataCenter,
@RequestParam(value = "ip", required = false) String clientIp) {
// 解析 notificationsAsString 参数,创建 ApolloConfigNotification 数组。
List<ApolloConfigNotification> notifications = null;
//创建deferredResultWrapper 对象
DeferredResultWrapper deferredResultWrapper = new DeferredResultWrapper(bizConfig.longPollingTimeoutInMilli());
deferredResultWrapper.onCompletion(() -> {
// 当这个对象resultWrapper完成setReuslt设置, 注销已经返回的监听
for (String key : watchedKeys) {
deferredResults.remove(key, deferredResultWrapper);
}
});
//注册DeferredResultWrapper 到 `deferredResults` 中,等待配置发生变化后通知。详见 `#handleMessage(...)` 方法。
for (String key : watchedKeys) {
this.deferredResults.put(key, deferredResultWrapper);
}
//返回DeferredResult对象,直到#handleMessage监听方法中设置了值返回
//或等待超时返回304
return deferredResultWrapper.getResult();
}
//配置发布后的通知方法
@Override
public void handleMessage(ReleaseMessage message, String channel) {
logger.info("message received - channel: {}, message: {}", channel, message);
List<DeferredResultWrapper> results = Lists.newArrayList(deferredResults.get(content));
if (results.size() > bizConfig.releaseMessageNotificationBatch()) {
largeNotificationBatchExecutorService.submit(() -> {
logger.debug("Async notify {} clients for key {} with batch {}", results.size(), content,
bizConfig.releaseMessageNotificationBatch());
for (int i = 0; i < results.size(); i++) {
//如果有配置更新,那么调用了DeferredResultWrapper类对象的setResult方法,
//这个方法就是获取内部的成量DeferredResult 并设置值。
results.get(i).setResult(configNotification);
}
});
return;
}
}