微服务系列(三)如何选择配置中心

微服务系列(三)如何选择配置中心

前面通过源码分析过了注册中心的实现,本文继续思考如何选择【微服务基础设施组件:配置中心】

对于注册中心,要对于项目特性,并结合注册中心的功能和实现特性来决定匹配度,那么对于配置中心,我们最应该了解的则是其配置信息的组织模式(决定系统的扩展性、可维护性等)、数据的存储方式和通信方式(决定性能、响应速度)、以及组件之间的契合度(笔者认为,如果两个组件之间是完全解耦的又不需要较多的额外开发就能配合工作的话,那么就认为两者的契合度很高)。

git上有很多开源的优秀的配置中心的实现,本文仅针对Apollo、Nacos以及Spring Cloud Config来介绍和分析,本质上spring cloud config应与前面两者划分开。

Apollo:呼声很高,功能很强大,较成熟

Nacos:合并了注册中心的功能,开发中

Spring Cloud Config:知名度高,使用者广泛

前言

由于配置中心的底层实现与注册中心的实现有很多相似之处,笔者将会减少源码分析的部分,重点分析配置中心可能面临的难题以及一些常用的解决方案。

Apollo

Apollo的README.md中有描述这样几个不错的特性(附上界面图):

  • 统一管理不同环境、不同集群的配置
    • Apollo提供了一个统一界面集中式管理不同环境(environment)、不同集群(cluster)、不同命名空间(namespace)的配置。
    • 同一份代码部署在不同的集群,可以有不同的配置,比如zk的地址等
    • 通过命名空间(namespace)可以很方便的支持多个不同应用共享同一份配置,同时还允许应用对共享的配置进行覆盖
  • 配置修改实时生效(热发布)
    • 用户在Apollo修改完配置并发布后,客户端能实时(1秒)接收到最新的配置,并通知到应用程序。
  • 版本发布管理
    • 所有的配置发布都有版本概念,从而可以方便的支持配置的回滚。
  • 灰度发布
    • 支持配置的灰度发布,比如点了发布后,只对部分应用实例生效,等观察一段时间没问题后再推给所有应用实例。
  • 权限管理、发布审核、操作审计
    • 应用和配置的管理都有完善的权限管理机制,对配置的管理还分为了编辑和发布两个环节,从而减少人为的错误。
    • 所有的操作都有审计日志,可以方便的追踪问题。
  • 客户端配置信息监控
    • 可以方便的看到配置在被哪些实例使用
  • 提供Java和.Net原生客户端
    • 提供了Java和.Net的原生客户端,方便应用集成
    • 支持Spring Placeholder, Annotation和Spring Boot的ConfigurationProperties,方便应用使用(需要Spring 3.1.1+)
    • 同时提供了Http接口,非Java和.Net应用也可以方便的使用
  • 提供开放平台API
    • Apollo自身提供了比较完善的统一配置管理界面,支持多环境、多数据中心配置管理、权限、流程治理等特性。
    • 不过Apollo出于通用性考虑,对配置的修改不会做过多限制,只要符合基本的格式就能够保存。
    • 在我们的调研中发现,对于有些使用方,它们的配置可能会有比较复杂的格式,如xml, json,需要对格式做校验。
    • 还有一些使用方如DAL,不仅有特定的格式,而且对输入的值也需要进行校验后方可保存,如检查数据库、用户名和密码是否匹配。
    • 对于这类应用,Apollo支持应用方通过开放接口在Apollo进行配置的修改和发布,并且具备完善的授权和权限控制
  • 部署简单
    • 配置中心作为基础服务,可用性要求非常高,这就要求Apollo对外部依赖尽可能地少
    • 目前唯一的外部依赖是MySQL,所以部署非常简单,只要安装好Java和MySQL就可以让Apollo跑起来
    • Apollo还提供了打包脚本,一键就可以生成所有需要的安装包,并且支持自定义运行时参数

在这里插入图片描述

可以看到,基本涵盖了大部分生产用的功能,是一个比较成熟的中间件,下面就继续了解它的API接入文档。

  • 为了支持高可用,需要配置meta server
  • 支持SPI扩展,用户可自定义寻址逻辑
  • 可配置集群(集群的概念是某命名空间下的多个服务组成的集合)
  • Java API接入方式,client模块提供了ConfigService接口提供服务,包括配置拉取、配置监听等功能
  • 官方提供了Spring整合Apollo client的文档,但没有提供接入Spring Cloud的实现
  • 仅支持xml、properties、xml文件格式的配置
  • 提供迁徙方案

