微服务中集成分布式配置中心 Apollo

4,002 阅读11分钟

背景

随着业务的发展、微服务架构的升级,服务的数量、程序的配置日益增多(各种微服务、各种服务器地址、各种参数),传统的配置文件方式和数据库的方式已无法满足开发人员对配置管理的要求:配置修改后实时生效,灰度发布,分环境、分集群管理配置,完善的权限、审核机制。分布式环境下,这些配置更加复杂。

因此,我们需要配置中心来统一管理配置!把业务开发者从复杂以及繁琐的配置中解脱出来,只需专注于业务代码本身,从而能够显著提升开发以及运维效率。同时将配置和发布包解藕也进一步提升发布的成功率,并为运维的细力度管控、应急处理等提供强有力的支持。

在之前的文章中,我们介绍过 Spring Cloud 中的分布式配置中心组件:Spring Cloud Config。本文将会介绍功能更为强大的 Apollo。

分布式配置中心

在一个分布式环境中,同类型的服务往往会部署很多实例。这些实例使用了一些配置,为了更好地维护这些配置就产生了配置管理服务。通过这个服务可以轻松地管理成千上百个服务实例的配置问题。配置中心的特点:

  • 配置的增删改查;
  • 不同环境配置隔离(开发、测试、预发布、灰度/线上);
  • 高性能、高可用性;
  • 请求量多、高并发;
  • 读多写少;

现有的配置中心组件有:Spring Cloud Config、Apollo、Disconf、Diamond 等等,这些组件在功能上有或多或少的差异,但是都具有基本的配置中心的功能。

Apollo 简介

Apollo(阿波罗)是携程框架部门研发的开源配置管理中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性。目前的有超过 14k 的 star,使用广泛。Apollo基于开源模式开发,开源地址:github.com/ctripcorp/a…

图片来源 Apollo
图片来源 Apollo

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

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

  • application (应用):实际使用配置的应用,Apollo客户端在运行时需要知道当前应用是谁,从而可以去获取对应的配置;每个应用都需要有唯一的身份标识 -- appId,应用身份是跟着代码走的,所以需要在代码中配置。
  • environment (环境):配置对应的环境,Apollo客户端在运行时需要知道当前应用处于哪个环境,从而可以去获取应用的配置。
  • cluster (集群):一个应用下不同实例的分组,比如典型的可以按照数据中心分,把上海机房的应用实例分为一个集群,把北京机房的应用实例分为另一个集群。对不同的cluster,同一个配置可以有不一样的值,如zookeeper地址。
  • namespace (命名空间):一个应用下不同配置的分组,可以简单地把namespace类比为文件,不同类型的配置存放在不同的文件中,如数据库配置文件,RPC配置文件,应用自身的配置文件等;应用可以直接读取到公共组件的配置namespace,如DAL,RPC等;应用也可以通过继承公共组件的配置namespace来对公共组件的配置做调整,如DAL的初始数据库连接数。

我们在集成 Apollo 时,可以根据需要创建相应的维度。

快速入门

下面我们搭建一个基于 Spring Boot 的微服务,集成 Apollo。

启动服务端

Apollo配置中心包括:Config Service、Admin Service 和 Portal。

  • Config Service:提供配置获取接口、配置推送接口,服务于Apollo客户端;
  • Admin Service:提供配置管理接口、配置修改发布接口,服务于管理界面Portal;
  • Portal:配置管理界面,通过MetaServer获取AdminService的服务列表,并使用客户端软负载SLB方式调用AdminService。

官网准备好了一个Quick Start安装包,大家只需要下载到本地,就可以直接使用,免去了编译、打包过程。也可以自行编译,较为繁琐。

Apollo服务端共需要两个数据库:ApolloPortalDB和ApolloConfigDB。创建的语句见安装包,创建好之后需要配置启动的脚本,即 demo.sh 脚本:

#apollo config db info
apollo_config_db_url=jdbc:mysql://localhost:3306/ApolloConfigDB?characterEncoding=utf8
apollo_config_db_username=用户名
apollo_config_db_password=密码(如果没有密码,留空即可)

# apollo portal db info
apollo_portal_db_url=jdbc:mysql://localhost:3306/ApolloPortalDB?characterEncoding=utf8
apollo_portal_db_username=用户名
apollo_portal_db_password=密码(如果没有密码,留空即可)

脚本会在本地启动3个服务,分别使用8070, 8080, 8090端口,请确保这3个端口当前没有被使用。执行

./demo.sh start

看到输出如下的日志信息:

==== starting service ====
Service logging file is ./service/apollo-service.log
Started [10768]
Waiting for config service startup.......
Config service started. You may visit http://localhost:8080 for service status now!
Waiting for admin service startup....
Admin service started
==== starting portal ====
Portal logging file is ./portal/apollo-portal.log
Started [10846]
Waiting for portal startup......
Portal started. You can visit http://localhost:8070 now!

Apollo 服务端启动成功。

客户端应用

搭建好 Apollo 服务器之后,接下来将我们的应用接入 Apollo。

引入依赖
        <dependency>
            <groupId>com.ctrip.framework.apollo</groupId>
            <artifactId>apollo-client</artifactId>
            <version>1.1.0</version>
        </dependency>

在依赖中只需要增加 apollo-client 的引用。

入口程序
@SpringBootApplication
@EnableApolloConfig("TEST1.product")
public class ApolloApplication {

    public static void main(String[] args) {
        SpringApplication.run(ApolloApplication.class, args);
    }
}

