Apollo 配置中心源码分析
Apollo是携程开源的一款分布式配置管理中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性,适用于微服务配置管理场景。
Apollo配置发布和通知的过程
-
用户在配置中心对配置进行修改并发布
-
配置中心通知Apollo客户端有配置更新
-
Apollo客户端从配置中心拉取最新的配置、更新本地配置并通知到应用
从Apollo模块看配置发布流程
Apollo四个核心模块及其主要功能
-
ConfigService
- 提供配置获取接口
- 提供配置推送接口
- 服务于Apollo客户端
-
AdminService
- 提供配置管理接口
- 提供配置修改发布接口
- 服务于管理界面Portal
-
Client
- 为应用获取配置,支持实时更新
- 通过MetaServer获取ConfigService的服务列表
- 使用客户端软负载SLB方式调用ConfigService
-
Portal
- 配置管理界面
- 通过MetaServer获取AdminService的服务列表
- 使用客户端软负载SLB方式调用AdminService
先对整体流程进行一个梳理:
*
用户修改和发布配置是通过portal调用AdminService,把配置变更保存在数据库中。
*
客户端通过长轮询访问ConfigService实时监听配置变更。默认超时时间是90秒。如果在超时前有配置变更,就会立即返回给客户端。客户端获取变化的配置,根据进行实时更新。如果超时也没有数据变更,就放304.客户端重新发起新的请求。
*
配置服务ConfigService有一个定时任务,每秒去扫描数据库,查看是否有新变更的数据。如果有数据变更就通知客户端。
下面打算对Apollo在页面修改配置后如何通知到客户端过程的源码进行分析。
说明:
- Apollo版本为1.9.1.
- 测试用的应用appid=apollo-demo,namespace=default,env=DEV,cluster=default
主要分为一下几个部分
- 页面发布配置(新增,修改和删除)
- configService获取到新发布的配置信息
- configService通知客户端最新的配置变更
- 客户端的同步更新Spring容器中注入的@Value的值
- Apollo 如何实现让自己的配置优先级最高
一、 Apollo修改配置与发布配置
1.1页面修改配置
修改name 旧值:张三 新值:张三1
URL: http://localhost:8070/apps/apollo-demo/envs/DEV/clusters/default/namespaces/application/item
参数:
{"id":1,"namespaceId":1,"key":"name","value":"张三1","lineNum":1,"dataChangeCreatedBy":"apollo","dataChangeLastModifiedBy":"apollo","dataChangeCreatedByDisplayName":"apollo","dataChangeLastModifiedByDisplayName":"apollo","dataChangeCreatedTime":"2022-02-26T12:26:12.000+0800","dataChangeLastModifiedTime":"2022-02-26T12:26:12.000+0800","tableViewOperType":"update","comment":"修改姓名"}
根据上面的分析在页面修改配置是portal调用AdminService保存到数据库。所以我们到Apollo的portal模块去查找请求。Apollo使用的是restful的请求方式,它的请求格式都是/参数名1/{参数值1}/参数名2/{参数值2}/……。所以我们就去portal查询"/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/item")
@PutMapping("/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/item")
public void updateItem(@PathVariable String appId,
@PathVariable String env,
@PathVariable String clusterName,
@PathVariable String namespaceName,
@RequestBody ItemDTO item) {
checkModel(isValidItem(item));
String username = userInfoHolder.getUser().getUserId();
item.setDataChangeLastModifiedBy(username);
configService.updateItem(appId, Env.valueOf(env), clusterName, namespaceName, item);
}
单个更新配置时portal通过configService.updateItem()保存数据中
public void updateItem(String appId, Env env,
String clusterName,
String namespace,
long itemId,
ItemDTO item) {
restTemplate.put(env, "apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items/{itemId}",
item, appId, clusterName, namespace, itemId);
}
这里就是portal通过restTemplate调用AdminService保存配置到数据库。
AdminService 中代码如下
@PutMapping("/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items/{itemId}")
public ItemDTO update(@PathVariable("appId") String appId,
@PathVariable("clusterName") String clusterName,
@PathVariable("namespaceName") String namespaceName,
@PathVariable("itemId") long itemId,
@RequestBody ItemDTO itemDTO) {
Item managedEntity = itemService.findOne(itemId);
if (managedEntity == null) {
throw new NotFoundException("item not found for itemId " + itemId);
}
Namespace namespace = namespaceService.findOne(appId, clusterName, namespaceName);
// In case someone constructs an attack scenario
if (namespace == null || namespace.getId() != managedEntity.getNamespaceId()) {
throw new BadRequestException("Invalid request, item and namespace do not match!");
}
Item entity = BeanUtils.transform(Item.class, itemDTO);
ConfigChangeContentBuilder builder = new ConfigChangeContentBuilder();
Item beforeUpdateItem = BeanUtils.transform(Item.class, managedEntity);
//protect. only value,comment,lastModifiedBy can be modified
managedEntity.setValue(entity.getValue());
managedEntity.setComment(entity.getComment());
managedEntity.setDataChangeLastModifiedBy(entity.getDataChangeLastModifiedBy());
// 保存配置到 Item表中
entity = itemService.update(managedEntity);
builder.updateItem(beforeUpdateItem, entity);
itemDTO = BeanUtils.transform(ItemDTO.class, entity);
if (builder.hasContent()) {
Commit commit = new Commit();
commit.setAppId(appId);
commit.setClusterName(clusterName);
commit.setNamespaceName(namespaceName);
commit.setChangeSets(builder.build());
commit.setDataChangeCreatedBy(itemDTO.getDataChangeLastModifiedBy());
commit.setDataChangeLastModifiedBy(itemDTO.getDataChangeLastModifiedBy());
// 保存发布信息到 commit 表中
commitService.save(commit);
}
return itemDTO;
}
我们看下数据item表中的配置信息。里面记录namespaceid,key,value,comment(配置的备注信息),可以根据上面信息查询到配置信息。
commit表中的信息。
每次修改配置都会新插入一条记录。其中changSets记录了这次变更的类型和内容。
每个changeSets中会按照createItems,updateItems,deleteItems分别记录了新增,修改和删除的配置项。每个分类里面又会记录具体的新增,修改和删除的具体配置信息。
1.2 查询配置列表
url:http://localhost:8070/apps/apollo-demo/envs/DEV/clusters/default/namespaces
列表分别显示了有两条配置修改了,但是没有发布。在上面标记了未发布的标签。这个是怎么判断的呢?
我们一起看下源码吧。根据上面的地址,我们去portal中查询 /apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces
@GetMapping("/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces")
public List<NamespaceBO> findNamespaces(@PathVariable String appId,
@PathVariable String env,
@PathVariable String clusterName) {
// 根据应用名,环境和集群查询配置列表,根据namespece返回配置列表。
List<NamespaceBO> namespaceBOs = namespaceService.findNamespaceBOs(
appId, Env.valueOf(env), clusterName);
for (NamespaceBO namespaceBO : namespaceBOs) {
if (permissionValidator.shouldHideConfigToCurrentUser(
appId, env, namespaceBO.getBaseInfo().getNamespaceName())) {
namespaceBO.hideItems();
}
}
return namespaceBOs;
}
NamespaceBO中的内容。里面包含基本信息,以及namespace内的配置列表。item中的isModified表示配置是否修改,但是没有发布。如果修改了,里面还会包含修改前后的值。
namespaceService.findNamespaceBOs()是查询该集群下所有namespaces和配置信息。现在看下namespaceService.findNamespaceBOs()的具体实现。
public List<NamespaceBO> findNamespaceBOs(String appId, Env env, String clusterName) {
// 根据查询应用,环境和集群查询当前的namespaces列表,
// 查询的表 namespace jpa语句 namespaceRepository.findByAppIdAndClusterNameAndNamespaceName(appId, clusterName,namespaceName);
List<NamespaceDTO> namespaces = namespaceAPI.findNamespaceByCluster(appId, env, clusterName);
if (namespaces == null || namespaces.size() == 0) {
throw new BadRequestException("namespaces not exist");
}
List<NamespaceBO> namespaceBOs = new LinkedList<>();
for (NamespaceDTO namespace : namespaces) {
NamespaceBO namespaceBO;
try {
//根据环境查询得到NamespaceBO
namespaceBO = transformNamespace2BO(env, namespace);
namespaceBOs.add(namespaceBO);
} catch (Exception e) {
logger.error("parse namespace error. app id:{}, env:{}, clusterName:{}, namespace:{}",
appId, env, clusterName, namespace.getNamespaceName(), e);
throw e;
}
}
return namespaceBOs;
}
transformNamespace2BO作用就是查询出namespace中哪些是修改的,哪些是删除的。看下面代码前的前置内容
-
apollo 对数据库的操作都是使用JPA,查询时@Where(clause = “isDeleted = 0”) 默认排除了已删除的
-
-对涉及到的几张表的说明
release:每次发布生效的配置记录。里面的Configurations 是对当前生效的配置列表的JSON串。已删除的配置不会保存在里面。
item:保存配置的表。adminService中新增,修改和删除配置都是更新这张表。里面是配置的最新值,但是配置的状态可能是已发布的,也可能是已修改但未发布的。
commit:保存每次配置修改的记录,里面记录每次修改配置提交时的新增,修改和删除的配置列表。
{“createItems”:[],“updateItems”:[{“oldItem”:{“namespaceId”:1,“key”:“age”,“value”:“21”,“comment”:“年龄修改”,“lineNum”:2,“id”:2,“isDeleted”:false,“dataChangeCreatedBy”:“apollo”,“dataChangeCreatedTime”:“2022-02-26 12:26:23”,“dataChangeLastModifiedBy”:“apollo”,“dataChangeLastModifiedTime”:“2022-03-05 09:56:27”},“newItem”:{“namespaceId”:1,“key”:“age”,“value”:“22”,“comment”:“年龄修改2”,“lineNum”:2,“id”:2,“isDeleted”:false,“dataChangeCreatedBy”:“apollo”,“dataChangeCreatedTime”:“2022-02-26 12:26:23”,“dataChangeLastModifiedBy”:“apollo”,“dataChangeLastModifiedTime”:“2022-03-05 21:35:48”}}],“deleteItems”:[]}
- 如何判断配置是否发布
如果在item表中存在值跟最新发布生效的配置值不一样,则可能是新增或者修改的值但是为发布
- 如何判断配置已删除
查询最后一次发布记录,获取最后一次发布配置的时间。然后查询commit表中在最后一次发布配置后,所有的commit记录。然后从里面取出所有的删除配置列表。就得到的删除但没有发布的配置列表
private NamespaceBO transformNamespace2BO(Env env, NamespaceDTO namespace) {
NamespaceBO namespaceBO = new NamespaceBO();
namespaceBO.setBaseInfo(namespace);
String appId = namespace.getAppId();
String clusterName = namespace.getClusterName();
String namespaceName = namespace.getNamespaceName();
fillAppNamespaceProperties(namespaceBO);
List<ItemBO> itemBOs = new LinkedList<>();
namespaceBO.setItems(itemBOs);
//latest Release
ReleaseDTO latestRelease;
Map<String, String> releaseItems = new HashMap<>();
Map<String, ItemDTO> deletedItemDTOs = new HashMap<>();
// 查询最后一次发布记录,里面保存了最新发布的,已经生效的的所有配置信息,不包括只删除的配置,json串保存。而items中的是最新的值,但可能是已发布的页可能是未发布的配置。
// 查询的表 Release jpa语句 releaseRepository.findFirstByAppIdAndClusterNameAndNamespaceNameAndIsAbandonedFalseOrderByIdDesc(appId,clusterName,namespaceName);
latestRelease = releaseService.loadLatestRelease(appId, env, clusterName, namespaceName);
if (latestRelease != null) {
releaseItems = GSON.fromJson(latestRelease.getConfigurations(), GsonType.CONFIG);
}
//not Release config items 开始处理未发布的配置
// 查询namespace下未删除的配置列表。列表中的内容可能有未发布的配置
// 查询的表 Item List<Item> items = itemRepository.findByNamespaceIdOrderByLineNumAsc(namespaceId);
List<ItemDTO> items = itemService.findItems(appId, env, clusterName, namespaceName);
additionalUserInfoEnrichService
.enrichAdditionalUserInfo(items, BaseDtoUserInfoEnrichedAdapter::new);
int modifiedItemCnt = 0;
for (ItemDTO itemDTO : items) {
// 判断内容是否更改,并设置修改前和修改后的值。通过对比最后一次发布记录中的值与当前最新的值是否一致,如果不一致说明是修改后没有发布。
ItemBO itemBO = transformItem2BO(itemDTO, releaseItems);
if (itemBO.isModified()) {
modifiedItemCnt++;
}
itemBOs.add(itemBO);
}
//deleted items 开始处理已删除的配置
// 调用adminService 获取最后一次发布后的已删除的配置列表
itemService.findDeletedItems(appId, env, clusterName, namespaceName).forEach(item -> {
deletedItemDTOs.put(item.getKey(),item);
});
List<ItemBO> deletedItems = parseDeletedItems(items, releaseItems, deletedItemDTOs);
itemBOs.addAll(deletedItems);
modifiedItemCnt += deletedItems.size();
namespaceBO.setItemModifiedCnt(modifiedItemCnt);
return namespaceBO;
}
1.3 发布配置
url:http://localhost:8070/apps/apollo-demo/envs/DEV/clusters/default/namespaces/application/releases
参数:{“releaseTitle”:“20220305225621-release”,“releaseComment”:“发布删除的111”,“isEmergencyPublish”:false}
public ReleaseDTO createRelease(@PathVariable String appId,
@PathVariable String env, @PathVariable String clusterName,
@PathVariable String namespaceName, @RequestBody NamespaceReleaseModel model) {
model.setAppId(appId);
model.setEnv(env);
model.setClusterName(clusterName);
model.setNamespaceName(namespaceName);
if (model.isEmergencyPublish() && !portalConfig.isEmergencyPublishAllowed(Env.valueOf(env))) {
throw new BadRequestException(String.format("Env: %s is not supported emergency publish now", env));
}
// 插入release记录
ReleaseDTO createdRelease = releaseService.publish(model);
ConfigPublishEvent event = ConfigPublishEvent.instance();
event.withAppId(appId)
.withCluster(clusterName)
.withNamespace(namespaceName)
.withReleaseId(createdRelease.getId())
.setNormalPublishEvent(true)
.setEnv(Env.valueOf(env));
// 发出发布event
publisher.publishEvent(event);
return createdRelease;
}
releaseService.publish(model) 调用adminService 中的插入release记录。adminService代码如下:
@PostMapping("/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/releases")
public ReleaseDTO publish(@PathVariable("appId") String appId,
@PathVariable("clusterName") String clusterName,
@PathVariable("namespaceName") String namespaceName,
@RequestParam("name") String releaseName,
@RequestParam(name = "comment", required = false) String releaseComment,
@RequestParam("operator") String operator,
@RequestParam(name = "isEmergencyPublish", defaultValue = "false") boolean isEmergencyPublish) {
Namespace namespace = namespaceService.findOne(appId, clusterName, namespaceName);
if (namespace == null) {
throw new NotFoundException(String.format("Could not find namespace for %s %s %s", appId,
clusterName, namespaceName));
}
// 保存发布记录
Release release = releaseService.publish(namespace, releaseName, releaseComment, operator