大致整理了一下,下面通过源码重点了解以下几个部分:

  1. 配置meta server来支持的高可用是如何实现的,可能存在哪些问题
  2. 配置的拉取是通过什么协议通信,配置监听又是如何实现的
  3. Apollo client会对配置文件进行本地文件缓存,是如何缓存的呢
  4. 权限控制采用了什么模式
  5. 客户端配置信息监控是如何做到的
  6. Apollo server端的配置信息是如何存储的

从官网推荐的文章上找到的一张Apollo架构图,好让我们初步的认识它

在这里插入图片描述

这张架构图可以简单回答问题1、4、6:

Config Service提供配置信息的管理,它的高可用依赖于Eureka集群和Meta Service,Meta Service起中转请求的作用,接收发现Config Service和Admin Service服务的请求,并向Eureka请求服务列表并返回给客户端。

运维相关的控制由Admin Service承担,它依赖于Config DB和Portal DB。

Config Service则依赖于Config DB,可以猜测到配置信息也是以表记录的形式存储在数据库中。

下面就用代码说话,完全理解Apollo的运行机制。

->apollo-configservice模块

有metaservice和configservice两个部分,在metaservice中发现configservice和adminservice服务发现的方式是http协议通信的方式。

@RestController
@RequestMapping("/services")
public class ServiceController {

  private final DiscoveryService discoveryService;

  private static Function<InstanceInfo, ServiceDTO> instanceInfoToServiceDTOFunc = instance -> {
    ServiceDTO service = new ServiceDTO();
    service.setAppName(instance.getAppName());
    service.setInstanceId(instance.getInstanceId());
    service.setHomepageUrl(instance.getHomePageUrl());
    return service;
  };

  public ServiceController(final DiscoveryService discoveryService) {
    this.discoveryService = discoveryService;
  }


  @RequestMapping("/meta")
  public List<ServiceDTO> getMetaService() {
    List<InstanceInfo> instances = discoveryService.getMetaServiceInstances();
    List<ServiceDTO> result = instances.stream().map(instanceInfoToServiceDTOFunc).collect(Collectors.toList());
    return result;
  }

  @RequestMapping("/config")
  public List<ServiceDTO> getConfigService(
      @RequestParam(value = "appId", defaultValue = "") String appId,
      @RequestParam(value = "ip", required = false) String clientIp) {
    List<InstanceInfo> instances = discoveryService.getConfigServiceInstances();
    List<ServiceDTO> result = instances.stream().map(instanceInfoToServiceDTOFunc).collect(Collectors.toList());
    return result;
  }

  @RequestMapping("/admin")
  public List<ServiceDTO> getAdminService() {
    List<InstanceInfo> instances = discoveryService.getAdminServiceInstances();
    List<ServiceDTO> result = instances.stream().map(instanceInfoToServiceDTOFunc).collect(Collectors.toList());
    return result;
  }
}

并且metaservice依赖了eureka的client,向eureka获取服务列表。

@Service
public class DiscoveryService {

  private final EurekaClient eurekaClient;

  public DiscoveryService(final EurekaClient eurekaClient) {
    this.eurekaClient = eurekaClient;
  }

  public List<InstanceInfo> getConfigServiceInstances() {
    Application application = eurekaClient.getApplication(ServiceNameConsts.APOLLO_CONFIGSERVICE);
    if (application == null) {
      Tracer.logEvent("Apollo.EurekaDiscovery.NotFound", ServiceNameConsts.APOLLO_CONFIGSERVICE);
    }
    return application != null ? application.getInstances() : Collections.emptyList();
  }

  public List<InstanceInfo> getMetaServiceInstances() {
    Application application = eurekaClient.getApplication(ServiceNameConsts.APOLLO_METASERVICE);
    if (application == null) {
      Tracer.logEvent("Apollo.EurekaDiscovery.NotFound", ServiceNameConsts.APOLLO_METASERVICE);
    }
    return application != null ? application.getInstances() : Collections.emptyList();
  }