我们通过 @EnableApolloConfig("TEST1.product") 注解开启注册到 Apollo 服务端,并指定了 namespace 为 TEST1.product

配置文件
app.id: spring-boot-logger
  # set apollo meta server address, adjust to actual address if necessary
apollo.meta: http://localhost:8080
server:
  port: 0

配置文件中指定了appid 和 Apollo 服务器的地址。

测试应用

我们通过动态设置输出的日志等级来测试接入的配置中心。

@Service
public class LoggerConfiguration {
    private static final Logger logger = LoggerFactory.getLogger(LoggerConfiguration.class);
    private static final String LOGGER_TAG = "logging.level.";

    @Autowired
    private LoggingSystem loggingSystem;

    @ApolloConfig
    private Config config;

    @ApolloConfigChangeListener
    // 监听 Apollo 配置中心的刷新事件
    private void onChange(ConfigChangeEvent changeEvent) {
        refreshLoggingLevels();
    }

    @PostConstruct
    // 设置刷新之后的日志级别
    private void refreshLoggingLevels() {
        Set<String> keyNames = config.getPropertyNames();
        for (String key : keyNames) {
            if (containsIgnoreCase(key, LOGGER_TAG)) {
                String strLevel = config.getProperty(key, "info");
                LogLevel level = LogLevel.valueOf(strLevel.toUpperCase());
                loggingSystem.setLogLevel(key.replace(LOGGER_TAG, ""), level);
                logger.info("{}:{}", key, strLevel);
            }
        }
    }

    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 配置中心的日志等级配置,设置本地服务的日志等级,并监听刷新事件,将刷新后的配置及时应用到本地服务,其中 @PostConstruct 注解用于在完成依赖项注入以执行任何初始化之后需要执行的方法。

@Service
public class PrintLogger {
    private static Logger logger = LoggerFactory.getLogger(PrintLogger.class);

    @ApolloJsonValue("${kk.v}")
    private String v;

    @PostConstruct
    public void printLogger() throws Exception {
        Executors.newSingleThreadExecutor().submit(() -> {
            while (true) {
                logger.error("=========" + v);
                logger.info("我是info级别日志");
                logger.error("我是error级别日志");
                logger.warn("我是warn级别日志");
                logger.debug("我是debug级别日志");
                TimeUnit.SECONDS.sleep(1);
            }
        });
    }
}

起一个线程,输出不同级别的日志。根据配置的日志等级,过滤后再打印。我们在如上的程序中,还自定义了一个字段,同样用以测试随机打印最新的值。

测试

我们在 Apollo 的配置界面中,增加如下的配置:

并将配置发布,启动我们本地的 SpringBoot 服务:

2019-05-28 20:31:36.688 ERROR 44132 --- [pool-1-thread-1] com.blueskykong.apollo.PrintLogger       : =========log-is-error-level.
2019-05-28 20:31:36.688 ERROR 44132 --- [pool-1-thread-1] com.blueskykong.apollo.PrintLogger       : 我是error级别日志

我们将调整日志的级别为warn,只需要在界面上编辑。

将编辑好的配置发布,应用服务将会收到刷新事件。

可以看到,服务刷新了日志的级别,打印 warn 的日志信息。

2019-05-28 20:35:56.819  WARN 44132 --- [pool-1-thread-1] com.blueskykong.apollo.PrintLogger       : 我是warn级别日志
2019-05-28 20:36:06.823 ERROR 44132 --- [pool-1-thread-1] com.blueskykong.apollo.PrintLogger       : =========log-is-warn-level.

原理细究

在体验了 Apollo 作为配置中心之后,我们将了解下 Apollo 的总体设计和实现的原理。

Apollo 整体架构

图片来源 Apollo
图片来源 Apollo

上图简要描述了 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进程中

ConfigService、AdminService、Portal 属于 Apollo 服务端的模块,其中提到的 Eureka 是为了保证高可用,Config 和 Admin 都是无状态以集群方式部署的,Client 怎么找到 Config?Portal 怎么找到 Admin?为了解决这个问题,Apollo在其架构中引入了Eureka服务注册中心组件,实现微服务间的服务注册和发现用于服务发现和注册,Config 和 Admin Service注册实例并定期报心跳, Eureka 与 ConfigService 一起部署。

MetaServer 其实是一个Eureka的Proxy,将Eureka的服务发现接口以更简单明确的HTTP接口的形式暴露出来,方便 Client/Protal 通过简单的 HTTPClient 就可以查询到 Config/Admin 的地址列表。获取到服务实例地址列表之后,再以简单的客户端软负载(Client SLB)策略路由定位到目标实例,并发起调用。

客户端实现

在配置中心中,一个重要的功能就是配置发布后实时推送到客户端。下面我们简要看一下这块是怎么设计实现的。

上图简要描述了配置发布的大致过程:用户在Portal操作配置发布;Portal调用Admin Service的接口操作发布;Admin Service发布配置后,发送ReleaseMessage给各个Config Service;Config Service收到ReleaseMessage后,通知对应的客户端。

如何通知客户端呢?我们看到 Apollo 的实现步骤如下:

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

小结

本文首先介绍分布式配置中心的概念和 Apollo 接入的实践,然后深入介绍了 Apollo 的总体架构和实现的一些细节。总得来说, Apollo 是现有配置中心组件中,功能最全的一个。能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性,适用于微服务配置管理场景。

本文对应的代码地址: github.com/keets2012/S…

订阅最新文章,欢迎关注我的公众号

微信公众号

参考

  1. apollo
  2. 微服务架构~携程Apollo配置中心架构剖析