Apollo - 分布式配置中心

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Apollo - 分布式配置中心相关的知识,希望对你有一定的参考价值。

参考技术A ​ Apollo(阿波罗)是携程框架部门研发的分布式配置中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性,适用于微服务配置管理场景。服务端基于Spring Boot和Spring Cloud开发,打包后可以直接运行,不需要额外安装Tomcat等应用容器。Java客户端不依赖任何框架,能够运行于所有Java运行时环境,同时对Spring/Spring Boot环境也有较好的支持。

了解 apollo 可以去 github 上 ,地址 https://github.com/ctripcorp/apollo

快速开始 : https://github.com/ctripcorp/apollo/wiki/Quick-Start

启动配置参数 : 优先级从高到低

其他参数 打通小异 :

如果需要关闭placeholder在运行时自动更新功能,可以通过以下方式关闭 apollo.autoUpdateInjectedSpringProperties=false

例如 redis.cache.expireSeconds 这样的key 存在 apollo服务器中 , 下面例子会自动将 expireSeconds 注入进去 , 但是这样有个问题就是 , 不会自动刷新配置 ..........

自动刷新 需要手动设置

可以将 application.yml 或 bootstrap.yml 换成 properties文件

可以看出 有个灰度列表 , 可以作为测试 发布出去 , 也可以取消

Apollo架构篇 - 分布式配置中心Apollo

简介

Apollo(阿波罗)是一款可靠的分布式配置管理中心,诞生于携程框架研发部,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性,适用于微服务配置管理场景。Apollo 采用 CP 架构。

配置中心原理

客户端向服务端发起一个获取配置信息的 Http 请求并将服务端返回的配置信息保存到本地磁盘与缓存中。

之后每隔 5 分钟发起一次获取配置信息的 Http 请求。这是一个 fallback 机制,为了防止推送机制失效导致配置不更新。可以在运行时指定系统参数 apollo.refreshInterval 来覆盖,单位为分钟。一般情况下,服务端都会返回 304 - Not Modified。

服务端接收到请求后,会保持住 60 秒。如果 60 秒之内有客户端关注的配置发生变化,则将客户端请求返回并告知客户端有配置发生变化的 namespace 信息。如果 60 秒之内没有客户端关注的配置发生变化,则返回 Http 状态码 304 给客户端。因为考虑到会有数万客户端向服务端发起长轮询,因此服务端使用了 Spring 的 DeferredResult 来处理客户端的长轮询请求。

客户端向服务端发起获取对应 namespace 的最新配置的 Http 请求。

ReleaseMessage 表的 Message 字段记录 appid + cluster + namespace 三个维度的信息。服务端通过对比客户端传递的 notificationId 与数据库的 ReleaseMessage 表中相同 appid+cluster+namespace 维度下的最大的主键 id 是否相等,来判断命名空间是否有配置发生了变化。

功能

Apollo 支持如下功能:

  • 统一管理不同环境、不同集群的配置

    Apollo 提供了一个统一界面集中式管理不同环境、不同集群、不同命名空间的配置。

    同一份代码部署在不同的集群,可以有不同的配置。

    通过命名空间可以很方便支持多个不同应用共享同一份配置,同时还允许应用对共享配置进行覆盖。

  • 配置修改实时生效(热发布)

    用户在 Apollo 修改完配置并发布后,客户端能实时(1秒)接收到最新的配置,并通知到应用程序。

  • 版本发布管理

    所有的配置发布都有版本概念,从而可以方便地支持配置的回滚。

  • 灰度发布

    支持配置的灰度发布,比如点了发布后,只对部分应用实例生效,等观察一段时间没问题后再推给所有的应用实例。

  • 权限管理、发布审核、操作审计

    应用和配置的管理都有完善的权限管理机制,对配置的管理还分为了编辑和发布两个环节,从而减少人为的错误。

    所有的操作都有审计日志,可以方便地追踪问题。

  • 客户端配置信息监控

    可以在界面上方便地看到配置在哪些实例使用。

  • 提供Java和.Net原生客户端

    提供了Java和.Net原生客户端,方便应用集成。

    支持 Spring Placeholder,Annotation 和 Spring Boot 的 ConfigurationProperties,方便应用使用。

    同时提供了 Http 接口,非 Java 和 .Net 应用也可以方便地使用。

  • 提供开放平台API

    Apollo 自身提供了比较完善的统一配置管理界面,支持多环境、多数据中心配置管理、权限、流程治理等特性。不过 Apollo 出于通用性考虑,不会对配置的修改做过多限制,只要符合基本的格式就能保存,不会针对不同的配置值进行针对性的校验。

  • 部署简单

    目前唯一的外部依赖是 MySQL,所以部署非常简单。

    Apollo 还提供了打包脚本,一键就可以生成所有需要的安装包,并且支持自定义运行时参数。