  public List<InstanceInfo> getAdminServiceInstances() {
    Application application = eurekaClient.getApplication(ServiceNameConsts.APOLLO_ADMINSERVICE);
    if (application == null) {
      Tracer.logEvent("Apollo.EurekaDiscovery.NotFound", ServiceNameConsts.APOLLO_ADMINSERVICE);
    }
    return application != null ? application.getInstances() : Collections.emptyList();
  }
}

跟我们平时写的业务代码没有什么分别…

也就是说,Config Service和Admin Service的高可用是依赖于eureka来解决的,由于这两个服务的数据存储依赖于数据库,所以无需进行数据同步,只需要保证连接同一数据源即可,对于客户端而言,无论请求到哪一台service,最终均会根据同一数据源的数据来进行处理返回,从而解决单点故障的问题。

那么继续看,是否完全依赖数据库?

从configservice包可以看到,其对外提供服务依旧是通过http接口,服务层有一个核心服务类com.ctrip.framework.apollo.configservice.service.config.ConfigService

它有两种实现类,com.ctrip.framework.apollo.configservice.service.config.ConfigServiceWithCachecom.ctrip.framework.apollo.configservice.service.config.DefaultConfigService

从controller出发

->com.ctrip.framework.apollo.configservice.service.config.ConfigService#loadConfig

->com.ctrip.framework.apollo.configservice.service.config.AbstractConfigService#loadConfig

->com.ctrip.framework.apollo.configservice.service.config.AbstractConfigService#findRelease

->com.ctrip.framework.apollo.configservice.service.config.DefaultConfigService#findActiveOnecom.ctrip.framework.apollo.configservice.service.config.DefaultConfigService#findLatestActiveRelease

最终定位到com.ctrip.framework.apollo.biz.service.ReleaseService

内部持有一个Repository类com.ctrip.framework.apollo.biz.repository.ReleaseRepository

(ps. 这里依赖了spring data模块的repository接口)

最终发现,其通过对数据库操作来获取配置信息,验证猜想。

值得一提的是ConfigServiceWithCache,它是有缓存的实现类,它的缓存机制实现的非常简单,通过在JVM中持有map并定期轮询数据库来更新,它通过参数config-service.cache.enabled决定是否开启。

到这里,验证了问题1、6的猜测,并且可以总结出问题1、6的答案:

  1. Apollo的高可用依赖于eureka集群和数据库服务,首先要保证数据库服务(可以为集群)稳定运行,一旦数据库服务出现问题,那么Apollo上层Config Service和Admin Service均无法正常提供服务,从Config Service的实现可以看到,其通过增加缓存来保护这一情况的发生,一旦数据库服务出现问题,那么写服务将瘫痪但读服务依旧可以正常使用;其次其依赖eureka集群来进行服务发现,eureka集群通过冗余备份的方式来保证高可用,虽然eureka和数据库服务的可靠性非常高,但apollo依旧依赖了较多的第三方服务,其可靠性也因此降低。
  2. 目前看来,Apollo的数据存储(至少配置信息和权限信息)是依赖于数据库存储,同时提供了缓存,执行效率是比较高的,但也可能因此成为性能瓶颈,数据库的读写性能取决于IO性能,可能会因为索引重建等问题而成为apollo的性能瓶颈,但目前来说还遇不到像消息系统那样规模的场景,所以实际上在性能上没有太大的分别。

继续了解权限部分是如何控制的,首先我们知道apollo是依赖数据库来进行数据存储的,那么权限部分很可能就是通过表设计来控制,首先找到提供权限控制的入口:

com.ctrip.framework.apollo.portal.spi.configuration.AuthConfiguration

com.ctrip.framework.apollo.portal.spi.springsecurity.SpringSecurityUserService

可以发现,它依赖了spring security框架,在其上实现权限控制的效果,权限控制关系由这几个表表示:

Users Authorities Permission Role RolePermission

这里就不介绍了。

以上均是apollo服务端的实现,接着进入客户端的实现部分。

com.ctrip.framework.apollo.internals.DefaultConfigManager

这个类用于与服务端通信来获取配置信息

个人觉得这部分的实现设计的很好,美中不足在于没有抽象到接口

例如这里有RepositoryFactoryManager/FactoryManagerConfig/ConfigFile四个概念

最外层是Service,service对外提供服务

service内部,由Manager管理配置,这一层级存在一级缓存(JVM层)

Manager内部存在FactoryManager用于管理不同namespace的Factory,这样就很好的隔离了不同namespace

每个Factory则仅负责该namespace下的Config的创建

