Apollo在基础架构中的实践经验

本文来自李伟超同窗的投稿,若是你有好的文章也欢迎联系我。

微服务配置中心 Apollo 使用指南,如下文档根据 apollo wiki 整理而来,部分最佳实践说明和代码改造基于笔者的工做经验整理而来,若有问题欢迎沟通。html

配置中心

在拆分为微服务架构前,曾经的单体应用只须要管理一套配置。而拆分为微服务后,每个系统都有本身的配置,而且都各不相同,并且由于服务治理的须要,有些配置还须要可以动态改变,如业务参数调整或须要熔断限流等功能,配置中心就是解决这个问题的。java

配置的基本概念

  • 配置是独立于程序的只读变量
    • 同个应用在不一样的配置有不一样的行为
    • 应用不该该改变配置
  • 配置伴随应用的整个生命周期
    • 初始化参数和运行参数
  • 配置能够有多种加载方式
  • 配置须要治理
    • 权限控制(应用级别、编辑发布隔离等)
    • 多环境集群配置管理
    • 框架类组件配置管理

配置中心

  • 配置注册与反注册
  • 配置治理
  • 配置变动订阅

Spring Environment

Environment 是 Spring 容器中对于应用环境两个关键因素(profile & properties)的一个抽象。git

  • profile

profile 是一个逻辑的分组,当 bean 向容器中注册的时候,仅当配置激活时生效。github

## 配置文件使用
spring.profiles.active=xxx 

## 硬编码注解形式使用
@org.springframework.context.annotation.Profile
复制代码
  • properties

Properties 在几乎全部应用程序中都扮演着重要的角色,而且可能来自各类各样的来源:properties 文件、JVM系统属性、系统环境变量、JNDI、Servlet Context 参数、ad-hoc Properties 对象、Map 等等。Environment 与 Properties 的关系是为用户提供一个方便的服务接口,用于配置属性源并从它们中解析属性。spring

Apollo 简介

简介

Apollo(阿波罗)是携程框架部门研发的开源配置管理中心,可以集中化管理应用不一样环境、不一样集群的配置,配置修改后可以实时推送到应用端,而且具有规范的权限、流程治理等特性。数据库

Apollo 支持4个维度管理 Key-Value 格式的配置:bootstrap

  • application (应用)

这个很好理解,就是实际使用配置的应用,Apollo 客户端在运行时须要知道当前应用是谁,从而能够去获取对应的配置。每一个应用都须要有惟一的身份标识,咱们认为应用身份是跟着代码走的,因此须要在代码中配置,具体信息请参见 Java 客户端使用指南。缓存

  • environment (环境)

配置对应的环境,Apollo 客户端在运行时须要知道当前应用处于哪一个环境,从而能够去获取应用的配置。咱们认为环境和代码无关,同一份代码部署在不一样的环境就应该可以获取到不一样环境的配置,因此环境默认是经过读取机器上的配置(server.properties中的env属性)指定的,不过为了开发方便,咱们也支持运行时经过 System Property 等指定,具体信息请参见Java客户端使用指南。bash

  • cluster (集群)

一个应用下不一样实例的分组,好比典型的能够按照数据中心分,把上海机房的应用实例分为一个集群,把北京机房的应用实例分为另外一个集群。对不一样的cluster,同一个配置能够有不同的值,如 zookeeper 地址。集群默认是经过读取机器上的配置(server.properties中的idc属性)指定的,不过也支持运行时经过 System Property 指定,具体信息请参见Java客户端使用指南。服务器

  • namespace (命名空间)

一个应用下不一样配置的分组,能够简单地把 namespace 类比为文件,不一样类型的配置存放在不一样的文件中,如数据库配置文件,RPC配置文件,应用自身的配置文件等。应用能够直接读取到公共组件的配置 namespace,如 DAL,RPC 等。应用也能够经过继承公共组件的配置 namespace 来对公共组件的配置作调整,如DAL的初始数据库链接数。

同时,Apollo 基于开源模式开发,开源地址:github.com/ctripcorp/a…

基础模型

以下便是Apollo的基础模型:

  1. 用户在配置中心对配置进行修改并发布
  2. 配置中心通知Apollo客户端有配置更新
  3. Apollo客户端从配置中心拉取最新的配置、更新本地配置并通知到应用

