本文是对apollo原理的个人理解,搭建使用请参见上一篇文章apollo 搭建使用
1. 模块介绍
下图是单个环境的架构图
config service
提供配置的读取
、推送
等功能,服务对象是Apollo 客户端
;
admin service
提供配置的修改
、发布
等功能,服务对象是Apollo Portal (管理界面)
eureka
提供服务的注册和发现,为了部署简单起见,eureka 通常和config service
在一个JVM 进程
中;
注: config service 和 admin service 都是多实例、无状态部署,故需要将自己部署到eureka 中并保持心跳;
在eureka 之上 加了一层Meta Service 用户封装Eureka 的服务发现接口;
apollo-client
Client 通过域名访问Meta Server 获取Config Service 服务列表(IP + PORT), 而后直接通过IP + PORT 访问服务,同时Cient 侧会做load Balance 、错误重试;
portal
portal 通过域名访问Mate Server 获取 Admin Service 服务列表(IP + PORT),而后直接过过IP + PORT 访问服务,同时Portal测会做load Balance 、错误重试
为了简化部署,我们实际上会把Config、Eureka 和 Meta Server 三个逻辑角色部署在同一个JVM 进程中;
2. 配置发布原理
admin service 发送消息
我们从Admin Service发布看起:
com.ctrip.framework.apollo.biz.message.DatabaseMessageSender#sendMessage
/**
* 发送消息
*/
@Override
@Transactional
public void sendMessage(String message, String channel) {
logger.info("Sending message {} to channel {}", message, channel);
if (!Objects.equals(channel, Topics.APOLLO_RELEASE_TOPIC)) {
logger.warn("Channel {} not supported by DatabaseMessageSender!", channel);
return;
}
Tracer.logEvent("Apollo.AdminService.ReleaseMessage", message);
Transaction transaction = Tracer.newTransaction("Apollo.AdminService", "sendMessage");
try {
ReleaseMessage newMessage = releaseMessageRepository.save(new ReleaseMessage(message));
toClean.offer(newMessage.getId());
transaction.setStatus(Transaction.SUCCESS);
} catch (Throwable ex) {
logger.error("Sending message to database failed", ex);
transaction.setStatus(ex);
throw ex;
} finally {
transaction.complete();
}
}
config service 定时任务扫描
com.ctrip.framework.apollo.biz.message.ReleaseMessageScanner
#scanAndSendMessages
#fireMessageScanned
/**
* scan messages and send
*
* @return whether there are more messages
*/
private boolean scanAndSendMessages() {
//current batch is 500
List<ReleaseMessage> releaseMessages =
releaseMessageRepository.findFirst500ByIdGreaterThanOrderByIdAsc(maxIdScanned);
if (CollectionUtils.isEmpty(releaseMessages)) {
return false;
}
fireMessageScanned(releaseMessages);
int messageScanned = releaseMessages.size();
maxIdScanned = releaseMessages.get(messageScanned - 1).getId();
return messageScanned == 500;
}
/**
* Notify listeners with messages loaded
* @param messages
*/
private void fireMessageScanned(List<ReleaseMessage> messages) {
for (ReleaseMessage message : messages) {
for (ReleaseMessageListener listener : listeners) {
try {
listener.handleMessage(message, Topics.APOLLO_RELEASE_TOPIC);
} catch (Throwable ex) {
Tracer.logError(ex);
logger.error("Failed to invoke message listener {}", listener.getClass(), ex);
}
}
}
}
apollo-client 长链接拉取
apollo-client 发送获取配置请求
private void doLongPollingRefresh(String appId, String cluster, String dataCenter, String secret) {
final Random random = new Random();
ServiceDTO lastServiceDto = null;
while (!m_longPollingStopped.get() && !Thread.currentThread().isInterrupted()) {
if (!m_longPollRateLimiter.tryAcquire(5, TimeUnit.SECONDS)) {
//wait at most 5 seconds
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
}
}
Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "pollNotification");
String url = null;
try {
if (lastServiceDto == null) {
List<ServiceDTO> configServices = getConfigServices();
lastServiceDto = configServices.get(random.nextInt(configServices.size()));
}
url =
assembleLongPollRefreshUrl(lastServiceDto.getHomepageUrl(), appId, cluster, dataCenter,
m_notifications);
logger.debug("Long polling from {}", url);
//【 建立HTTP 长链接 LONG_POLLING_READ_TIMEOUT=90S】
HttpRequest request = new HttpRequest(url);
request.setReadTimeout(LONG_POLLING_READ_TIMEOUT);
if (!StringUtils.isBlank(secret)) {
Map<String, String> headers = Signature.buildHttpHeaders(url, appId, secret);
request.setHeaders(headers);
}
transaction.addData("Url", url);
final HttpResponse<List<ApolloConfigNotification>> response = m_httpUtil.doGet(request, m_responseType);
logger.debug("Long polling response: {}, url: {}", response.getStatusCode(), url);
if (response.getStatusCode() == 200 && response.getBody() != null) {
updateNotifications(response.getBody());
updateRemoteNotifications(response.getBody());
transaction.addData("Result", response.getBody().toString());
notify(lastServiceDto, response.getBody());
}
//try to load balance
if (response.getStatusCode() == 304 && random.nextBoolean()) {
lastServiceDto = null;
}
m_longPollFailSchedulePolicyInSecond.success();
transaction.addData("StatusCode", response.getStatusCode());
transaction.setStatus(Transaction.SUCCESS);
} catch (Throwable ex) {
// 省略。。。
} finally {
transaction.complete();
}
}
}
config-service 接受请求:
@RestController
@RequestMapping("/notifications/v2")
public class NotificationControllerV2 implements ReleaseMessageListener {
@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) {
}
}
NotificationControllerV2 不会立刻返回结果,而是把请求挂起。考虑到会有数万客户端向服务端发起长连,
在服务端使用了async servlet
(Spring DeferredResult) 来服务Http Long Polling 请求;
如果在90S内没有该客户端关心的配置发布,那么会返回Http 状态码304
给客户端;
如果有该客户端关心的配置发布,NotificationV2会调动DeferredResult的setResult方法,传入有配置变化的namespace信息,同事该请求会立即返回,客户端从返回的结果中获取到配置变化的namespace后,会立即请求Config Service 获取namespace 的最新配置;
客户端的获取设计
除了之前介绍的客户端和服务端保持一个长连接,从而能第一时间获得配置更新的推送外,客户端还会定时从Apollo配置中心服务端拉取应用的最新配置。
- 这是一个备用机制,为了防止推送机制失效导致配置不更新
- 客户端定时拉取会上报本地版本,所以一般情况下,对于定时拉取的操作,服务端都会返回304 - Not Modified
- 定时频率默认为每5分钟拉取一次,客户端也可以通过在运行时指定System Property: apollo.refreshInterval来覆盖,单位为分钟