每个Config内部抽象了自动刷新的功能,监听了配置变化

而与远程通信的部分由Repository承担,Repository有本地实现和远程实现,这里使用了装饰者模式

Config内部持有一个LocalFileConfigRepository

LocalFileConfigRepository内部持有RemoteConfigRepository

这样又很好的解耦了远程与本地的配置信息管理

那么与服务端的通信都封装在RemoteConfigRepository

感兴趣的可以直接看这段逻辑com.ctrip.framework.apollo.internals.RemoteConfigRepository#sync

->com.ctrip.framework.apollo.internals.RemoteConfigRepository#loadApolloConfig

->HttpResponse<ApolloConfig> response = m_httpUtil.doGet(request, ApolloConfig.class);

最终定位到【老套路】,发起http请求,获取响应,解析…

那么监听又是如何实现的?

当本地配置仓库同步信息成功,感应到配置信息变更后会调用

->com.ctrip.framework.apollo.internals.LocalFileConfigRepository#onRepositoryChange

->com.ctrip.framework.apollo.internals.AbstractConfigRepository#fireRepositoryChange

触发listeners集合的onRepositoryChange()方法

而listeners集合是在Config被构造出来的时候进行初始化的过程中将自身作为监听器add进去,这样就实现了一个【Config内部抽象了自动刷新的功能,监听了配置变化】的Config对象

触发同步的时机有这样几种情况:

  1. 初始化时同步一次
  2. 获取配置时如果没有命中缓存,则同步一次
  3. 定期同步(默认5秒)

这样就基本保证了客户端尽快感知到配置变更情况。(官方称实时性在1s以内)

而在这段代码的阅读中,对于问题5也得到了回答:

以sync过程为例:

Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "syncRemoteConfig");
...
try {
      ...

      transaction.setStatus(Transaction.SUCCESS);
    } catch (Throwable ex) {
      transaction.setStatus(ex);
      throw ex;
    } finally {
      transaction.complete();
    }

由一个Transaction对象来存储监控信息。

通过反射构造Transaction对象的Factory对象的Method对象,这里听起来有点绕,不理解的话最好是追踪一下源码。

最终由Method对象来构造Transaction对象。

到这里,只剩下最后一个client如何进行文件缓存的问题:

前面说到本地配置信息的类是com.ctrip.framework.apollo.internals.LocalFileConfigRepository

直接进入这个类,一切谜底就揭晓了…

-> om.ctrip.framework.apollo.internals.LocalFileConfigRepository#updateFileProperties

-> com.ctrip.framework.apollo.internals.LocalFileConfigRepository#persistLocalCacheFile

->java.util.Properties#store(java.io.OutputStream, java.lang.String)

最终将Properties类信息写入文件,读文件反之

->com.ctrip.framework.apollo.internals.LocalFileConfigRepository#loadFromLocalCacheFile

->java.util.Properties#load(java.io.InputStream)

写的时机:

  1. 尝试同步上游仓库(远程仓库)的信息,发现信息有变更时,写入本地缓存
  2. 监听到仓库配置信息变化时,写入本地缓存(可以看出,这样做的效率很低,如果频繁更改则会频繁IO)

读的时机:

  1. 当读取本地仓库的信息时,发现未命中缓存,则发起一次同步,同步后依然没有命中,则读本地缓存文件并同步到本地仓库

总结以上六个问题:

  1. 配置meta server来支持的高可用是如何实现的,可能存在哪些问题
  2. 配置的拉取是通过什么协议通信,配置监听又是如何实现的
  3. Apollo client会对配置文件进行本地文件缓存,是如何缓存的呢
  4. 权限控制采用了什么模式
  5. 客户端配置信息监控是如何做到的
  6. Apollo server端的配置信息是如何存储的

答:

  1. Config Service提供配置信息的管理,它的高可用依赖于Eureka集群和Meta Service,Meta Service起中转请求的作用,接收发现Config Service和Admin Service服务的请求,并向Eureka请求服务列表并返回给客户端。
  2. http协议,监听是通过定时轮询+主动拉取的方式
  3. 使用java.util.Properties类,直接调用store、load方法来读写(每次监听到变化均会写,变化频繁时系统压力较大)
  4. apollo的权限控制基于Spring Security框架实现,并依赖了数据库
  5. 通过com.ctrip.framework.apollo.tracer.Tracer类来记录信息,具体实现可自行跟踪源码
  6. 依赖数据库,目前支持mysql,数据持久层使用了Spring Data的Repository。