总体设计

Apollo 的总体设计如下:

Config Service 提供配置的读取、推送等功能,服务对象是 Apollo 客户端。

Admin Service 提供配置的修改、发布等功能,服务对象是 Apollo Portal(管理界面)。

Config ServiceAdmin Service 都是多实例、无状态部署,所以需要将自己注册到 Eureka 中并保持心跳。

Meta Server 在 Eureka 之上架了一层,用于封装 Eureka 的服务发现接口。

Client 通过域名访问 Meta Server 获取 Config Service 服务列表(IP+Port),而后直接通过 IP+Port 访问服务,同时在 Client 侧会做 load balance、错误重试。

Portal 通过域名访问 Meta Server 获取 Admin Service 服务列表(IP+Port),而后直接通过 IP+Port 访问服务,同时在 Portal 侧会做 load balance、错误重试。

为了简化部署,实际上会把 Config ServiceEurekaMeta Server 三个逻辑角色部署在同一个 jvm 进程中。

核心概念

Apollo 支持四个维度管理 key-value 格式的配置 - application(应用)、environment(环境)、cluster(集群)、namespace(命名空间)。

1、application(应用)

实际使用配置的应用,Apollo 客户端在运行时需要知道当前应用是谁,从而可以去获取对应的配置。

每个应用都需要唯一的身份标识,即 appId。

2、environment(环境)

配置对应的环境,Apollo 客户端在运行时需要知道当前应用处于哪个环境,从而可以去获取应用的配置。

3、cluster(集群)

一个应用下不同实例的分组。比如典型的可以按照数据中心分,把上海机房的应用实例分为一个集群,把北京机房的应用实例分为另一个集群。

对于不同的集群,同一个配置可以有不一样的值。

集群默认是通过读取机器上的配置(server.properties 中的 idc 属性)指定的,不过也支持运行时通过 System Property 指定。

4、namespace(命名空间)

一个应用下不同配置的分组,可以简单把命名空间类比为文件,不同类型的配置存放在不同的文件中。

应用可以直接读取到公共组件的配置。

应用也可以通过继承公共组件的配置来对公共组件的配置做出调整。

Namespace

什么是Namespace

Namespace 是配置项的集合,类似于一个配置文件的概念。

Apollo 在创建项目的时候,都会默认创建一个 application 的 namespace。对于 90% 的应用来说,application 的 namespace 已经满足日常配置的使用场景了。

客户端获取 application namespace 的代码如下:

Config config = ConfigService.getAppConfig();

客户端获取非 application namespace 的代码如下:

Config config = ConfigService.getConfig(namespace);

Namespace的格式

支持 properties、xml、yml、yaml、json 等,默认是 properties。

Namespace的获取权限

Namespace 的获取权限分为两种 - private(私有的)、public(公共的)。

获取权限是相对于 Apollo 客户端而言的。

private权限

private 权限的 namespace,只能被所属的应用获取到。一个应用尝试获取其它应用的 private 权限的 namespace,Apollo 会报 404 异常。