图片

Apollo 架构说明

Apollo 项目自己就使用了 Spring Boot & Spring Cloud 开发。

服务端

图片

上图简要描述了Apollo的整体设计,咱们能够从下往上看:

  • Config Service 提供配置的读取、推送等功能,服务对象是Apollo客户端。
  • Admin Service 提供配置的修改、发布等功能,服务对象是Apollo Portal(管理界面)。
  • Config Service 和 Admin Service 都是多实例、无状态部署,因此须要将本身注册到 Eureka 中并保持心跳
  • 在 Eureka 之上咱们架了一层 Meta Server 用于封装 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 Service、Eureka 和 Meta Server 三个逻辑角色部署在同一个 JVM 进程中。

客户端

图片

  • 客户端和服务端保持了一个长链接,从而能第一时间得到配置更新的推送。
  • 客户端还会定时从 Apollo 配置中心服务端拉取应用的最新配置。
    • 这是一个fallback机制,为了防止推送机制失效致使配置不更新
    • 客户端定时拉取会上报本地版本,因此通常状况下,对于定时拉取的操做,服务端都会返回304 - Not Modified
    • 定时频率默认为每5分钟拉取一次,客户端也能够经过在运行时指定 System Property: apollo.refreshInterval 来覆盖,单位为分钟。
  • 客户端从Apollo配置中心服务端获取到应用的最新配置后,会保存在内存中
  • 客户端会把从服务端获取到的配置在本地文件系统缓存一份
    • 在遇到服务不可用,或网络不通的时候,依然能从本地恢复配置
  • 应用程序从Apollo客户端获取最新的配置、订阅配置更新通知

长链接实现上是使用的异步+轮询实现,具体实现的解析请查看下面两篇文章

service notifications

client polling

Apollo 高可用部署

在 Apollo 架构说明中咱们提到过 client 和 portal 都是在客户端负载均衡,根据 ip+port 访问服务,因此 config service 和 admin service 是无状态的,能够水平扩展的,portal service 根据使用 slb 绑定多台服务器达到切换,meta server 同理。 | 场景 | 影响 | 降级 | 缘由 | |:----|:----|:----|:----| | 某台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自动切换到其它存活的数据中心 |

Apollo 使用说明

使用说明

Apollo使用指南

Java客户端使用指南

最佳实践

在 Spring Boot & Spring Cloud 中使用。

  • 每一个应用都须要有惟一的身份标识,咱们认为应用身份是跟着代码走的,因此须要在代码中配置。关于应用身份标识,应用标识对第三方中间件应该是统一的,扩展支持 apollo 身份标识和 spring.application.name 一致(具体查看 fusion-config-apollo 中代码),其余中间件同理。
  • 应用开发过程当中如使用代码中的配置,应该充分利用 Spring Environment Profile,增长本地逻辑分组 local,非开发阶段关闭 local 逻辑分组。同时关闭 apollo 远程获取配置,在 VM options 中增长 -Denv=local。

图片

如下代码是扩展 apollo 应用标识使用 spring.application.name,并增长监控配置,监控通常是基础架构团队提供的功能,从基础框架硬编码上去,业务侧作到彻底无感知。

import com.ctrip.framework.apollo.ConfigService;
import com.ctrip.framework.apollo.spring.config.PropertySourcesConstants;
import com.ctrip.framework.foundation.internals.io.BOMInputStream;
import com.ctrip.framework.foundation.internals.provider.DefaultApplicationProvider;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringApplicationRunListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.util.StringUtils;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Properties;
import java.util.Set;
/**
 * ApolloSpringApplicationRunListener
 * <p>
 * SpringApplicationRunListener
 * 接口说明 https://blog.csdn.net/u011179993/article/details/51555690https://blog.csdn.net/u011179993/article/details/51555690
 *
 * @author Weichao Li (liweichao0102@gmail.com)
 * @since 2019-08-15
 */