ps.吐槽一句,apollo的代码还是非常适合新手看的,典型的web MVC的风格,大量依赖Spring框架,底层的抽象部分做的还是欠缺了一些,可能最开始设计的时候没有考虑过多的情况,但从结果上看还是非常成功的。

Nacos

关于Nacos的config server的实现,相对于naming server简单的多。

目前Nacos已发布第一个RC版本:1.0.0-RC1

那么源码部分的分析以该版本为准。

要知道,作为开发者,需要构建一个稳定可靠的微服务环境,对于配置中心这样的公用基础设施,最为重要的则是服务的可用性。

基于以下几个核心问题的考量来分析:

  1. 高可用如何做?
  2. 接入方式如何?
  3. C/S间采用怎样的通信协议?
  4. 提供了怎样的特性?

首先从官网找入口,Config Service的sdk入口:

https://nacos.io/zh-cn/docs/sdk.html

定位到client模块的com.alibaba.nacos.api.config.ConfigService这个接口

继续进入它的实现类com.alibaba.nacos.client.config.NacosConfigService

以getConfig为例追踪

->com.alibaba.nacos.client.config.NacosConfigService#getConfig

->com.alibaba.nacos.client.config.NacosConfigService#getConfigInner

->(如果在本地缓存没有找到)com.alibaba.nacos.client.config.impl.ClientWorker#getServerConfig

->result = agent.httpGet(Constants.CONFIG_CONTROLLER_PATH, null, params, agent.getEncode(), readTimeout);

->最终调用HttpSimpleClient.httpGet(),可以发现其底层封装了HttpURLConnection来进行http请求

那么可以追踪到请求的url为 /v1/cs/configs

通过全局搜索,定位到com.alibaba.nacos.config.server.controller.ConfigController

定位到方法->com.alibaba.nacos.config.server.controller.ConfigController#getConfig

->com.alibaba.nacos.config.server.controller.ConfigServletInner#doGetConfig

->(如果内存缓存中没有找到)

if (STANDALONE_MODE && !PropertyUtil.isStandaloneUseMysql()) {
    configInfoBase = persistService.findConfigInfo4Beta(dataId, group, tenant);
} else {
    file = DiskUtil.targetBetaFile(dataId, group, tenant);
}

这段代码告诉我们,当使用单机模式且不使用mysql作为数据源时,使用persistService获取,而persisteService则是以debyDB为数据源来实现的,即依赖debyDB数据库来进行数据存储;当处于集群模式或使用mysql作为数据源时,从文件中获取配置信息,并通过transferTo的API将文件中的内容写入响应outputStream。

(这里使用了零拷贝提高性能)

到这里,还是疑惑的,没有看到有关于可用性的代码部分,如果有多个节点,文件之间是如何保持同步的呢?还是采用冗余备份+低一致性的方式呢?

那么,就要知道这个文件是如何产生的。

笔者这里直接贴出来入口:com.alibaba.nacos.config.server.service.dump.DumpProcessor#process

定位的过程就不做介绍了,大概想法是笔者认为既然没有找到节点间数据同步的部分,那么很可能是依赖数据库的可用性,文件只是作为缓存而存在。

ConfigInfo4Beta cf = dumpService.persistService.findConfigInfo4Beta(dataId, group, tenant);
boolean result;
if (null != cf) {
    result = ConfigService.dumpBeta(dataId, group, tenant, cf.getContent(), lastModified, cf.getBetaIps());
    if (result) {
        ConfigTraceService.logDumpEvent(dataId, group, tenant, null, lastModified, handleIp,
            ConfigTraceService.DUMP_EVENT_OK, System.currentTimeMillis() - lastModified,
            cf.getContent().length());
    }
} else {
    result = ConfigService.removeBeta(dataId, group, tenant);
    if (result) {
        ConfigTraceService.logDumpEvent(dataId, group, tenant, null, lastModified, handleIp,
            ConfigTraceService.DUMP_EVENT_REMOVE_OK, System.currentTimeMillis() - lastModified, 0);
    }
}