public权限

public 权限的 namespace,能被所有应用获取到。

Namespace的类型

Namespace 的类型有三种:私有类型、公共类型、关联类型(继承类型)。

私有类型

私有类型的 namespace 具有 private 权限。

公共类型

公共类型的 namespace 具有 public 权限。公共类型的 namespace 名称必须全局唯一。

使用场景:

  • 部门级别共享的配置
  • 小组级别共享的配置
  • 几个项目之间共享的配置
  • 中间件客户端的配置

关联类型

关联类型也称为继承类型,具有 private 权限。关联类型的 namespace 继承于指定的公共类型 namespace,用于覆盖公共 namespace 的某些配置。

例如公共的 namespace 有两个配置项

k1 = v1
k2 = v2

然后应用 a 有一个关联类型的 namespace 关联了此公共的 namespace,并且覆盖了配置项 k1,新值为 v3。那么在应用 a 实际运行时,获取到的公共 namespace 的配置为:

k1 = v3
k2 = v2

使用场景:

  • 对于默认的公共配置可以动态调整。

可用性

场景影响降级原因
某台 Config Service 下线无影响Config Service 无状态,客户端重连其它 Config Service
所有 Config Service 下线客户端无法读取最新配置,Portal 无影响客户端重启时,可以读取本地缓存配置文件
某台 Admin Service 下线无影响Admin Service 无状态,Portal 重连其它 Admin Service
所有 Admin Service 下线客户端无影响,Portal 无法更新配置
某台 Portal 下线无影响Portal 域名通过 slb 绑定多台服务器,重试后指向可用的服务器
全部 Portal 下线客户端无影响,Portal 无法更新配置
某个数据中心下线无影响多数据中心部署,数据完全同步,Meta Server/Portal 域名通过 slb 自动切换到其它存活的数据中心

源码分析 - 客户端

一、ApolloApplicationContextInitializer

实现了 ApplicationContextInitializer 接口。ApplicationContextInitializer 参考文章

在 Spring 容器初始化时,ApplicationContextInitializer 接口的所有实现类都会被实例化。在 Spring 容器刷新之前,调用 ApplicationContextInitializer 接口的所有实现类的 initialize 方法,可以对上下文做一些操作。

@Override
public void initialize(ConfigurableApplicationContext context) 
  ConfigurableEnvironment environment = context.getEnvironment();

  // 判断 apollo.bootstrap.enabled 属性值,默认 false
  if (!environment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED, Boolean.class, false)) 
    logger.debug("Apollo bootstrap config is not enabled for context , see property: $", context, PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED);
    return;
  
  logger.debug("Apollo bootstrap config is enabled for context ", context);

  initialize(environment);

接着看下 initialize 的重载方法的内部逻辑。

protected void initialize(ConfigurableEnvironment environment) 

  if (environment.getPropertySources().contains(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME)) 
    return;
  

  String namespaces = environment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_NAMESPACES, ConfigConsts.NAMESPACE_APPLICATION);
  logger.debug("Apollo bootstrap namespaces: ", namespaces);
  List<String> namespaceList = NAMESPACE_SPLITTER.splitToList(namespaces);

  CompositePropertySource composite = new CompositePropertySource(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME);
  for (String namespace : namespaceList) 
    // 获取指定命名空间的配置信息
    Config config = ConfigService.getConfig(namespace);

    composite.addPropertySource(configPropertySourceFactory.getConfigPropertySource(namespace, config));
  

  environment.getPropertySources().addFirst(composite);

二、ConfigService

public static Config getConfig(String namespace) 
  return s_instance.getManager().getConfig(namespace);

三、DefaultConfigManager

@Override
public Config getConfig(String namespace) 
  Config config = m_configs.get(namespace);

  if (config == null) 
    synchronized (this) 
      config = m_configs.get(namespace);

      if (config == null) 
        ConfigFactory factory = m_factoryManager.getFactory(namespace);

        config = factory.create(namespace);
        m_configs.put(namespace, config);
      
    
  

  return config;

