作者 | 孙自然
策划 | 蔡芳芳
1引言
在从单体应用向微服务架构转型的过程中,服务配置管理从只需要应对一个单体服务,变为应对大量分布式服务,难度呈几何级增加。为了解决这个难题,各种应对分布式服务的配置中心应运而生,如何搭建一个高效合理的配置中心已经成为每个大型分布式系统必经的考验。而在服务“上云”的大趋势下,如何让配置中心在云平台顺利落地,更进一步,如何借助云计算的优势让配置中心如虎添翼,目前业内对这一块还处于探索阶段。本文将介绍 FreeWheel 核心业务系统在 AWS 云平台上搭建配置中心的实战,作为搭建云原生配置中心的参考,希望能给大家带来启发。
更多相关内容参见系列文章:云原生环境下的微服务实践全解
2为什么需要配置中心?
分布式系统兴起之后,配置管理变得尤为复杂。1984 年,业界就有两位学者 Kramer Jeff 和 Magee Jeff 在 IEEE 发表了一篇文章《分布式系统的动态配置》,其中阐述到:
动态系统配置是在系统运行时对其进行修改和扩展的能力。在大型分布式系统中,这是一个必不可缺的功能,因为如果需要停止整个系统来对其部分硬件或软件进行修改,在生产环境是难以接受的,或者会产生较大经济损失。另外,动态配置也有助于系统在生产环境上组件的增量集成,以达成系统演进的目的。
这篇文章指出了动态配置对于分布式系统的重要性,即在系统运行时,如何经济安全地对系统进行调整。现在分布式系统已经发展到了从前难以想象的复杂程度,除了动态配置,配置管理还面临更多挑战,例如:
- 如何统一管理数量众多的服务配置
- 如何在异地众多机器节点上部署配置
- 如何实现灰度
- 如何确定配置是否生效
- 如何对配置进行灾备
- …
为了解决这些挑战,配置中心应运而生。配置中心就是分布式服务中统一管理服务配置的系统,它具备高效和动态控制服务的能力。配置中心一般具备服务注册、服务配置管理、部署服务配置等基本功能。下面是一个微服务架构下配置中心的示意图:
配置中心一般会包含服务端、客户端和界面这三个组件:每个微服务启动时可以通过客户端进行服务注册;用户可以通过界面创建、修改和部署配置;动态配置功能可以通过服务端实时推送、客户端定期拉取或者两者推拉结合来实现。
3FreeWheel 云原生配置中心实战
痛点
- 服务配置的数量大幅增加
Freewheel 核心业务系统中已拆分出数十个独立的微服务,每个微服务都需要部署多个环境(Staging、Production、开发、测试环境),多个集群,多个区域 (Region)。开发人员需要维护多套服务配置。
- 服务配置的部署方式不同
Freewheel 核心业务系统在云平台和数据中心的部署方式,以及在不同环境的部署方式各不相同。开发人员需要投入时间成本去学习和管理各种部署方式。
- 缺乏动态配置功能
Freewheel 核心业务系统在运行时修改服务配置的流程较为繁琐,包括提交、评审、合并分支、运行测试、打包、部署 Staging 和 Production 等。这个效率不能满足团队需求,例如 Freewheel 作为面向企业级客户提供广告投放服务的系统,在广告投放的高峰期处理的数据量远高于平常,工程师团队需要动态配置服务的超时参数;又如在生产环境对问题进行定位和调试时,需要尽快调高日志级别,以便快速解决问题。
- 保证 Freewheel 核心业务系统面向企业级用户的高可用性
软件系统总是会宕机的,当配置中心系统被许多微服务依赖做配置管理时,一定得考虑到它宕机时其他服务该怎么办。所以配置中心需要实现为弱依赖而非强依赖,即配置中心出现系统故障时,其他服务也能正常启动和运行。
保证配置中心的安全性配置中心的管理对象是比较敏感的服务配置项,对安全性有较高要求,需要合理配置用户和集群的访问权限。
配置中心选型
为了解决上述痛点,我们开始为 Freewheel 核心业务系统设计并搭建配置中心。在选型阶段,我们参考了当时较为成熟的几个配置中心产品,如 Apollo、Nacos、Consul 等。配置中心的第一个版本中,我们选择了 Apollo 作为服务端和界面,因为 Apollo 在用户界面友好度、核心功能支持度、社区文档完善度方面都较为突出。Apollo 产品架构主要包含 Config Service(提供配置推送和拉取接口)、Admin Service(提供配置管理接口)、 Portal(用户界面)和客户端,如下图所示:
Freewheel 核心业务系统当时正往 AWS 云服务上迁徙,我们为配置中心开发了客户端,并在 AWS 开发环境部署了 Apollo 的相关服务。但随后我们产生了一些顾虑。
首先是学习维护成本:Freewheel 核心业务系统的微服务架构使用 GO 技术栈,与 Apollo 使用的 Java 不一致,工程师团队需要投入额外的学习成本;使用 Apollo 还需要在 AWS 上维护四套非云原生的服务:Config Service、Admin Service、Portal 和 DB,由于缺乏产品级的 Apollo 技术支持,会产生较大的维护成本。其次是产品国际化的问题:Freewheel 对于开源产品的使用有严格的审计流程,需要提交代码库、英文版的架构设计和使用说明文档。Apollo 作为一款国内自研产品,没有发布详细的英文文档。
因此我们开始考察其他产品如 AWS AppConfig。调研发现,AppConfig 的功能没有 Apollo 那么全面:
配置中心一个重要的服务端推送功能不被 AppConfig 支持,这会影响配置中心的 SLA,即配置生效的时延。然而作为一家 B2B 公司,Freewheel 对配置中心 SLA 的要求不太高。而配置中心其他几个重要功能:客户端拉取、用户权限控制、用户界面,我们能基于现有的微服务架构用较小的开发成本实现。但使用 AppConfig 的好处是:作为 AWS 云原生服务,AppConfig 跟我们的 AWS 服务集群有很好的契合度,能方便获得 AWS 技术团队的支持,降低学习维护成本和使用成本。我们估算了配置中心的费用,主要包括 AppConfig API 调用和 S3 费用。假设一个微服务部署两个区域,启动 3 个 POD,配置文件大小为 10K,每天更新两次配置,每分钟轮询一次 AppConfig,那么这个微服务使用配置中心的费用大约是 0.6 美元 / 月。
综合考虑 Freewheel 的业务需求和使用成本后,我们采用了基于 AWS AppConfig 的配置中心架构。
配置中心架构
配置中心的核心模块包括 AWS AppConfig 服务端、微服务客户端、用户界面。主要使用场景包括:
- 各个微服务通过用户界面管理配置:包括创建配置应用程序,向 AWS S3 读写配置文件, 通过 AppConfig 部署最新的配置,在数据库中记录用户的操作历史。
- 各个微服务通过客户端对 AppConfig 服务端进行定期轮询,一旦发现配置更新,就从 AppConfig 服务端拉取配置并使之在微服务中生效。
配置中心落地实现
AWS AppConfig 服务端AWS AppConfig 是 AWS 开发用来创建、管理和快速部署应用配置的服务。客户端和用户界面的实现与 AppConfig 提供服务的实体密切相关。AppConfig 通过以下实体来管理应用配置:
- 应用程序(Application):应用程序就是需要 AppConfig 提供配置管理的应用,如在 EC2 实例上运行的微服务,AWS Lambda 的无服务器应用程序等等。
- 环境(Environment):对于每个应用程序,可以定义一个或多个环境,例如 Staging 或 Production。
- 配置(Configuration)和配置文件(Configuration Profile):每个环境下只有一个配置,配置文件就是记录配置内容的文件对象。每次更新配置,实际更新的是配置文件的内容(版本)。
- 配置策略(Deployment Strategy):配置策略定义了配置的部署方式,如部署节点是线性扩张还是指数扩张、部署时长、监控和回滚策略等。
下图是 Freewheel 核心业务系统集群的实际应用场景,由于 Development、Staging 、Production 三个环境的集群互相隔离,我们为不同环境的集群创建了独立的 AppConfig 服务端和用户界面。微服务在用户界面创建与之关联的应用程序,这个应用程序仅包含一个环境。我们选择了 S3 来存储配置文件,可以通过用户界面读写配置文件。目前配置中心在部署时使用的配置策略是每 30 秒部署 50% 的节点。
配置中心客户端
客户端是微服务进行配置轮询和配置更新的重要组件。我们在配置中心客户端做了灾备处理,从而实现了微服务集群对配置中心的弱依赖。即便配置中心的服务端或者用户界面出现故障,微服务集群的运行也并不受影响,只是不能使用配置管理的功能。配置中心客户端的工作流程如下:
微服务启动后,我们会将备份配置文件加载到内存中,然后启动一个 Go Routine 关联配置中心,按照一定时间间隔来轮询配置。如果发现配置更新,就把更新内容合并到内存配置和其他定制的配置中,否则等待下一次轮询。客户端使用 Go 语言开发,下面是关联和查询配置中心的示例代码:
func InitConfigCenter(serviceName string, callbacks ...CustomCallback) {
go func() {
for {
func() {
defer func() {
// 处理Panic
}()
appConfigClient := getAppConfigClient()
// 查询配置更新,如有更新则使之生效
GetAndMergeAppConfig(appConfigClient, serviceName, callbacks...)
}()
time.Sleep(time.Duration(pollingDuration) * time.Second)
}
}()
}
- 参数 serviceName:在服务端具有唯一性的标志符,一般等同微服务名。管理人员在用户界面需要输入同样的 serviceName 去创建 AppConfig 应用程序,然后客户端和服务端通过该 serviceName 匹配应用程序。
- 参数 CustomCallback:微服务可定制的修改配置的接口。获取配置更新后,客户端会默认修改内存配置使配置生效。但有些配置不是从内存配置中读取的,例如存储在全局变量里的配置,此时可以通过这个接口定制更新配置的方法。
考虑到弱依赖的设计原则,客户端内存配置的更新采用了合并策略(Merge)而非替代策略。初始内存配置从备份读取,随后从服务端不停拉取最新配置进行合并。服务端配置可以对内存配置进行全量覆盖、部分覆盖、或者新增配置。
客户端的内存配置管理是基于 Viper("github.com/spf13/viper")实现的,合并配置时使用了 Viper.MergeConfig 方法:
func mergeAppConfig(newConfig *appconfig.GetConfigurationOutput, callbacks ...CustomCallback) {
memoryConfig := config.Get()
if err := memoryConfig.MergeConfig(bytes.NewReader(newConfig.Content)); err != nil {
// 处理配置合并错误
}
// 更新内存配置
config.Set(memoryConfig)
// 更新定制配置
for i, callback := range callbacks {
callback.CustomCallbackFunc(newConfig)
}
}
客户端的一个重要逻辑是配置版本的保存和比对。客户端在本地存储了之前轮询获得的服务端最新配置版本,每次调用 AppConfig API 查询时都会输入这个配置版本。AppConfig API 会比较请求里的配置版本和服务端最新的配置版本,两者不一致时会返回最新的配置版本和配置内容,否则返回原来的配置版本。版本不一致时,调用 API 的费用会比平时高很多。客户端收到服务端答复后,再次比较本地和答复里的配置版本,如果不一致就会保存新的版本,并且进行配置合并。下面是调用 AppConfig GetConfiguration API 的代码:
import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/appconfig"
)
var latestConfigVersion = "-1" // 第一次查询时的初始版本
func getAppConfig(region, env, clientId, configuration, applicationName string) (*appconfig.GetConfigurationOutput, error) {
awsConfig := &aws.Config{ Region: aws.String(region) }
mySession := session.Must(session.NewSession(awsConfig))
appConfigClient := appconfig.New(mySession, aws.NewConfig())
return appConfigClient.GetConfiguration(&appconfig.GetConfigurationInput{
Application: &applicationName,
Environment: &env,
Configuration: &configuration,
ClientConfigurationVersion: &latestConfigVersion, // 使用本地记录的最新服务端配置版本
ClientId: &clientId,
})
}
GetConfiguration 输入包括 AppConfig 的应用程序名、环境名、配置名、配置文件版本和客户端指定的唯一 ClientId,输出 GetConfigurationOutput 包括配置文件版本和配置内容(可选项)。
配置中心用户界面
配置中心用户界面是用来统一管理各个微服务配置的窗口。我们在 Freewheel 内部业务数据查询平台 Falcon 上搭建了配置中心的用户界面,仅允许 LDAP 账户开通配置中心的访问或管理权限。
AppConfig 服务在 AWS 控制平台也有自己的管理界面,但是不能满足我们的需求。首先 AWS 控制平台的权限管理比较严格,工程师团队在生产环境只有部分读权限,没有写权限,如果我们需要在 AWS 控制平台上管理和部署配置,每次都需要单独申请权限。另外 AppConfig 原生的管理界面比较简单,不能看到具体的配置项内容,需要去相应的 S3 页面下载配置文件,也不具备配置对比和查看用户历史操作的功能。
配置中心用户界面架构:
配置中心用户界面包含了前端和后端模块,前端模块由 React 实现,包括以下页面:
- 主页:展示所有微服务应用程序列表。
- 应用页面:展示单个微服务应用程序的详细信息,由主页进入。
- 创建页面:为一个新的微服务创建应用程序,由主页进入。
- 配置上传页面:上传新的配置文件,由应用页面进入。
- 配置部署页面:选择一个配置文件版本进行配置部署,由应用页面进入。
- 历史记录页面:展示应用程序所有部署历史和用户,由应用页面进入。
后端模块由 Node.js 实现,分为配置管理和用户管理两个子模块。在配置管理模块调用 JS SDK 的 AppConfig Client 和 S3 Client 实现上述前端页面功能;在用户管理模块实现了权限管理和历史记录功能,用户的创建、上传、部署行为会被记录到数据库中。创建一个可用的 AppConfig 应用程序实际上包含了四个步骤:创建应用程序,创建环境,上传初始配置文件,在应用程序中绑定配置文件。在应用程序中关联配置文件后,会记录配置文件的地址和版本。每次为这个应用上传并部署新的配置文件后,关联配置文件的版本就会变动。在历史记录页面可以看到历次部署的状态、开始时间、配置版本、部署时长和操作用户,还可以对配置内容进行灵活对比。下面给大家展示一下配置中心的用户界面。
Falcon 平台主页:
配置中心主页:
配置中心历史记录页面:
4落地常见问题
在搭建配置中心的实战过程中,我们踩了不少的坑,也总结了一些经验。在这里分享给大家,希望能对大家有所帮助。
- 如何合理使用 AppConfig 服务使其收费最低?
GetConfiguration API 是 AWS AppConfig 服务中最重要的 API,通过轮询这个 API 可以获得配置版本变化信息和最新的配置项内容。该 API 请求所带的参数 ClientConfigurationVersion 与服务端最新的配置版本一致时收费较低,否则收费较高。为避免额外收费,客户端一定要在本地存储之前查询的服务端最新的配置版本,在调用 API 时使用。
客户端还需要注意一个逻辑,就是客户端真实生效的配置版本不一定等同于服务端最新的配置版本,因为客户端从发现配置版本变化到启动配置更新这一过程是可能出错的。即使客户端在配置更新过程出错,也要保存出错版本供下次调用使用。
- 如何获取有效的配置文件版本?
AppConfig 的配置文件版本等同于 S3 文件版本。但 S3 上传配置文件和 AppConfig 部署配置不是一个事务操作,所以最新的 S3 文件版本不等同于 AppConfig 的有效配置文件版本。所以要获取 AppConfig 最新生效的配置文件版本,不能调用 S3 API,而是调用 AppConfig ListDeploymentsCommand API,读取返回列表中最新的配置版本。
- 如何在本地开发环境调试 AppConfig?
在本地开发环境调试 AppConfig 时不能使用生产环境的 IAM 角色,可以使用一个 AWS 账号的临时凭证来发送 AppConfig API 请求:
import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
)
func getAWSSession(region, env, accessKey, secretKey, awsSessionToken string) *session.Session {
awsConfig := &aws.Config{ Region: region }
if env == DEV_ENV {
creds := credentials.NewStaticCredentials(accessKey, secretKey, awsSessionToken)
awsConfig.Credentials = creds
}
return session.Must(session.NewSession(awsConfig))
}
其中 accessKey、secretKey、awsSessionToken 来源于 AWS CLI 为这个 AWS 账号提供的临时凭证信息,可在 AWS 控制平台上用账号登陆后实时查询。不添加这个临时凭证信息就会自动使用 EC2 默认或者配置的 IAM 角色凭证。
- 如何合理配置 AppConfig 服务的读写权限?
以 Freewheel 配置中心的客户端和用户界面为例,客户端需要发送大量 AppConfig 读请求,用户界面需要发送少量 AppConfig 读写请求。所以我们为客户端 EC2 的默认 IAM 配置了 AppConfig 读权限,为用户界面 EC2 申请了特殊 IAM 角色并为它配置了 AppConfig 读写权限。要使特殊 IAM 角色生效,需要修改 K8S 部署文件:
// deployment.yaml
spec:
template:
sepc:
serviceAccountName: {{ $.Your.Arn.Account.Name }}
// serviceaccount.yaml
annotations:
eks.amazonaws.com/role-arn: {{ $.Your.Arn.Role.Name }}
特殊 IAM 角色配置成功后,可以在 POD 里查询环境变量确认:AWS_ROLE_ARN,AWS_WEB_IDENTITY_TOKEN_FILE。使用特殊 IAM 角色,需要通过 AWS STS 获取临时凭证后再发送 AWS 服务请求。注意如使用 JS SDK V3 发送请求,则需使用 v3.10 或以上版本(否则不支持获取凭证的功能),如下所示:
// AWS JS SDK V3获取凭证
const { AppConfigClient } = require("@aws-sdk/client-appconfig");
const { getDefaultRoleAssumerWithWebIdentity } = require("@aws-sdk/client-sts");
const { fromTokenFile } = require("@aws-sdk/credential-provider-web-identity");
const appconfig = new AppConfigClient({ credentials: fromTokenFile({
roleAssumerWithWebIdentity: getDefaultRoleAssumerWithWebIdentity() })
});
- 使用特殊 IAM 角色遇到 ExpiredTokenException 怎么解决?
EC2 默认 IAM 的权限长期有效,特殊 IAM 角色的凭证是有期限的。如果在服务运行时遇到了 ExpiredTokenException,需要审视一下 AWS API Client 的生命周期。如配置中心用户界面,为每次请求重新生成一个 AppConfigClient 来避免凭证过期。
5配置中心未来展望
配置驱动资源正在成为云计算的一个重要技术趋势,即认为不光是应用进程,与云计算相关的所有资源都可以通过配置去驱动。这将令配置中心的云端之路充满变化和挑战。未来我们也会继续思考配置中心的其他应用模式,比如如何在云服务平台上与其他服务整合,如何去独立支撑某些业务场景等等。
作者简介:
孙自然 Lead Software Engineer,FreeWheel
毕业于北京大学计算机系,目前就职于 Comcast FreeWheel 核心业务团队。研究方向为微服务架构、云计算、产品设计等领域,热衷于新技术的探索与分享。
点击阅读原文链接即可阅读系列文章:云原生环境下的微服务实践全解
本周好文推荐
继进入紧急状态后,美国再次提升优先级,将黑客攻击与恐怖袭击并列
微软在低代码领域憋大招,跟RPA厂商抢生意?
基于HarmonyOS 2的“超级终端”来了!这几款手机即日起可升级
问了尤雨溪25个问题后,我的很多想法开始变了
字节跳动与腾讯隔空骂战;网传“美团员工黑入拼多多获薪资信息”;深圳大数据杀熟或可罚5000万元|Q资讯
每周精要上线移动端,立刻订阅,你将获得
InfoQ 用户每周必看的精华内容集合:
资深技术编辑撰写或编译的全球 IT 要闻;
一线技术专家撰写的实操技术案例;
InfoQ 出品的课程和技术活动报名通道;
“码”上关注,订阅每周新鲜资讯
点个在看少个 bug👇