最终猜想验证,Nacos的高可用依赖于数据库,如果是单机模式且不使用mysql的情况,则无需进行文件缓存,直接从数据源获取;而如果是集群模式或使用mysql的情况,则启动dump线程,定期将数据源中的信息dump到文件(作为缓存,仅提供读),值得注意的是,为了保证数据,不能仅仅通过定期dump来保持文件和数据源之间的数据同步,而是写入数据库的同时通过异步通知来令数据写入文件缓存。

ps.那么就会存在一个小问题,如果server cpu压力大或IO压力大的情况下,可能存在写入配置后不能第一时间感知到配置的变更。

继续思考问题2,这个问题其实官网已经给出了答案,前文的https://nacos.io/zh-cn/docs/sdk.html。

Nacos提供了client模块,由用户来构建ConfigService对象,并使用其上API来接入。

这样的方式与Apollo一致,比起直接提供restful api而言更加方便,无需用户封装http client请求,对用户更加友好。

另外,在Spring Cloud Alibaba工程也提供了Spring Cloud Alibaba Config的接入方式,用户可以通过直接引入jar包,无需配置、直接接入。

指的注意的是,Nacos提供了两种部署方式:

一、Naming Server和Config Server合并部署

二、Naming Server和Config Server分离部署

于是,Spring Cloud Alibaba依然提供了Spring Cloud Alibaba Config Server的支持,这样就大大简化了Spring Cloud工程的接入。

到这里问题3得到了部分的回答,为什么说是部分呢?虽然Config Server对外提供的大部分功能是使用了http协议,但其实现了配置监听的功能,需要了解其监听的方式是如何实现的,这样才能完整的回答问题3。

首先读完前文,我们知道了Apollo的监听实现方式采用了定时轮询+(缓存未命中时)主动同步拉取的方式。

继续追踪Nacos监听配置的过程:

->com.alibaba.nacos.client.config.NacosConfigService#addListener

->com.alibaba.nacos.client.config.impl.ClientWorker#addTenantListeners

->com.alibaba.nacos.client.config.impl.CacheData#addListener

->listeners.addIfAbsent(wrap)

这里实现的过程比较复杂,就不贴源码了。

最终将listener包装后(先不论包装类起了作用)添加到CacheData持有的listeners中。

(每个CacheData对应了由dataId+group组装成的key,即某一数据中心下的某个组的配置的数据均存储在一个CacheData中。)

并开启多个LongPollingRunnable(一个task对应处理多个CacheData)任务,不断轮询,从Config Server拉取信息并写入其对应的CacheData,一旦检测到CacheData的内容发生了变化,则回调一次(用户自定义逻辑的)监听器,并且Nacos将长轮询任务使用的线程池和通知任务使用的线程池分离,异步通知。

ps.这里使用了MD5来区分内容是否发生变化,主要考量是MD5的加密速度较快,而无需使用equals的方式在每次变化时都要解析一次,这样的加速了比较内容是否发生变化的过程。

可以看到,与Apollo的方式有些不同,Nacos采用长轮询的方式监听配置的变化。

可以看到两者的效果异同之处:

  1. Nacos使用Cache线程池,且无间断的轮询,那么基本可以保证第一时间监听到配置变化,但如果不同CacheData的监听器过多的情况下,则会发起多个无间隔长轮询任务,且会无限制的发起新的轮询任务,造成客户端大量时间令CPU空转,浪费CPU资源。
  2. Apollo则由于使用了同步加锁的方式,并舍弃了部分响应速度来保证系统的性能,相对而言的问题也很明显,配置监听的响应时间不够快,且如果出现某客户端不断获取服务端不存在的配置信息时,则会加大网络IO的压力,另一方面,Apollo比对内容异同的方式是采用equals,相对MD5的方式性能相对较弱。

ps.所以本质上监听的方式也是使用了http协议,周期性请求。

而对于问题4:

相对Apollo,Nacos没有配置继承的概念,从实现上看,后获取的配置会覆盖先获取的配置。

Nacos的接入方式、部署方式相对容易的多。

从代码上看,Nacos在很多地方运用了异步,看起来能提高系统的吞吐量,但还需要经历考验。

所以,似乎也没有特别亮眼的特性,或许AP/CP/MIX模式是一个比较容易引起注意的功能,但目前看来似乎没有太大的优势,这么做仅仅是为了旧版本兼容。

不过,Nacos官网提供了多注册中心的同步组件https://nacos.io/zh-cn/docs/nacos-sync.html,用于迁徙还是非常方便的。