使用双重检查锁,创建 Config 实例。

四、DefaultConfigFactory

@Override
public Config create(String namespace) 
  ConfigFileFormat format = determineFileFormat(namespace);
  // 如果配置文件是 yaml、yml 格式的
  if (ConfigFileFormat.isPropertiesCompatible(format)) 
    return new DefaultConfig(namespace, createPropertiesCompatibleFileConfigRepository(namespace, format));
  
  return new DefaultConfig(namespace, createLocalConfigRepository(namespace));

createLocalConfigRespository

LocalFileConfigRepository createLocalConfigRepository(String namespace) 
  // 判断环境是否是 LOCAL
  if (m_configUtil.isInLocalMode()) 
    logger.warn(
        "==== Apollo is in local mode! Won't pull configs from remote server for namespace  ! ====",
        namespace);
    return new LocalFileConfigRepository(namespace);
  
  return new LocalFileConfigRepository(namespace, createRemoteConfigRepository(namespace));

createRemoteConfigRepository

RemoteConfigRepository createRemoteConfigRepository(String namespace) 
  return new RemoteConfigRepository(namespace);

接下来分别分析 LocalFileConfigRepository、RemoteConfigRepository。

五、RemoteConfigRepository

先看下 RemoteConfigRepository 构造器方法。

public RemoteConfigRepository(String namespace) 
  m_namespace = namespace;
  m_configCache = new AtomicReference<>();
  m_configUtil = ApolloInjector.getInstance(ConfigUtil.class);
  m_httpUtil = ApolloInjector.getInstance(HttpUtil.class);
  m_serviceLocator = ApolloInjector.getInstance(ConfigServiceLocator.class);
  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(),
      m_configUtil.getOnErrorRetryInterval() * 8);
  // 客户端从Apollo配置中心服务端获取到应用的最新配置后,会保存在内存中,同时持久化到磁盘中
  // 应用程序可以从客户端获取最新的配置、订阅配置更新通知
  this.trySync();
  // 客户端定时调度处理
  this.schedulePeriodicRefresh();
  // 客户端长轮询处理
  this.scheduleLongPollingRefresh();

1、trySync 方法

首先看下 trySync 方法的处理逻辑。

protected boolean trySync() 
  try 
    sync();
    return true;
   catch (Throwable ex) 
    Tracer.logEvent("ApolloConfigException", ExceptionUtil.getDetailMessage(ex));
    logger.warn("Sync config failed, will retry. Repository , reason: ", this.getClass(), ExceptionUtil
            .getDetailMessage(ex));
  
  return false;

进入 sync 方法内部一窥究竟。

@Override
protected synchronized void sync() 
  Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "syncRemoteConfig");

  try 
    // 从 m_configCache 缓存中获取本地配置
    ApolloConfig previous = m_configCache.get();
    // 从服务端加载远程配置
    ApolloConfig current = loadApolloConfig();

    // 如果本地配置与远程配置不一致,即远程配置发生了变化
    if (previous != current) 
      logger.debug("Remote Config refreshed!");
      // 更新 m_configCache 缓存
      m_configCache.set(current);
      // 回调所有 RepositoryChangeListener 的 onRepositoryChange 方法
      this.fireRepositoryChange(m_namespace, this.getConfig());
    

    if (current != null) 
      Tracer.logEvent(String.format("Apollo.Client.Configs.%s", current.getNamespaceName()),
          current.getReleaseKey());
    

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

客户端从 Apollo 服务端获取到应用的最新配置后,会更新本地缓存。

2、schedulePeriodicRefresh 方法

初始延迟 5 分钟,之后每隔 5 分钟重复调度一次 trySync 方法。

