Apollo 配置中心源码分析

本文深入分析了Apollo配置中心的源码,包括配置的发布和通知过程,四个核心模块的功能,以及客户端如何实现配置的实时通知和定时拉取。详细探讨了Apollo的页面修改配置、发布配置的步骤,以及客户端如何接收和更新配置。
摘要由CSDN通过智能技术生成

Apollo 配置中心源码分析

​ Apollo是携程开源的一款分布式配置管理中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性,适用于微服务配置管理场景。

Apollo配置发布和通知的过程
  1. 用户在配置中心对配置进行修改并发布

  2. 配置中心通知Apollo客户端有配置更新

  3. Apollo客户端从配置中心拉取最新的配置、更新本地配置并通知到应用

image-20220301221014007

从Apollo模块看配置发布流程

image-20220305161457345

Apollo四个核心模块及其主要功能
  1. ConfigService

    • 提供配置获取接口
    • 提供配置推送接口
    • 服务于Apollo客户端
  2. AdminService

    • 提供配置管理接口
    • 提供配置修改发布接口
    • 服务于管理界面Portal
  3. Client

    • 为应用获取配置,支持实时更新
    • 通过MetaServer获取ConfigService的服务列表
    • 使用客户端软负载SLB方式调用ConfigService
  4. 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

主要分为一下几个部分

  1. 页面发布配置(新增,修改和删除)
  2. configService获取到新发布的配置信息
  3. configService通知客户端最新的配置变更
  4. 客户端的同步更新Spring容器中注入的@Value的值
  5. 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(配置的备注信息),可以根据上面信息查询到配置信息。

image-20220305170636763

commit表中的信息。

image-20220305170732235

每次修改配置都会新插入一条记录。其中changSets记录了这次变更的类型和内容。

image-20220305171225028

每个changeSets中会按照createItemsupdateItemsdeleteItems分别记录了新增,修改和删除的配置项。每个分类里面又会记录具体的新增,修改和删除的具体配置信息。

1.2 查询配置列表

url:http://localhost:8070/apps/apollo-demo/envs/DEV/clusters/default/namespaces

image-20220305171521709

列表分别显示了有两条配置修改了,但是没有发布。在上面标记了未发布的标签。这个是怎么判断的呢?

我们一起看下源码吧。根据上面的地址,我们去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表示配置是否修改,但是没有发布。如果修改了,里面还会包含修改前后的值。

image-20220305173014035

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
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值