大纲

看本文之前,建议看看 apollo 的官方文档,特别是数据库设计文档。

  1. 主流程分析

2.1 聊聊细节

2.2 loadConfig() 加载配置

2.3 auditReleases() 方法记录此次访问详情

1. 主流程分析

具体代码在 com.ctrip.framework.apollo.configservice.controller.ConfigController#queryConfig 方法中。

代码如下:

// 这个 . 号是防止 Spring 框架去除了 . 号后面的字符,例如 xxx.json, xxx.properties
@RequestMapping(value = "/{appId}/{clusterName}/{namespace:.+}", method = RequestMethod.GET)
public ApolloConfig queryConfig(@PathVariable String appId, @PathVariable String clusterName,
@PathVariable String namespace,
@RequestParam(value = "dataCenter", required = false) String dataCenter,
@RequestParam(value = "releaseKey", defaultValue = "-1") String clientSideReleaseKey,//20180704093033-648d208dc9c1c9be
@RequestParam(value = "ip", required = false) String clientIp,
@RequestParam(value = "messages", required = false) String messagesAsString,//{"details":{"SampleApp+default+application":19}}
HttpServletRequest request, HttpServletResponse response) throws IOException {
String originalNamespace = namespace;
//strip out .properties suffix 剔除后缀
namespace = namespaceUtil.filterNamespaceName(namespace);
//fix the character case issue, such as FX.apollo <-> fx.apollo
// 改名字
namespace = namespaceUtil.normalizeNamespace(appId, namespace); if (Strings.isNullOrEmpty(clientIp)) {
clientIp = tryToGetClientIp(request);// 获取客户端 IP
} // 转换成对象(目前没有地方用到?)
ApolloNotificationMessages clientMessages = transformMessages(messagesAsString); List<Release> releases = Lists.newLinkedList(); String appClusterNameLoaded = clusterName;
// appID 如果不是占位符号
if (!ConfigConsts.NO_APPID_PLACEHOLDER.equalsIgnoreCase(appId)) {
// 获取发布信息
Release currentAppRelease = configService.loadConfig(appId, clientIp, appId, clusterName, namespace,
dataCenter, clientMessages);
if (currentAppRelease != null) {
releases.add(currentAppRelease);// 添加进集合
//we have cluster search process, so the cluster name might be overridden 这个解释看不懂??? 集群搜索过程指的是?
appClusterNameLoaded = currentAppRelease.getClusterName();// 使用release 的集群名称.
}
}
// 关联类型?
//if namespace does not belong to this appId, should check if there is a public configuration
// 如果命名空间不属于这个 appId, 那么应该检查他是否是公共配置
if (!namespaceBelongsToAppId(appId, namespace)) {
Release publicRelease = this.findPublicConfig(appId, clientIp, clusterName, namespace,
dataCenter, clientMessages);// 获取公共配置
if (!Objects.isNull(publicRelease)) {
releases.add(publicRelease);// 添加进集合
}
} if (releases.isEmpty()) {// 空的话,返回 404
response.sendError(HttpServletResponse.SC_NOT_FOUND,//404
String.format(
"Could not load configurations with appId: %s, clusterName: %s, namespace: %s",
appId, clusterName, originalNamespace));
Tracer.logEvent("Apollo.Config.NotFound",
assembleKey(appId, clusterName, originalNamespace, dataCenter));
return null;
} auditReleases(appId, clusterName, dataCenter, clientIp, releases);// 保存实例信息,(就是配置灰度规则时的ip选择列表+ 实例列表)
// + 号拼接所有的 release key
String mergedReleaseKey = releases.stream().map(Release::getReleaseKey)
.collect(Collectors.joining(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR)); if (mergedReleaseKey.equals(clientSideReleaseKey)) {// 如果客户端那边的 key 和这边的 key 一致,则 304
// Client side configuration is the same with server side, return 304
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);// 返回 304
Tracer.logEvent("Apollo.Config.NotModified",
assembleKey(appId, appClusterNameLoaded, originalNamespace, dataCenter));
return null;
}
// releaseKey 不一致,则创建 config 对象返回.
ApolloConfig apolloConfig = new ApolloConfig(appId, appClusterNameLoaded, originalNamespace,
mergedReleaseKey);
apolloConfig.setConfigurations(mergeReleaseConfigurations(releases));// 合并所有的配置, 其中,私有配置优先公共配置 Tracer.logEvent("Apollo.Config.Found", assembleKey(appId, appClusterNameLoaded,
originalNamespace, dataCenter));
return apolloConfig;
}