@Order(value = ApolloSpringApplicationRunListener.APOLLO_SPRING_APPLICATION_RUN_LISTENER_ORDER)
@Slf4j
public class ApolloSpringApplicationRunListener implements SpringApplicationRunListener {
    public static final int APOLLO_SPRING_APPLICATION_RUN_LISTENER_ORDER = 1;
    private static final String APOLLO_APP_ID_KEY = "app.id";
    private static final String SPRINGBOOT_APPLICATION_NAME = "spring.application.name";
    private static final String CONFIG_CENTER_INFRA_NAMESPACE = "infra.monitor";
    public ApolloSpringApplicationRunListener(SpringApplication application, String[] args) {
    }
    /**
     * 刚执行run方法时
     */
    @Override
    public void starting() {
    }
    /**
     * 环境创建好时候
     *
     * @param env 环境信息
     */
    @Override
    public void environmentPrepared(ConfigurableEnvironment env) {
        Properties props = new Properties();
        props.put(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED, true);
        props.put(PropertySourcesConstants.APOLLO_BOOTSTRAP_EAGER_LOAD_ENABLED, true);
        env.getPropertySources().addFirst(new PropertiesPropertySource("apolloConfig", props));
        // 初始化appId
        this.initAppId(env);
        // 初始化基础架构提供的默认配置,需在项目中关联公共 namespaces
        this.initInfraConfig(env);
    }
    /**
     * 上下文创建好的时候
     *
     * @param context 上下文
     */
    @Override
    public void contextPrepared(ConfigurableApplicationContext context) {
    }
    /**
     * 上下文载入配置时候
     *
     * @param context 上下文
     */
    @Override
    public void contextLoaded(ConfigurableApplicationContext context) {
    }
    @Override
    public void started(ConfigurableApplicationContext context) {
    }
    @Override
    public void running(ConfigurableApplicationContext context) {
    }
    @Override
    public void failed(ConfigurableApplicationContext context, Throwable exception) {
    }
    /**
     * 初始化 apollo appId
     *
     * @param env 环境信息
     */
    private void initAppId(ConfigurableEnvironment env) {
        String apolloAppId = env.getProperty(APOLLO_APP_ID_KEY);
        if (StringUtils.isEmpty(apolloAppId)) {
            //此处须要判断一下 meta-inf 下的文件中的 app id
            apolloAppId = getAppIdByAppPropertiesClasspath();
            if (StringUtils.isEmpty(apolloAppId)) {
                String applicationName = env.getProperty(SPRINGBOOT_APPLICATION_NAME);
                if (!StringUtils.isEmpty(applicationName)) {
                    System.setProperty(APOLLO_APP_ID_KEY, applicationName);
                } else {
                    throw new IllegalArgumentException(
                            "config center must config app.id in " + DefaultApplicationProvider.APP_PROPERTIES_CLASSPATH);
                }
            } else {
                System.setProperty(APOLLO_APP_ID_KEY, apolloAppId);
            }
        } else {
            System.setProperty(APOLLO_APP_ID_KEY, apolloAppId);
        }
    }
    /**
     * 初始化基础架构提供的配置
     *
     * @param env 环境信息
     */
    private void initInfraConfig(ConfigurableEnvironment env) {
        com.ctrip.framework.apollo.Config apolloConfig = ConfigService.getConfig(CONFIG_CENTER_INFRA_NAMESPACE);
        Set<String> propertyNames = apolloConfig.getPropertyNames();
        if (propertyNames != null && propertyNames.size() > 0) {
            Properties properties = new Properties();
            for (String propertyName : propertyNames) {
                properties.setProperty(propertyName, apolloConfig.getProperty(propertyName, null));
            }
            EnumerablePropertySource enumerablePropertySource =
                    new PropertiesPropertySource(CONFIG_CENTER_INFRA_NAMESPACE, properties);
            env.getPropertySources().addLast(enumerablePropertySource);
        }
    }
    /**
     * 从 apollo 默认配置文件中取 app.id 的值,调整优先级在 spring.application.name 以前
     *
     * @return apollo app id
     */
    private String getAppIdByAppPropertiesClasspath() {
        try {
            InputStream in = Thread.currentThread().getContextClassLoader()
                    .getResourceAsStream(DefaultApplicationProvider.APP_PROPERTIES_CLASSPATH);
            if (in == null) {
                in = DefaultApplicationProvider.class
                        .getResourceAsStream(DefaultApplicationProvider.APP_PROPERTIES_CLASSPATH);
            }
            Properties properties = new Properties();
            if (in != null) {
                try {
                    properties.load(new InputStreamReader(new BOMInputStream(in), StandardCharsets.UTF_8));
                } finally {
                    in.close();
                }
            }
            if (properties.containsKey(APOLLO_APP_ID_KEY)) {
                String appId = properties.getProperty(APOLLO_APP_ID_KEY);
                log.info("App ID is set to {} by app.id property from {}", appId, DefaultApplicationProvider.APP_PROPERTIES_CLASSPATH);
                return appId;
            }
        } catch (Throwable ignore) {
        }
        return null;
    }
}
复制代码