private void schedulePeriodicRefresh() 
  logger.debug("Schedule periodic refresh with interval:  ",
      m_configUtil.getRefreshInterval(), m_configUtil.getRefreshIntervalTimeUnit());
  // 默认初始延迟5分钟,之后每隔5分钟重复调度一次
  // 可以通过 apollo.refreshInterval 属性修改默认值
  m_executorService.scheduleAtFixedRate(
      new Runnable() 
        @Override
        public void run() 
          Tracer.logEvent("Apollo.ConfigService", String.format("periodicRefresh: %s", m_namespace));
          logger.debug("refresh config for namespace: ", m_namespace);
          trySync();
          Tracer.logEvent("Apollo.Client.Version", Apollo.VERSION);
        
      , m_configUtil.getRefreshInterval(), m_configUtil.getRefreshInterval(),
      m_configUtil.getRefreshIntervalTimeUnit());

客户端定时从服务端拉取应用的最新配置。

3、scheduleLongPollingRefresh 方法

客户端向服务端发起长轮询请求。实际上客户端和服务端保持了一个长连接,从而能第一时间获得配置更新的推送。

private void scheduleLongPollingRefresh() 
  // 交给 RemoteConfigLongPollService 处理
  remoteConfigLongPollService.submit(m_namespace, this);

接着看下 RemoteConfigLongPollService 的 submit 方法是如何处理的。

public boolean submit(String namespace, RemoteConfigRepository remoteConfigRepository) 
  // 更新 m_longPollNamespaces 缓存
  boolean added = m_longPollNamespaces.put(namespace, remoteConfigRepository);
  // 更新 m_notifications 缓存
  m_notifications.putIfAbsent(namespace, INIT_NOTIFICATION_ID);
  if (!m_longPollStarted.get()) 
    // 执行 startLongPolling 方法
    startLongPolling();
  
  return added;

接着看下 startLongPolling 方法的处理逻辑。

private void startLongPolling() 
  if (!m_longPollStarted.compareAndSet(false, true)) 
    return;
  
  try 
    final String appId = m_configUtil.getAppId();
    final String cluster = m_configUtil.getCluster();
    final String dataCenter = m_configUtil.getDataCenter();
    final String secret = m_configUtil.getAccessKeySecret();
    // 默认 2000 毫秒
    final long longPollingInitialDelayInMills = m_configUtil.getLongPollingInitialDelayInMills();
    m_longPollingService.submit(new Runnable() 
      @Override
      public void run() 
        if (longPollingInitialDelayInMills > 0) 
          try 
            logger.debug("Long polling will start in  ms.", longPollingInitialDelayInMills);
            TimeUnit.MILLISECONDS.sleep(longPollingInitialDelayInMills);
           catch (InterruptedException e) 
            //ignore
          
        
        // 执行 doLongPollingRefresh 方法
        doLongPollingRefresh(appId, cluster, dataCenter, secret);
      
    );
   catch (Throwable ex) 
    m_longPollStarted.set(false);
    ApolloConfigException exception =
        new ApolloConfigException("Schedule long polling refresh failed", ex);
    Tracer.logError(exception);
    logger.warn(ExceptionUtil.getDetailMessage(exception));
  

接着看下 doLongPollingRefresh 方法的处理逻辑。

private void doLongPollingRefresh(String appId, String cluster, String dataCenter, String secret) 
  final Random random = new Random();
  ServiceDTO lastServiceDto = null;
  while (!m_longPollingStopped.get() && !Thread.currentThread().isInterrupted()) 
    // 限流判断
    if (!m_longPollRateLimiter.tryAcquire(5, TimeUnit.SECONDS)) 
      try 
        TimeUnit.SECONDS.sleep(5);
       catch (InterruptedException e) 
      
    
    Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "pollNotification");
    String url = null;
    try Apollo分布式配置中心入门学习

Apollo架构篇 - 分布式配置中心Apollo

分布式配置中心 携程 apollo

使用Docker搭建Apollo分布式配置中心

分布式配置中心Apollo

Apollo分布式配置中心部署以及使用