代码有点长,具体细节等下慢慢聊,这里说说主要逻辑:

  1. 调整 namespace 的名字。获取客户端 IP(为了灰度)。

  2. 判断 appId 是不是占位符。如果不是,就尝试加载该 AppId 下的 Cluster 下的 namespace 的 release 配置。并添加进结果集。

  3. 判断是否是公共 namespac, 假设这个 namespace 不属于当前 AppId,那么就是公共配置,需要加载公共配置(通常就是管理的 namespace)。注意:这个时候,可能会有 2 个结果集:当前 AppId 发布的重写公共配置的配置 + 公共配置。

  4. 如果结果集合是空,返回 404。

  5. auditReleases 方法会异步的保存此次客户端获取配置的详细信息到数据库中,portal 页面就可以看到这些信息了。

  6. 比较服务端的 key 和客户端的 key 是否相同,因为每次发布配置都会有一个唯一的 key 生成,这里比较一下,就可以知道配置是否发生更改,如果相同,返回 304.

  7. 如果不同,构造一个 Config 对象返回给客户端。这里有个注意的地方: mergeReleaseConfigurations 方法会将 release 集合反转一下,目的是让私有的重写配置优先于公共的配置。

目前来看,不是很复杂,主要就是根据指定的 namespace 加载配置,并和客户端的 key 进行比较。如果不同,就返回新的配置。

2.1 聊聊细节

步骤1,2 都是处理 namespace,大小写,后缀什么的,优先使用服务端的名称。

转换了 messagesAsString 为 ApolloNotificationMessages 对象,目前没用到。

可以看到,比较重要的方法就是 configService.loadConfig 方法。这个方法是获取配置的核心方法。下面的获取公共配置的 findPublicConfig 方法内部也是调用的此方法。

然后还有 auditReleases 方法,这个其实就是记录此次客户端获取配置的详细信息的。

然后还有一个反转方法。这个很简单,大家可以自己看看。

先看看 configService.loadConfig 方法。

2.2 loadConfig() 加载配置

说方法之前,先看看 ConfigService 这个接口。最上层的是监听器接口,用于监听消息变化。然后是 ConfigService 接口,定义 loadConfig 方法并返回 release 对象。

最下面是具体实现类,抽象类是用了模板模式,定义了获取配置的骨架,下面则有 2 个实现类,一个基于缓存,一个基于 DB。默认是 DB。具体使用哪个类要看 server_config 表里的配置,配置的 key 是 config-service.cache.enabled,value 要么 true 要么 false。

注意,使用缓存很耗费内存,小心 OOM 哦。

看看抽象类里 loadConfig 方法的实现:

  @Override
public Release loadConfig(String clientAppId, String clientIp, String configAppId, String configClusterName,
String configNamespace, String dataCenter, ApolloNotificationMessages clientMessages) {
// load from specified cluster fist 如果不是默认的 cluster(私有或者灰度)
if (!Objects.equals(ConfigConsts.CLUSTER_NAME_DEFAULT, configClusterName)) {
Release clusterRelease = findRelease(clientAppId, clientIp, configAppId, configClusterName, configNamespace,
clientMessages); if (!Objects.isNull(clusterRelease)) {
return clusterRelease;
}
} // try to load via data center 试图通过数据中心获取
if (!Strings.isNullOrEmpty(dataCenter) && !Objects.equals(dataCenter, configClusterName)) {
Release dataCenterRelease = findRelease(clientAppId, clientIp, configAppId, dataCenter, configNamespace,
clientMessages);
if (!Objects.isNull(dataCenterRelease)) {
return dataCenterRelease;
}
} // fallback to default release 从默认的 cluster 获取
return findRelease(clientAppId, clientIp, configAppId, ConfigConsts.CLUSTER_NAME_DEFAULT, configNamespace,
clientMessages);
}

步骤:首先加载私有/灰度的 cluster,这个就是客户端配置文件里的 apollo.cluster 配置, 然后再加载 server.properties 配置文件的 idc 属性,最后加载默认的。

他们的优先级如图:

细心的你可以发现,3 个 if 判断力都是调用的 findRelease 方法,只是第四个参数不同,这个参数就是 configClusterName —— 不同的 cluster。