动态刷新

支持 Apollo 配置自动刷新类型,支持 @Value @RefreshScope @ConfigurationProperties 以及日志级别的动态刷新。具体代码查看下文连接。

  • @Value

@Value Apollo 自己就支持了动态刷新,须要注意的是若是@Value 使用了 SpEL 表达式,动态刷新会失效。

// 支持动态刷新
@Value("${simple.xxx}")
private String simpleXxx;
// 不支持动态刷新
@Value("#{'${simple.xxx}'.split(',')}")
private List<String> simpleXxxs;
复制代码
  • @RefreshScope

RefreshScope(org.springframework.cloud.context.scope.refresh)是 Spring Cloud 提供的一种特殊的 scope 实现,用来实现配置、实例热加载。

动态实现过程:

配置变动时,调用 refreshScope.refreshAll() 或指定 bean。提取标准参数(System,jndi,Servlet)以外全部参数变量,把原来的Environment里的参数放到一个新建的 Spring Context 容器下从新加载,完事以后关闭新容器。提取更新过的参数(排除标准参数) ,比较出变动项,发布环境变动事件,RefreshScope 用新的环境参数从新生成Bean。从新生成的过程很简单,清除 refreshscope 缓存幷销毁 Bean,下次就会从新从 BeanFactory 获取一个新的实例(该实例使用新的配置)。

  • @ConfigurationProperties

apollo 默认是不支持 ConfigurationProperties 刷新的,这块须要配合 EnvironmentChangeEvent 刷新的。

  • 日志级别

apollo 默认是不支持日志级别刷新的,这块须要配合 EnvironmentChangeEvent 刷新的。

  • EnvironmentChangeEvent(Spring Cloud 提供)

当观察到 EnvironmentChangeEvent 时,它将有一个已更改的键值列表,应用程序将使用如下内容: 1,从新绑定上下文中的任何 @ConfigurationProperties bean,代码见org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder。 2,为logging.level.*中的任何属性设置记录器级别,代码见 org.springframework.cloud.logging.LoggingRebinder。 支持动态刷新

import com.ctrip.framework.apollo.model.ConfigChangeEvent;
import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
import org.springframework.cloud.context.scope.refresh.RefreshScope;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Configuration;
/**
 * LoggerConfiguration
 *
 * @author Weichao Li (liweichao0102@gmail.com)
 * @since 2019/11/14
 */
@Configuration
@Slf4j
public class ApolloRefreshConfiguration implements ApplicationContextAware {
    private ApplicationContext applicationContext;
    @Autowired
    private RefreshScope refreshScope;
    @ApolloConfigChangeListener
    private void onChange(ConfigChangeEvent changeEvent) {
        applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));
        refreshScope.refreshAll();
    }
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}
复制代码

注意原有配置若是有日志级别须要初始化。

import com.ctrip.framework.apollo.Config;
import com.ctrip.framework.apollo.spring.annotation.ApolloConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.logging.LogLevel;
import org.springframework.boot.logging.LoggingSystem;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import java.util.Set;
/**
 * logging 初始化
 *
 * @author Weichao Li (liweichao0102@gmail.com)
 * @since 2019/11/14
 */
