Apollo-3-定时-长轮询拉取配置的设计

Apollo 基础模型

前言

如上图所示,Apollo portal 更新配置后,进行轮询的客户端获取更新通知,然后再调用接口获取最新配置。不仅仅只有轮询,还有定时更新(默认 5 分钟一次)。目的就是让客户端能够稳定的获取到最新的配置。

一起来看看他的设计。

核心代码

具体的类是 RemoteConfigRepository,每一个 Config —— 也就是 namespace 都有一个 RemoteConfigRepository 对象,表示这个 Config 的远程配置仓库,可以利用这个仓库请求远程服务,得到配置。

RemoteConfigRepository 的构造方法需要一个 namespace 字符串,表明这个 Repository 所属的 Config 名称。

下面是该类的构造方法。

public RemoteConfigRepository(String namespace) {
m_namespace = namespace;// Config 名称
m_configCache = new AtomicReference<>(); // Config 引用
m_configUtil = ApolloInjector.getInstance(ConfigUtil.class);// 单例的 config 配置,存放 application.properties
m_httpUtil = ApolloInjector.getInstance(HttpUtil.class);// HTTP 工具
m_serviceLocator = ApolloInjector.getInstance(ConfigServiceLocator.class);// 远程服务 URL 更新类
remoteConfigLongPollService = ApolloInjector.getInstance(RemoteConfigLongPollService.class);// 长轮询服务
m_longPollServiceDto = new AtomicReference<>();// 长轮询发现的当前配置发生变化的服务
m_remoteMessages = new AtomicReference<>();
m_loadConfigRateLimiter = RateLimiter.create(m_configUtil.getLoadConfigQPS());// 限流器
m_configNeedForceRefresh = new AtomicBoolean(true);// 是否强制刷新
m_loadConfigFailSchedulePolicy = new ExponentialSchedulePolicy(m_configUtil.getOnErrorRetryInterval(),//1
m_configUtil.getOnErrorRetryInterval() * 8);// 1 * 8;失败定时重试策略: 最小一秒,最大 8 秒.
gson = new Gson();// json 序列化
this.trySync(); // 第一次同步
this.schedulePeriodicRefresh();// 定时刷新
this.scheduleLongPollingRefresh();// 长轮询刷新
}

可以看到,在构造方法中,就执行了 3 个本地方法,其中就包括定时刷新和长轮询刷新。这两个功能在 apollo 的 github 文档中也有介绍:

1.客户端和服务端保持了一个长连接,从而能第一时间获得配置更新的推送。
2.客户端还会定时从Apollo配置中心服务端拉取应用的最新配置。
3.这是一个fallback机制,为了防止推送机制失效导致配置不更新。
4.客户端定时拉取会上报本地版本,所以一般情况下,对于定时拉取的操作,服务端都会返回304 - Not Modified。
5.定时频率默认为每5分钟拉取一次,客户端也可以通过在运行时指定System Property: apollo.refreshInterval来覆盖,单位为分钟。

所以,长连接是更新配置的主要手段,然后用定时任务辅助长连接,防止长连接失败。

那就看看长连接和定时任务的具体代码。

定时任务

定时任务主要由一个单 core 的线程池维护这定时任务。

static {
// 定时任务,单个 core. 后台线程
m_executorService = Executors.newScheduledThreadPool(1,
ApolloThreadFactory.create("RemoteConfigRepository", true));
}

private void schedulePeriodicRefresh() {
// 默认 5 分钟同步一次.
m_executorService.scheduleAtFixedRate(
new Runnable() {
@Override
public void run() {
trySync();
}
}, m_configUtil.getRefreshInterval(), m_configUtil.getRefreshInterval(),// 5
m_configUtil.getRefreshIntervalTimeUnit());//单位:分钟
}

具体就是每 5 分钟执行 sync 方法。我简化了一下 sync 方法,一起看看:

protected synchronized void sync() {
ApolloConfig previous = m_configCache.get();
// 加载远程配置
ApolloConfig current = loadApolloConfig();

//reference equals means HTTP 304
if (previous != current) {
m_configCache.set(current);
// 触发监听器
this.fireRepositoryChange(m_namespace, this.getConfig());
}
}

首先,拿到上一个 config 对象的引用,然后,加载远程配置,判断是否相等,如果不相等,更新引用缓存,触发监听器。

可以看出,关键是加载远程配置和触发监听器,这两个操作。

loadApolloConfig 方法主要逻辑就是通过 HTTP 请求从 configService 服务里获取到配置。大概步骤如下:

  1. 首先限流。获取服务列表。然后根据是否有更新通知,决定此次重试几次,如果有更新,重试2次,反之一次。
  2. 优先请求通知自己的 configService,如果失败了,就要进行休息,休息策略要看是否得到更新通知了,如果是,就休息一秒,否则按照 SchedulePolicy 策略来。
  3. 拿到数据后,重置强制刷新状态和失败休息状态,返回配置。

触发监听器步骤:

  1. 循环远程仓库的监听器,调用他们的 onRepositoryChange 方法。其实就是 Config。
  2. 然后,更新 Config 内部的引用,循环向线程池提交任务—— 执行 Config 监听器的 onChange 方法。

好,到这里,定时任务就算处理完了,总之就是调用 sync 方法,请求远程 configServer 服务,得到结果后,更新 Config 对象里的配置,并通知监听器。

再来说说长轮询。

长连接 / 长轮询

长轮询实际上就是在一个类似死循环里,不停请求 ConfigServer 的配置变化通知接口 notifications/v2,如果配置有变更,就会返回变更信息,然后向定时任务线程池提交一个任务,任务内容是执行 sync 方法。

在请求 ConfigServer 的时候,ConfigServer 使用了 Servlet 3 的异步特性,将 hold 住连接 30 秒,等到有通知就立刻返回,这样能够实现一个基于 HTTP 的长连接。

关于为什么使用 HTTP 长连接,初次接触 Apollo 的人都会疑惑,为什么使用这种方式,而不是”那种”方式?

下面是作者宋顺的回复:

总结一下:

  1. 为什么不使用消息系统?太复杂,杀鸡用牛刀。
  2. 为什么不用 TCP 长连接?对网络环境要求高,容易推送失败。且有双写问题。
  3. 为什么使用 HTTP 长轮询?性能足够,结合 Servlet3 的异步特性,能够维持万级连接(一个客户端只有一个长连接)。直接使用 Servlet 的 HTTP 协议,比单独用 TCP 连接方便。HTTP 请求/响应模式,保证了不会出现双写的情况。最主要还是简单,性能暂时不是瓶颈

总结

本文没有贴很多的代码。因为不是一篇源码分析的文章。

总之,Apollo 的更新配置设计就是通过定时轮询和长轮询进行组合而来。

定时轮询负责调用获取配置接口,长轮询负责调用配置更新通知接口,长轮询得到结果后,将提交一个任务到定时轮询线程池里,执行同步操作——也就是调用获取配置接口。

为什么使用 HTTP 长轮询? 简单!简单!简单!