这个方法首先或加载灰度的,然后再加载普通的。所以,客户端的 IP 就显得重要了。

  private Release findRelease(String clientAppId, String clientIp, String configAppId, String configClusterName,
String configNamespace, ApolloNotificationMessages clientMessages) {
// 获取灰度release id, 从应用的缓存中获取规则.
Long grayReleaseId = grayReleaseRulesHolder.findReleaseIdFromGrayReleaseRule(clientAppId, clientIp, configAppId,
configClusterName, configNamespace); Release release = null; if (grayReleaseId != null) {// 如果有灰度
// return releaseRepository.findByIdAndIsAbandonedFalse(releaseId);// 没有放有回滚的发布
release = findActiveOne(grayReleaseId, clientMessages); // 获取灰度 release
} if (release == null) {// 如果没有发布的新灰度
// 获取最新的普通的发布活动
release = findLatestActiveRelease(configAppId, configClusterName, configNamespace, clientMessages);
}
// 灰度 --> 最新的
return release;
}

步骤:调用grayReleaseRulesHolderfindReleaseIdFromGrayReleaseRule 方法获取灰度发布 ID。当然,这也是个缓存。

如果有灰度,则根据 id 获取对应的 release 信息,得到配置,release 里面包含了全量的配置信息(从数据库获取)。

如果没有灰度,则获取最新的普通的发布信息(从数据库获取)。

关键在于获取灰度 id。

看看这个方法:

  public Long findReleaseIdFromGrayReleaseRule(String clientAppId, String clientIp, String
configAppId, String configCluster, String configNamespaceName) {
String key = assembleGrayReleaseRuleKey(configAppId, configCluster, configNamespaceName);// 组装 key
if (!grayReleaseRuleCache.containsKey(key)) {// 从缓存中获取, 缓存是个 handler 监听器 + 定时任务
return null;
}
//create a new list to avoid ConcurrentModificationException 如果存在,就处理
List<GrayReleaseRuleCache> rules = Lists.newArrayList(grayReleaseRuleCache.get(key));
for (GrayReleaseRuleCache rule : rules) {
//check branch status 必须是激活的状态
if (rule.getBranchStatus() != NamespaceBranchStatus.ACTIVE) {
continue;
}// 如果匹配上了 ip 和 客户端的 appId, 就返回
if (rule.matches(clientAppId, clientIp)) {
return rule.getReleaseId();
}
}
return null;
}

步骤:首先用 + 号将 appId,cluster,namespace 拼接,再从缓存中获取,获取的是该 key 对应的灰度规则。

而缓存由一个定时任务更新(60s) + 监听器更新。

如果存在,就循环比较规则,如果规则是激活状态,且 appId 和 ip 和当前客户端匹配,那么就返回这个灰度的 release Id。

这个就是得到灰度 release Id 的具体逻辑,可以看到,这里是优先加载灰度的。

那么这个定时任务 + 监听器具体是怎么样的呢?

刚刚说了,定时任务是 60 s 一次,相对于配置中心来说,及时性肯定是不够的,所以,他更多的是一种补偿措施,即监听器失效了,定时任务能够保证 60s 内配置是最新的。

而监听器才是最新的配置。具体方法则是 handleMessage 方法。这个方法会得到一个发布消息,包含 appId + clusterName + namespace,有了这个信息,就可以得到 release 信息了。

每当发布一个配置 ,或回滚一个配置,都会发送一个消息到数据库,ConfigService 会扫描得到这个消息,然后通知所有的监听器。执行监听器的 handlerMessage 方法。

在灰度规则监听器中,会检查灰度发布规则表,根据消息的内容(appId + cluster + namespace 组成的唯一 namespace key)并进行处理,处理的逻辑则是更新缓存中的规则内容。

总结一下这个 loadConfig 方法:

这是 ConfigService 接口定义的方法,由 一个抽象类和 2 个派生类组成,默认使用 DB 模式的派生类,抽象类定义了 loadConfig 的方法骨架,利用模板模式,2 个子类可以根据自己的特性返回数据 —— release。loadConfig 里,在获取 release 的时候,会有一个查找顺序,首先找私有/灰度的 cluster,然后找 idc(这个一般公司用不到,携程内部的特性),最后找默认的。而他们调用的 findRelease 方法内部也会有一个查找顺序:首先根据灰度规则查找灰度发布 ID,如果没有,查找默认的最新发布 —— 也就是灰度的规则比默认的规则高。