@Configuration
@Slf4j
public class LoggingConfiguration {
    private static final String LOGGER_TAG = "logging.level.";
    private static final String DEFAULT_LOGGING_LEVEL = "info";
    @Autowired
    private LoggingSystem loggingSystem;
    @ApolloConfig
    private Config config;
    @PostConstruct
    public void changeLoggingLevel() {
        Set<String> keyNames = config.getPropertyNames();
        for (String key : keyNames) {
            if (containsIgnoreCase(key, LOGGER_TAG)) {
                String strLevel = config.getProperty(key, DEFAULT_LOGGING_LEVEL);
                LogLevel level = LogLevel.valueOf(strLevel.toUpperCase());
                loggingSystem.setLogLevel(key.replace(LOGGER_TAG, ""), level);
            }
        }
    }
    private static boolean containsIgnoreCase(String str, String searchStr) {
        if (str == null || searchStr == null) {
            return false;
        }
        int len = searchStr.length();
        int max = str.length() - len;
        for (int i = 0; i <= max; i++) {
            if (str.regionMatches(true, i, searchStr, 0, len)) {
                return true;
            }
        }
        return false;
    }
}
复制代码

Apollo 最佳实践 - 配置治理

权限控制

因为配置能改变程序的行为,不正确的配置甚至能引发灾难,因此对配置的修改必须有比较完善的权限控制。应用和配置的管理都有完善的权限管理机制,对配置的管理还分为了编辑和发布两个环节,从而减小人为的错误。全部的操做都有审计日志,能够方便地追踪问题

  • everyone 要有本身的帐户(最主要的前置条件)
  • 每个项目都至少有一个 owner(项目管理员,项目管理员拥有如下权限)
    • 能够管理项目的权限分配
    • 能够建立集群
    • 能够建立 Namespace
  • 项目管理员(owner)根据组织结构分配配置权限
    • 编辑权限容许用户在 Apollo 界面上建立、修改、删除配置
      • 配置修改后只在 Apollo 界面上变化,不会影响到应用实际使用的配置
    • 发布权限容许用户在 Apollo 界面上发布、回滚配置
      • 配置只有在发布、回滚动做后才会被应用实际使用到
      • Apollo在用户操做发布、回滚动做后实时通知到应用,并使最新配置生效
  • 项目管理员管理权限界面

图片

项目建立完,默认没有分配配置的编辑和发布权限,须要项目管理员进行受权。

  1. 点击application这个namespace的受权按钮

图片

  1. 分配修改权限

图片

  1. 分配发布权限

图片

Namespace

Namespace 权限分类

apollo 获取权限分类分为私有的和公共的。

  • private (私有的)

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

  • public (公共的)

public权限的Namespace,能被任何应用获取。

Namespace 的分类

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

Apollo 私有类型 Namespace 使用说明

私有类型的 Namespace 具备 private 权限。例如服务默认的“application” Namespace 就是私有类型。

  1. 使用场景
  • 服务自身的配置(如数据库、业务行为等配置)
  1. 如何使用私有类型 Namespace

一个应用下不一样配置的分组,能够简单地把namespace类比为文件,不一样类型的配置存放在不一样的文件中,如数据库配置文件,业务属性配置,配置文件等

Apollo 公共类型 Namespace 使用说明

公共类型的 Namespace 具备 public 权限。公共类型的 Namespace 至关于游离于应用以外的配置,且经过 Namespace 的名称去标识公共 Namespace,因此公共的 Namespace 的名称必须全局惟一。

  1. 使用场景
  • 部门级别共享的配置
  • 小组级别共享的配置
  • 几个项目之间共享的配置
  • 中间件客户端的配置
  1. 如何使用公共类型 Namespace
  • 代码侵入型
@EnableApolloConfig({"application", "poizon-infra.jaeger"})
复制代码
  • 配置方式形式
# will inject 'application' namespace in bootstrap phase
apollo.bootstrap.enabled = true
# will inject 'application', 'poizon-infra.jaeger' namespaces in bootstrap phase
apollo.bootstrap.namespaces = application,poizon-infra.jaeger
复制代码

Apollo 关联类型 Namespace 使用说明

关联类型又可称为继承类型,关联类型具备 private 权限。关联类型的 Namespace 继承于公共类型的 Namespace,用于覆盖公共 Namespace 的某些配置。

使用建议

  • 基础框架部分的统一配置,如 DAL 的经常使用配置
  • 基础架构的公共组件的配置,如监控,熔断等公共组件配置
相关文章
相关标签/搜索