Spring Cloud Config

由于Spring Cloud Config是基于Spring Boot来开发的,源码分析的部分将会减少,读者可以根据需要阅读Spring及Spring Boot源码。

使用Spring Cloud Config的方式很简单,在一个Spring Boot应用上使用@EnableConfigServer注解,即激活启动Spring Cloud Config Server。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(ConfigServerConfiguration.class)
public @interface EnableConfigServer {

}

可以看到其导入了ConfigServerConfiguration.class

@Configuration
public class ConfigServerConfiguration {
   class Marker {}

   @Bean
   public Marker enableConfigServerMarker() {
      return new Marker();
   }
}

并通过内部类Marker开启ConfigServerAutoConfiguration自动配置

@Configuration
@ConditionalOnBean(ConfigServerConfiguration.Marker.class)
@EnableConfigurationProperties(ConfigServerProperties.class)
@Import({ EnvironmentRepositoryConfiguration.class, CompositeConfiguration.class, ResourceRepositoryConfiguration.class,
      ConfigServerEncryptionConfiguration.class, ConfigServerMvcConfiguration.class })
public class ConfigServerAutoConfiguration {

}

到这里可以看到,最终进行了一些配置,而主要初始化的内容包括:

  • EnvironmentRepositoryConfiguration:配置中心核心类的配置和注入,包含了各种具体实现,如Git、fileSystem、Db、Consul
  • CompositeConfiguration:如果存在多个配置中心的实现,则进行该配置,将各个实现类综合到一个类中来管理
  • ResourceRepositoryConfiguration:资源仓库的配置,用于加载并读取本地文件
  • ConfigServerEncryptionConfiguration:加密配置,将publicKey等加密信息通过http接口的形式提供给其他客户端(保证仅有集群内的节点能对信息进行解读)
  • ConfigServerMvcConfiguration:MVC配置,将EnvironmentController(提供配置获取等接口)、ResourceController(提供资源获取等接口)注入到Spring容器

大致了解了其配置的内容后,进入核心的配置类EnvironmentRepositoryConfiguration了解一下其实现:

以其默认实现git为例,从代码逻辑上看,默认配置下生效并注入的代码有以下

	@Bean
	@ConditionalOnProperty(value = "spring.cloud.config.server.health.enabled", matchIfMissing = true)
	public ConfigServerHealthIndicator configServerHealthIndicator(
			EnvironmentRepository repository) {
		return new ConfigServerHealthIndicator(repository);
	}

	@Bean
	@ConditionalOnMissingBean(search = SearchStrategy.CURRENT)
	public MultipleJGitEnvironmentProperties multipleJGitEnvironmentProperties() {
		return new MultipleJGitEnvironmentProperties();
	}

	@Configuration
	@ConditionalOnMissingBean(EnvironmentWatch.class)
	protected static class DefaultEnvironmentWatch {

		@Bean
		public EnvironmentWatch environmentWatch() {
			return new EnvironmentWatch.Default();
		}
	}

    @Configuration
    @ConditionalOnClass(TransportConfigCallback.class)
    static class JGitFactoryConfig {

        @Bean
        public MultipleJGitEnvironmentRepositoryFactory gitEnvironmentRepositoryFactory(
                ConfigurableEnvironment environment, ConfigServerProperties server,
                Optional<ConfigurableHttpConnectionFactory> jgitHttpConnectionFactory,
                Optional<TransportConfigCallback> customTransportConfigCallback) {
            return new MultipleJGitEnvironmentRepositoryFactory(environment, server, jgitHttpConnectionFactory,
					customTransportConfigCallback);
        }
    }

    @Configuration
    @ConditionalOnClass({ HttpClient.class, TransportConfigCallback.class })
    static class JGitHttpClientConfig {

        @Bean
        public ConfigurableHttpConnectionFactory httpClientConnectionFactory() {
            return new HttpClientConfigurableHttpConnectionFactory();
        }
    }

@Configuration
@ConditionalOnMissingBean(value = EnvironmentRepository.class, search = SearchStrategy.CURRENT)
class DefaultRepositoryConfiguration {
	@Autowired
	private ConfigurableEnvironment environment;

	@Autowired
	private ConfigServerProperties server;

	@Autowired(required = false)
	private TransportConfigCallback customTransportConfigCallback;