2.3 auditReleases() 方法记录此次访问详情

这个方法主要是调用 instanceConfigAuditUtil 的 audit 方法:

  private void auditReleases(String appId, String cluster, String dataCenter, String clientIp,
List<Release> releases) {
if (Strings.isNullOrEmpty(clientIp)) {
//no need to audit instance config when there is no ip
return;
}
for (Release release : releases) {
instanceConfigAuditUtil.audit(appId, cluster, dataCenter, clientIp, release.getAppId(),
release.getClusterName(),
release.getNamespaceName(), release.getReleaseKey());
}
}

注意:这里的判断,如果没有 ip, 就不记录了。这里用的是 aduit 审计的概念,我想就是类似记录吧,方便后面进行复盘,查看啥的。

而这个 audit 方法具体的内容则是:构造一个 InstanceConfigAuditModel 对象放到一个阻塞队列中,由另一个线程异步处理。

public boolean audit(String appId, String clusterName, String dataCenter, String
ip, String configAppId, String configClusterName, String configNamespace, String releaseKey) {
return this.audits.offer(new InstanceConfigAuditModel(appId, clusterName, dataCenter, ip,
configAppId, configClusterName, configNamespace, releaseKey));
}
// 长度为 10000 的阻塞队列
private BlockingQueue<InstanceConfigAuditModel> audits = Queues.newLinkedBlockingQueue
(10000);

那么异步执行的内容是怎么样的呢?

当然要看队列 poll 或者 take 后做什么了.

  @Override
public void afterPropertiesSet() throws Exception {
auditExecutorService.submit(() -> {
while (!auditStopped.get() && !Thread.currentThread().isInterrupted()) {
try {
InstanceConfigAuditModel model = audits.poll();
if (model == null) {
TimeUnit.SECONDS.sleep(1);
continue;
}
doAudit(model);
} catch (Throwable ex) {
Tracer.logError(ex);
}
}
});
}

该类实现了 Spring 的 InitializingBean 接口,重写了 afterPropertiesSet 方法,这个方法会在属性注入完毕后执行。

方法其实就是提交了一个任务,任务内容则是从队列中取出对象,然后执行 doAudit 方法。如果取出是空,休眠 1 秒。

这个方法的主要内容就是更新客户端访问信息,或者创建客户端访问信息。使用 2 个缓存,存储 instanceId 和 releaseKey,这个是为了提高校验数据的性能。

而这个实例在数据库是这样的:

从 Instance 表结构看,记录是每台机器最新访问的记录,而 InstanceConfig 则是记录的此次访问的具体 namespace 的 发布信息。

对应的是控制台的实例列表:

关于这个方法的具体内容我就不贴了,感兴趣的可以自己看看,主要内容就是记录 Instance 的访问信息用于后台审计查看。

总结

好了,apollo 客户端访问 ConfigService 获取配置的大概思路和具体细节就介绍完了。这里总结一下。

  1. 获取配置的时候,可能会有 2 个结果集(关联类型),那么会将私有的优先(放到前面)。如果集合是空,返回 404 ,如果没有新的发布信息,返回 304.

  2. 当服务器加载配置信息的时候,有几个顺序,特别是集群的顺序:私有/灰度 cluster (apollo.cluster)----> 数据中心(server.properties 的 idc)-----> 默认的 cluster。同时,加载集群内部配置的时候,也会优先加载灰度的配置(根据 IP),然后才是默认的配置。

  3. 最后,会记录此次访问的信息,方便后台审计。如果是 10 分钟之内访问的,即不会更新。

