在了解携程Applo配置中心之前,我们需要在心里思考以下这些问题:
- 什么是配置中心,如果我们自己去实现一个配置中心,它应该具备哪些功能;
- 我们用过哪些配置中心,它们有什么区别;
- 如何实现一个配置中心;
一、配置中心概述
1.1 配置中心的由来
由于微服务框架的兴起,以前一个大型项目,有许多个模块,比如用户模块、订单模块、商品模块、库存模块等,整个项目可能单单java文件就能有几百上千个。这种大项目打一次包可能就要几十分钟甚至几个小时,而且这种大项目在一个服务器上跑,浪费的资源是十分大的,而且访问量也低,这就是早期的单体项目。后来人们将这个大项目按模块拆开,各个模块自己作为一个web项目,彼此之间通过网络进行服务器间的通讯(RPC、HTTP等),各个模块自己随意部署,就渐渐演变成微服务项目。
以Spring Cloud微服务项目为例,我们将大项目拆成小项目后,我们的application.yml(或者applicaion.proprties)配置文件也同样变成了好几份,即便你两个小项目用的是同样的配置,你也要复制两次。一旦我们的小项目数量多了起来,那么管理这些配置将会变得困难。
而且一旦某个项目部署了十几二十个机器上后,你开发修改了某一个配置项,那就意味这必须重启这十几二十个机器上的项目。为了避免这种事情的发生,衍生出了一个项目专门管理配置项,也就是配置中心。
1.2 配置中心功能
配置中心的核心功能主要包含:
- 抽象标准化:配置中心应该屏蔽掉其实现的细节,方便用户进行自主式配置管理,一般用户只需要关注两个抽象和标准化的接口即可:
- 配置管理界面UI,方便应用开发人员管理和发布配置;
-
封装好的客户端API,方便应用集成和获取配置;
- 高可用:如果大量服务的运行依赖于配置中心,当配置中心宕机时,会影响大面积服务的运行。
- 多环境多集群:微服务应用大多采用多环境部署,一般标准的环境有开发/测试/生产等,有的应用还需要多集群部署,如支持跨机房或多版本部署。配置中心需要支持对多环境和多集群应用配置的集中式管理;
- 实时性:配置更新需要尽快通知到客户端,有些配置的实时性要求很高,像是主备切换配置,这些需要秒级切换配置的能力。
-
治理:配置需要治理,具体包括:
- 配置格式校验:应用的配置数据存储在配置中心一般都会以一种配置格式存储,比如properties、json、yml等,如果配置格式错误,会导致客户端解析配置失败引起生产故障,配置中心对配置的格式校验能够有效防止人为错误操作的发生,是配置中心核心功能中的刚需;
-
配置审计:需要记录修改人,修改内容和修改事件,方便出现问题时能后追溯;
-
配置版本控制:每次变更需要版本化,出现问题可以及时回滚到上一版本的配置;
-
配置权限控制:配置变更发布需要认证授权;
-
灰度发布:配置发布时可以先让少数实例生效,确定没有问题就可以扩大应用范围。
- 监听查询:当排查问题或者进行统计的时候,需要知道一个配置被哪些应用实例使用到,以及一个实例使用到了哪些配置。
1.3 常用的配置中心
目前市面上流行的配置中心种类繁多,常用的主要有以下几种(下表来源自其它博客):
功能点 | Spring Cloud Config | Apollo | Nacos |
开源时间 | 2014.9 | 2016.5 | 2018.6 |
配置实时推送 | 支持(Spring Cloud Bus) | 支持(HTTP长轮询1S内) | 支持(HTTP长轮询1S内) |
版本管理 | 支持(Git) | 支持 | 支持 |
配置回滚 | 支持(Git) | 支持 | 支持 |
灰度发布 | 支持 | 支持 | 不支持 |
权限管理 | 支持 | 支持 | 不支持 |
多集群 | 支持 | 支持 | 支持 |
多环境 | 支持 | 支持 | 支持 |
监听查询 | 支持 | 支持 | 支持 |
多语言 | 支支持java | go、c++、python、php、.net等 | node.js、c++、python、java等 |
单机部署 | Config-Server + Git + Spring Cloud Bus | Apollo-quickstart + mysql | nacos单节点 |
分布式部署 | Config-Server + Git + MQ | Config + Admin + Portal + mysql | nacos + mysql |
配置格式支持 | 不支持 | 支持 | 支持 |
通信协议 | HTTP和AMQP | HTTP | HTTP |
数据一致性 | Git保证数据一致性、Config-server从Git读数据 | 数据库模拟消息队列,Apollo定时读消息 | HTTP异步通知 |
二、Apollo配置中心设计
Apollo(阿波罗)是携程框架部门研发的分布式配置中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性,适用于微服务配置管理场景。
2.1 Apollo基本模型
2.2 架构模块
2.3 服务端设计
2.4 客户端设计
以上内容摘自携程Apollo配置中心官网,关于Apollo配置中心的设计官网已经写的很详细了,这里不过多设计。官网介绍:https://www.apolloconfig.com/#/zh/design/apollo-design。
三、源码分析
最近看了一部分Apollo的源码,由于代码写的比较早,数据库当时使用的还是hibernate,服务间调用采用的RetryableRestTemplate(作者自己对RestTemplate进行了封装,加了重试机制),并且代码中使用到了大量事件监听者模式,理清了业务逻辑后,代码还是很容易读懂的,这里就不做具体介绍了。源码分析请移步芋道源码。
四、多环境、多集群的实现
4.1、问题思考
Q1、先来说一下多环境的实现,我们的环境一般分为开发环境、测试环境、生产环境,那如何保证指定环境启动时服务能正确读取到配置中心上相应环境的配置文件呢?
Q2、以开发环境为例,一个大型分布式微服务系统会有很多微服务子项目,那么在开发环境怎么对这些微服务配置进行管理呢?
Q3、再来说一下多集群的实现,配置中心如何支持同一个服务跨机房和多版本部署时使用不同的配置?
4.2 Apollo实现思路
我们先来介绍一下Apollo的实现,Apollo采用的是在每个环境下部署一个Config Service、Admin Service、以及我们的应用,服务发现是通过Eureka实现的,这样各个环境的程序注册到对应环境的Eureka上。这样就可以解决我们第一个问题。
Apollo会维护一个应用列表,应用列表中的每个应用由我们创建,主要包含应用名称、应用id等字段,我们通过该应用id来标识每个应用,就可以解决第二个问题。
针对第三个问题,Apollo加入了集群管理,通过为应用添加集群,将一个应用划分为若干个集群,可以使同一服务在不同的集群(如不同的数据中心,针对多版本情况,我们可以把相同版本的同一个服务划分为一个集群)使用不同的配置。
Apollo配置中心也有命名空间的概念,一个集群由若干个命名空间,每个命名空间中就包含我们的配置信息。以Spring Boot应用的application.properties文件为例,我们可以将application.properties拆分为若干份,比如数据库配置的、日志配置的、权限配置的,每一份对应一个命名空间。
Apollo中心的本质就是维护一个应用列表。下图是Apollo配置中心其中一个应用的全部信息,一个应用主要由项目信息、环境列表、集群、命名空间组成。
从上图中,我们发现Apollo配置信息是以key-value方式存放的。
其实体ER图大致如下(下图只绘制了部分字段,你去看官方给的脚本,你会发现表中存在大量字段冗余以及同一字段数据长度在不同表设置还不一样,MMP...):
Apollo中Portal服务是多个环境公用,通过在Portal服务中配置每个环境下Admin Service的地址,实现直接管理各个环境的配置。
4.3 自己实现
由于Apollo在不同的环境下都要部署一套Config Service、Admin Service,部署起来麻烦。
我们可以参考nacos的实现,只部署一套nacos服务,实现多环境下得配置管理。
我们采用命名空间的设计思想, 由Namespace + Group + Data ID 共同构建(类似Java里的包名 类名 泛型):
- namespace是可以用于区分部署环境的;
- Group是用来对服务分组的;
- Data ID是用来区分具体的服务;
默认情况: Namespace=public,Group=DEFAULT_GROUP。
Namespace默认是public,Namespace主要用来实现环境隔离。 比方说我们现在有三个环境:开发、测试、生产,我们就可以创建三个Namespace,不同的Namespace之间是隔离的。
Group默认是DEFAULT_GROUP,Group主要用来对服务分组。一个服务可以包含多个Cluster(集群),比如我们常说的zookeeper、kafka、hadoop等集群,我们在杭州机房的入若干台机器上分别部署zookeeper服务,组成Zookeeper集群A; 然后我们又在广州机房的入若干台机器上分别部署zookeeper服务,组成Zookeeper集群B,集群A和集群B分别使用不同的配置。这里的集群A的配置就对应我们的组A和zookeeper服务的Data ID确定的配置,集群B的配置就对应我们的组B和zookeeper服务的Data ID确定的配置。
综上所述,我们需要创建一张数据表config_namespace:
id | namespace_id | namesapce_name | remark | create_time | create_by | update_time | update_by |
主键 | 命名空间唯一标识符 | 命名空间名称 | 描述信息 | 创建时间/发布时间 | 创建者 | 更新时间 | 更新者 |
这里我们并没有去创建group、应用表。这些信息在为一个应用创建配置信息时指定即可。
五、服务治理的实现
5.1 配置格式校验
配置文件的格式一般有properties、yml、json、xml、text这几种。配置信息我们以字符串的信息存放,如图:
这里保存(为了简单,我们保存就是发布)时其实就是调用相应的接口,在各个接口中对各种格式的数据进行校验,校验通过时保存到数据库。
以properties文件为例,为了简单,我们可以创建一个数据表config_content:
id | data_id | group_name | content | format | remark | create_time | create_by | update_time | update_by |
主键 | 应用id | 组id | 配置内容 | 文件格式 | 描述信息 | 创建时间/发布时间 | 创建者 | 更新时间 | 更新者 |
将上图配置内容保存在content字段中即可,而Apollo是将properties文件中每一个键值对保存成一条记录,存放在Item表中的。而其它格式文件Apollo保存为一条记录。
这里我们为了统一,所以就将配置信息都放在content中,使用一条记录保存。
5.2 配置版本控制
我们需要在每次对配置进行新增、删除、修改的时候,记录下配置的变更信息。以Apollo Commit为例:
我们可以发现Commit表的ChangeSet保存了我们每次对配置的操作记录。ChangeSet是一个json字符串,主要包含三块内容createItems、updateItems、deleteItems,比如我们将某个namespace的key为key4的配置项的值从value4123修改为value41234,将会产生一条这样的纪录:
{ "createItems": [ ], "updateItems": [ { "oldItem": { "namespaceId": 32, "key": "key4", "value": "value4123", "comment": "123", "lineNum": 4, "id": 15, "isDeleted": false, "dataChangeCreatedBy": "apollo", "dataChangeCreatedTime": "2018-04-27 16:49:59", "dataChangeLastModifiedBy": "apollo", "dataChangeLastModifiedTime": "2018-04-27 22:37:52" }, "newItem": { "namespaceId": 32, "key": "key4", "value": "value41234", "comment": "123", "lineNum": 4, "id": 15, "isDeleted": false, "dataChangeCreatedBy": "apollo", "dataChangeCreatedTime": "2018-04-27 16:49:59", "dataChangeLastModifiedBy": "apollo", "dataChangeLastModifiedTime": "2018-04-27 22:38:58" } } ], "deleteItems": [ ] }
同理,我们也可以采用类似的方式,创建一张config_commit表,用来记录每次对配置的增加、删除、修改信息。
id | content_id | insert | update_before | update_after | delete | create_time | create_by |
主键 | 应用id | 插入内容 | 更新前内容 | 更新后内容 | 删除内容 | 创建时间 | 创建者 |
5.3 配置审计
配置审计其实就可以看作日志记录,只不过这个日志记录只是记录用户对配置信息的操作, 主要记录某人在某时做了什么配置操作。
参考文章:
[3] Apollo官网指导手册