	@Bean
	public MultipleJGitEnvironmentRepository defaultEnvironmentRepository(
	        MultipleJGitEnvironmentRepositoryFactory gitEnvironmentRepositoryFactory,
			MultipleJGitEnvironmentProperties environmentProperties) throws Exception {
		return gitEnvironmentRepositoryFactory.build(environmentProperties);
	}
}

而new EnvironmentWatch.Default()是一个空实现,所以实际上并没有起作用。

该接口可用于实现配置监听,可以参考Consul的实现:org.springframework.cloud.config.server.environment.ConsulEnvironmentWatch

最终发现,默认配置下,其最终注入了这样一个类MultipleJGitEnvironmentRepository.class

且它是由MultipleJGitEnvironmentRepositoryFactory的build方法,并传入MultipleJGitEnvironmentProperties参数构造出的bean。

org.springframework.cloud.config.server.environment.MultipleJGitEnvironmentRepositoryFactory#build

从方法中可以看到,对MultipleJGitEnvironmentRepository进行了transport相关配置。

进入org.springframework.cloud.config.server.environment.MultipleJGitEnvironmentRepository这个核心类

首先不关心其实现(无非就是配置的存储、解析、读取),作为Spring家族的成员,最应该学习其思想,先观察其结构:

![Spring Cloud Config类图](/img/Spring Cloud Config类图.jpg)

从名称可以看出,

Abstract作为模板抽象类

JGit*作为git的原生实现类

Multiple*则作为聚合多个JGit的实现类

且均实现了SearchPathLocator接口,该接口用于定位本地git仓库的路径

ResourceLoaderAware则是用于注入ResourceLoader,可以进行资源加载的操作。

现在进入MultipleJGitEnvironmentRepository类,可以发现其仅实现了afterPropertiesSet()、getLocations()、findOne()

从实现上看也可以很容易了解JGit实现逻辑,一顿git的命令,拉到本地然后加载到内存(由于利用了git,可以比对版本,所以只有在版本有变更时拉取最新数据并重新加载文件)。

可以发现,用git来进行配置的管理相对来说更加笨重,每次获取配置时都可能经历耗时的拉取和加载文件的操作,但通过阅读这部分源码,认识到Spring框架的强大之处。

假设我们需要实现带缓存的git配置中心,仅需要继承AbstractScmEnvironmentRepository并向Spring容器注入该bean,大部分工作均由Spring框架来完成了。

另外,还有一个需要注意的类,有助于我们基于Spring Cloud Config来实现定制化的配置中心:

ConfigServerHealthIndicator:用于心跳检测,Spring也做了非常方便的封装,我们可以通过继承org.springframework.boot.actuate.health.AbstractHealthIndicator来实现我们自己的心跳检测器

结合前文提到的另一个org.springframework.cloud.config.server.environment.EnvironmentWatch

就能实现配置中心这两个重要特性:

  1. 配置监听
  2. 心跳检测

另外,Spring环境中的配置信息是以org.springframework.core.env.PropertySources类存在,从Spring Cloud Config环境仓库获取到的配置信息会最终转换为PropertySource并插入到PropertySources中,形成Spring环境的一部分,由用户使用Spring 的方式(如@Value等)来使用。

小结

Apollo的实现虽然优雅程度不高,但五脏六腑俱全,成功的经历了很多考验,社区也很活跃,易于解决问题。

Nacos的实现从设计上看是可行的,性能等指标相比Apollo应该更具优势,且提供了方便的接入方案以及Spring Cloud Alibaba的支持,更加方便于Spring Cloud用户接入。

Spring Cloud Config则显得跟这两者没有可比性,本质上Spring Cloud Config也是提供了几种配置中心的实现,但无论是git还是svn,其实都是相对来说不太适合作为配置中心的实现,虽然其也实现了db,与Apollo和Nacos相比,在对db的管控、架构方面的考虑等方面都有所欠缺。

所以笔者认为Spring Cloud Config应该与Apollo、Nacos这样的配置中心中间件划分开,其虽然有默认的实现,本身能作为一个配置中心中间件存在,但其最大的优势是具备高度抽象的能力,可以在Spring应用中完美接入其他第三方中间件,例如其就实现了Consul。

ps.对于Spring Cloud应用,Nacos也提供了Spring Cloud Config的实现,感兴趣的可以关注https://github.com/spring-cloud-incubator/spring-cloud-alibaba

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章