Apollo 6 — ConfigService 获取配置接口的更多相关文章

  1. Apollo 8 — ConfigService 异步轮询接口的实现

    源码 Apollo 长轮询的实现,是通过客户端轮询 /notifications/v2 接口实现的.具体代码在 com.ctrip.framework.apollo.configservice.con ...

  2. python访问Apollo获取配置

    操作系统 : CentOS7.3.1611_x64 Python 版本 : 3.6.8 Apollo源码地址: https://github.com/ctripcorp/apollo 访问Apollo ...

  3. 分布式配置系统Apollo如何实时更新配置的?

    引言 记得我们那时候刚开始学习Java的时候都只是一个单体项目,项目里面的配置基本都是写在项目里面的properties文件中,比如数据库配置啥的,各种逻辑开关,一旦这些配置修改了,还需要重启项目这修 ...

  4. PHP微信公众平台开发1 配置接口

    1.简介 微信公众平台是腾讯公司在微信的基础上新增的功能模块,通过这一平台,个人和企业都可以打造一个微信的公众号,并实现和特定群体的文字.图片.语音的全方位沟通.互动. 2.通讯机制 3.注册微信公众 ...

  5. 微信js-sdk开发获取签名和获取地理位置接口示例

    ###微信js-sdk开发获取签名和获取地理位置接口示例 前言:在做微信公众号开发时需要获取用户的地理位置信息,之前通过高德或者百度.腾讯等地图的api时发现经常获取不到,毕竟第三方的东西,后来改为采 ...

  6. java反射注解妙用-获取所有接口说明

    转载请注明出处:https://www.cnblogs.com/wenjunwei/p/10293490.html 前言 最近在做项目权限,使用shiro实现restful接口权限管理,对整个项目都进 ...

  7. 微信开发(2):微信js sdk分享朋友圈,朋友,获取config接口注入权限验证(转)

    进行微信开发已经一阵子了,从最初的什么也不懂,到微信授权登录,分享,更改底部菜单,素材管理,等. 今天记录一下微信jssdk 的分享给朋友的功能,获取config接口注入. 官方文档走一下简单说:四步 ...

  8. JavaWeb学习之Servlet(四)----ServletConfig获取配置信息、ServletContext的应用

    [声明] 欢迎转载,但请保留文章原始出处→_→ 文章来源:http://www.cnblogs.com/smyhvae/p/4140877.html [正文] 一.ServletConfig:代表当前 ...

  9. 如何通过图片在 HTTPS 网站中获取 HTTP 接口数据

    <script> (function() { var Decode=function(b){var e;e=[];var a=b.width,c=b.height,d=document.c ...

随机推荐

  1. How to setup Visual Studio without pain

    Visual Studio (VS) can be very hard to install. If you are lucky, one whole day may be enough to ins ...

  2. php中出现乱码

    对于初学着来说,编辑中文php时,会出现乱码 在php代码中加入 随后在浏览器中,就会看到如下页面 这样就解决了php 中文乱码的问题.

  3. #224 Profile Lookup (for in & if )

    我们有一个对象数组,里面存储着通讯录. 函数 lookUp 有两个预定义参数:firstName值和prop属性 . 函数将会检查通讯录中是否存在一个与传入的 firstName 相同的联系人.如果存 ...

  4. chat.css

    *, *:before, *:after { box-sizing: border-box;}body, html { height: 100%; overflow: hidden;}body, ul ...

  5. 包建强的培训课程(14):Android与ReactNative

    @import url(http://i.cnblogs.com/Load.ashx?type=style&file=SyntaxHighlighter.css);@import url(/c ...

  6. pwn入门之栈溢出练习

    本文原创作者:W1ngs,本文属i春秋原创奖励计划,未经许可禁止转载!前言:最近在入门pwn的栈溢出,做了一下jarvisoj里的一些ctf pwn题,感觉质量都很不错,难度循序渐进,把自己做题的思路 ...

  7. HTTP 协议常见首部字段

    首部字段 1.HTTP协议的请求和响应报文中必定包含HTTP首部.首部内容为客户端和服务器处理请求和响应提供了所必须的信息. 2.HTTP首部字段是由首部字段名和字段值构成,中间用冒号“:”隔开.字段 ...

  8. Day7:html和css

    Day7:html和css 如果有浮动,会导致脱标,定位也能脱标,我们没有清除浮动,因为里面有子绝父相. 清除浮动的方法 额外标签法,在最后一个浮动元素后面添加一个空的标签代码: <div st ...

  9. 微信公众号接入之排序问题小记 Arrays.sort()

    微信公众号作为强大的自媒体工具,对接一下是很正常的了.不过这不是本文的方向,本文的方向公众号接入的排序问题. 最近接了一个重构的小项目,需要将原有的php的公众号后台系统,转换为java系统.当然,也 ...

  10. MySQL(4)---慢查询

    慢查询 简介       开启慢查询日志,可以让MySQL记录下查询超过指定时间的语句,通过定位分析性能的瓶颈,才能更好的优化数据库系统的性能. 一.配置慢查询 1.参数说明 slow_query_l ...