作者 | 宋顺
编辑 | 小智
随着微服务的流行,应用和机器数量急剧增长,程序配置也愈加繁杂:各种功能的开关、参数的配置、服务器的地址等等。在这样的大环境下,传统的通过配置文件、数据库等方式已经越来越无法满足我们对配置管理的需求。配置中心,应运而生!在一定程度上,配置中心就成为了微服务的大脑,如何用好这个大脑,让微服务更智能?本文整理自携程框架架构研发部技术专家宋顺在 QCon 上海 2018 站的演讲。
背景介绍
随着微服务的流行,应用和机器数量急剧增长,程序配置也愈加繁杂:各种功能的开关、参数的配置、服务器的地址等等。同时,我们对程序配置的期望值也越来越高:配置修改后实时生效,灰度发布,分环境、分集群管理,完善的权限、审核机制等等。
在这样的大环境下,传统的通过配置文件、数据库等方式已经越来越无法满足我们对配置管理的需求。
配置中心,应运而生!
通过配置中心,我们可以方便地管理微服务在不同环境中的配置,从而可以在运行时动态调整服务行为,真正实现配置即『控制』的目标。所以,在一定程度上,配置中心就成为了微服务的大脑,如何用好这个大脑,让微服务更『智能』,也就成为了一项比较重要的议题。
为什么需要配置中心?
程序的发布其实和卫星的发射有一些相似之处。
当卫星发射升天后,基本就处于自主驾驶状态了,一般情况下就是按照预设的轨道运行,间歇可能会收到一些来自地面的『控制』信号对运行姿态进行一定的调整。
程序发布其实也是这样,当程序发布到生产环境后,一般就是按照预设的逻辑运行,我们无法直接去干预程序的行为,不过可以通过调整配置参数来动态调整程序的行为。这些配置参数就代表着我们对程序的『控制』信号。
由此可见,配置对程序的运行非常重要,我们需要一种可靠性高、实时性好的手段,从而可以随时对程序『发号施令』。
鉴于配置对程序正确运行的重要性,所以配置的治理就显得尤为重要了,比如:
1、权限控制、审计日志
由于配置能改变程序的行为,不正确的配置甚至能引起灾难,所以对配置的修改必须有比较完善的权限控制。同时也需要有一套完善的审计机制,能够方便地追溯是谁改的配置、改了什么、什么时候改的等等。
2、灰度发布、配置回滚
对于一些比较重要的配置变更,我们一般会倾向于先在少量机器上修改看看效果,如果没问题再推给所有机器。同时如果发现配置改得有问题的话,需要能够方便地回滚配置。
3、不同环境、集群管理
同一份程序在不同的环境(开发,测试,生产)、不同的集群(如不同的数据中心)经常需要有不同的配置,所以需要有完善的环境、集群配置管理。
单体应用时代,应用数量比较少,配置也相对比较简单,还有可能让运维登上机器一台一台修改程序的配置文件。
随着微服务的流行,大应用拆成小应用,小应用拆成多个独立的服务,导致微服务的节点数量非常多,配置也随着服务数量增加而急剧增长,再让运维登上机器一台一台手工修改配置不仅效率低,而且还容易出错。如果碰到了紧急事件需要大规模迅速修改配置,估计运维人员也只能两手一摊了。
所以,综合以上几个要素,我们 需要一个统一的配置中心来管理微服务的配置。
配置中心的一般模样
那么,我们应该需要一个什么样的配置中心呢?
接下来就以 开源配置中心 Apollo 为例,来看一下配置中心的一般模样。
如前面所论述的:配置需要治理,所以配置中心需要具备完善的治理能力,比如:
统一管理不同环境、不同集群的配置
支持灰度发布
支持已发布的配置回滚
完善的权限管理、操作审计日志
Apollo 配置中心的管理界面如下图所示,可以发现相应的治理功能还是非常齐全的。
配置即『控制』,所以在一定程度上,配置中心已经成为了微服务的大脑,作为大脑,可用性显然是要求非常高的。
接下来我们一起看一下 Apollo 是怎么实现高可用的。
Apollo at a glance
如下即是 Apollo 的基础模型:
用户在配置中心对配置进行修改并发布
配置中心通知 Apollo 客户端有配置更新
Apollo 客户端从配置中心拉取最新的配置、更新本地配置并通知到应用
服务端高可用
上图简要描述了 Apollo 的服务端设计,我们可以从下往上看:
首先最下面是一个 DB,我们的配置是放在 DB 里的,然后在 DB 之上有两个服务:Config Service 和 Admin Service;
Config Service 提供配置的读取、推送等功能,服务对象是 Apollo 客户端;
Admin Service 提供配置的修改、发布等功能,服务对象是 Apollo Portal(管理界面);
Config Service 和 Admin Service 都是多实例、无状态部署,所以需要将自己注册到 Eureka 中并保持心跳;
在 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 Service、Eureka 和 Meta Server 三个逻辑角色部署在同一个 JVM 进程中;
通过上述的设计,可以看到整个服务端是无单点,有效地保证了服务端的可用性。
客户端高可用
上图简要描述了 Apollo 客户端的实现原理:
客户端和服务端保持了一个长连接,从而能第一时间获得配置更新的推送。(通过 Http Long Polling 实现);
-
客户端还会定时从 Apollo 配置中心服务端拉取应用的最新配置;
这是一个 fallback 机制,为了防止推送机制失效导致配置不更新;
客户端定时拉取会上报本地版本,所以一般情况下,对于定时拉取的操作,服务端都会返回 304 - Not Modified。
客户端从 Apollo 配置中心服务端获取到应用的最新配置后,会保存在内存中,所以我们的应用程序来获取配置的时候其实始终是从内存中获取的;
-
客户端还会把从服务端获取到的配置在本地文件系统缓存一份;
这主要是为了容灾,假设应用程序重启的时候,恰好远端服务全挂了,或者网络有故障,应用程序依然能从本地恢复配置。
通过这种推拉结合的机制,以及内存和本地文件双缓存的方式,有效地保证了客户端的可用性。
可用性场景举例
配置即『控制』,所以我们希望我们的控制指令能迅速、准确地传达到应用程序,我们来看看 Apollo 是如何实现实时性的。
上图简要描述了配置发布的大致过程:
用户在 Portal 操作配置发布;
Portal 调用 Admin Service 的接口操作发布;
Admin Service 发布配置后,发送 ReleaseMessage 给各个 Config Service;
Config Service 收到 ReleaseMessage 后,通知对应的客户端。
发送 ReleaseMessage 的实现方式
Admin Service 在配置发布后,需要通知所有的 Config Service 有配置发布,从而 Config Service 可以通知对应的客户端来拉取最新的配置。
从概念上来看,这是一个典型的消息使用场景,Admin Service 作为 producer 发出消息,各个 Config Service 作为 consumer 消费消息。通过一个消息组件(Message Queue)就能很好的实现 Admin Service 和 Config Service 的解耦。
在实现上,考虑到 Apollo 的实际使用场景,以及为了尽可能减少外部依赖,我们没有采用外部的消息中间件,而是通过数据库实现了一个简单的消息队列。
实现方式如下:
Admin Service 在配置发布后会往 ReleaseMessage 表插入一条消息记录,消息内容就是配置发布的 AppId+Cluster+Namespace
Config Service 有一个线程会每秒扫描一次 ReleaseMessage 表,看看是否有新的消息记录
Config Service 如果发现有新的消息记录,解析得到配置发布的 AppId+Cluster+Namespace 后,通知到对应的客户端
示意图如下:
如何让微服务更『智能』?
接下来我们来看一下结合配置中心,我们能做哪些有趣的事情,让微服务更智能。
发布开关
发布开关一般用于发布过程中,比如:
有些新功能依赖于其它系统的新接口,而其它系统的发布周期未必和自己的系统一致,可以加个发布开关,默认把该功能关闭,等依赖系统上线后再打开;
有些新功能有较大风险,可以加个发布开关,上线后一旦有问题可以迅速关闭。
需要注意的是,发布开关应该是短暂存在的(1-2 周),一旦功能稳定后需要及时清除开关代码。
实验开关
实验开关通常用于对比测试或功能测试,比如:
-
A/B 测试
针对特定用户应用新的推荐算法;
针对特定百分比的用户使用新的下单流程;
-
QA 测试
有些重大功能已经对外宣称在某年某日发布
可以事先发到生产环境,只对内部用户打开,测试没问题后按时对全部用户开放
实验开关应该也是短暂存在的,一旦实验结束了需要及时清除实验开关代码。
运维开关
运维开关通常用于提升系统稳定性,比如:
大促前可以把一些非关键功能关闭来提升系统容量;
当系统出现问题时可以关闭非关键功能来保证核心功能正常工作。
运维开关可能会长期存在,而且一般会涉及多个系统,所以需要提前规划。
限流
服务就像高速公路一样,在正常情况下非常通畅,不过一旦流量突增(比如大促、遭受 DDOS 攻击)时,如果没有做好限流,就会导致系统整个被冲垮,所有用户都无法访问。
正常的高速公路
超出容量的高速公路
所以我们需要限流机制来应对此类问题,一般的做法是在网关或 RPC 框架层添加限流逻辑,结合配置中心的动态推送能力实现动态调整限流规则配置。
黑白名单
对于一些关键服务,哪怕是在内网环境中一般也会对调用方有所限制,比如:
有敏感信息的服务可以通过配置白名单来限制只有某些应用或 IP 才能调用
某个调用方代码有问题导致超大量调用,对服务稳定性产生了影响,可以通过配置黑名单来暂时屏蔽这个调用方或 IP
一般的做法是在 RPC 框架层添加校验逻辑,结合配置中心的动态推送能力来实现动态调整黑白名单配置。
数据库的迁移也是挺普遍的,比如:原来使用的 SQL Server,现在需要迁移到 MySQL,这种情况就可以结合配置中心来实现平滑迁移:
单写 SQL Server,100% 读 SQL Server;
初始化 MySQL;
双写 SQL Server 和 MySQL,100% 读 SQL Server;
线下校验、补齐 MySQL 数据;
双写 SQL Server 和 MySQL,90% 读 SQL Server,10% 读 MySQL;
双写 SQL Server 和 MySQL,100% 读 MySQL;
单写 MySQL,100% 读 MySQL;
切换完成。
上述的读写开关和比例配置都可以通过配置中心实现动态调整。
服务运行过程中,经常会遇到需要通过日志来排查定位问题的情况,然而这里却有个两难:
如果日志级别很高(如:ERROR),可能对排查问题也不会有太大帮助
如果日志级别很低(如:DEBUG),日常运行会带来非常大的日志量,造成系统性能下降
为了兼顾性能和排查问题,我们可以借助于日志组件和配置中心实现日志级别动态调整。
以 Spring Boot 和 Apollo 结合为例:
@ApolloConfigChangeListener
private void onChange(ConfigChangeEvent changeEvent) {
refreshLoggingLevels(changeEvent.changedKeys());
}
private void refreshLoggingLevels(Set<String> changedKeys) {
boolean loggingLevelChanged = false;
for (String changedKey : changedKeys) {
if (changedKey.startsWith("logging.level.")) {
loggingLevelChanged = true;
break;
}
}
if (loggingLevelChanged) {
// refresh logging levels
this.applicationContext.publishEvent(new EnvironmentChangeEvent(changedKeys));
}
}
详细样例代码可以参考:
https://github.com/ctripcorp/apollo-use-cases/tree/master/spring-cloud-logger
网关的核心功能之一就是路由转发,而其中的路由信息也是经常会需要变化的,我们也可以结合配置中心实现动态更新路由信息。
以 Spring Cloud Zuul 和 Apollo 结合为例:
@ApolloConfigChangeListener
public void onChange(ConfigChangeEvent changeEvent) {
boolean zuulPropertiesChanged = false;
for (String changedKey : changeEvent.changedKeys()) {
if (changedKey.startsWith("zuul.")) {
zuulPropertiesChanged = true;
break;
}
}
if (zuulPropertiesChanged) {
refreshZuulProperties(changeEvent);
}
}
private void refreshZuulProperties(ConfigChangeEvent changeEvent) {
// rebind configuration beans, e.g. ZuulProperties
this.applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));
// refresh routes
this.applicationContext.publishEvent(new RoutesRefreshedEvent(routeLocator));
}
详细样例代码可以参考:
https://github.com/ctripcorp/apollo-use-cases/tree/master/spring-cloud-zuul
数据库是应用运行过程中的一个非常重要的资源,承担了非常重要的角色。
在运行过程中,我们会遇到各种不同的场景需要让应用程序切换数据库连接,比如:数据库维护、数据库宕机主从切换等。
切换过程如下图所示:
以 Spring Boot 和 Apollo 结合为例:
@Configuration
public class RefreshableDataSourceConfiguration {
@Bean
public DynamicDataSource dataSource(DataSourceManager dataSourceManager) {
DataSource actualDataSource = dataSourceManager.createDataSource();
return new DynamicDataSource(actualDataSource);
}
}
public class DynamicDataSource implements DataSource {
private final AtomicReference<DataSource> dataSourceAtomicReference;
public DynamicDataSource(DataSource dataSource) {
dataSourceAtomicReference = new AtomicReference<>(dataSource);
}
// set the new data source and return the previous one
public DataSource setDataSource(DataSource newDataSource){
return dataSourceAtomicReference.getAndSet(newDataSource);
}
@Override
public Connection getConnection() throws SQLException {
return dataSourceAtomicReference.get().getConnection();
}
...
}
@ApolloConfigChangeListener
public void onChange(ConfigChangeEvent changeEvent) {
boolean dataSourceConfigChanged = false;
for (String changedKey : changeEvent.changedKeys()) {
if (changedKey.startsWith("spring.datasource.")) {
dataSourceConfigChanged = true;
break;
}
}
if (dataSourceConfigChanged) {
refreshDataSource(changeEvent.changedKeys());
}
}
private synchronized void refreshDataSource(Set<String> changedKeys) {
try {
// rebind configuration beans, e.g. DataSourceProperties
this.applicationContext.publishEvent(new EnvironmentChangeEvent(changedKeys));
DataSource newDataSource = dataSourceManager.createAndTestDataSource();
DataSource oldDataSource = dynamicDataSource.setDataSource(newDataSource);
asyncTerminate(oldDataSource);
} catch (Throwable ex) {
logger.error("Refreshing data source failed", ex);
}
}
详细样例代码可以参考:
https://github.com/ctripcorp/apollo-use-cases/tree/master/dynamic-datasource
最佳实践
公共组件是指那些发布给其它应用使用的客户端代码,比如 RPC 客户端、DAL 客户端等。
这类组件一般是由单独的团队(如中间件团队)开发、维护,但是运行时是在业务实际应用内的,所以本质上可以认为是应用的一部分。
这类组件的特殊之处在于大部分的应用都会直接使用中间件团队提供的默认值,少部分的应用需要根据自己的实际情况对默认值进行调整。
比如数据库连接池的最小空闲连接数量(minimumIdle),出于对数据库资源的保护,DBA 要求将全公司默认的 minimumIdle 设为 1,对大部分的应用可能都适用,不过有些核心 / 高流量应用可能觉得太小,需要设为 10。
针对这种情况,可以借助于 Apollo 提供的 Namespace 实现:
-
中间件团队创建一个名为
dal
的公共 Namespace,设置全公司的数据库连接池默认配置minimumIdle = 1 maximumPoolSize = 20
dal 组件的代码会读取
dal
公共 Namespace 的配置对大部分的应用由于默认配置已经适用,所以不用做任何事情
对于少量核心 / 高流量应用如果需要调整 minimumIdle 的值,只需要关联
dal
公共 Namespace,然后对需要覆盖的配置做调整即可,调整后的配置仅对该应用自己生效
minimumIdle = 10
通过这种方式的好处是不管是中间件团队,还是应用开发,都可以灵活地动态调整公共组件的配置。
对于重要的配置一定要做灰度发布,先在一台或多台机器上生效后观察效果,如果没有问题再推给所有的机器。
对于公共组件的配置,建议先在一个或多个应用上生效后观察效果,没有问题再推给所有的应用。
生产环境建议启用发布审核功能,简单而言就是如果某个人修改了配置,那么必须由另一个人审核后才可以发布,以避免由于头脑不清醒、手一抖之类的造成生产事故。
结 语
本文主要介绍了以下几方面:
-
为什么需要配置中心?
配置即『控制』
配置需要治理
微服务带来的配置复杂性
-
配置中心的一般模样
以 Apollo 为例子,介绍了配置中心所具备的特征
介绍了 Apollo 是如何实现高可用和实时性的
-
如何让微服务更『智能』?
通过几个案例,分享了如何借助于配置中心使微服务更『智能』
-
配置中心的最佳实践
公共组件的配置
灰度发布
发布审核
最后,希望大家在平时工作中都能用好配置中心,更好地服务于业务场景,使微服务更『智能』,实现从青铜到王者的跨越!