亚马逊AWS官方博客

搭建云原生配置中心的技术选型和落地实践

引言

在从单体应用向微服务架构转型的过程中,服务配置管理从只需要应对一个单体服务,变为应对大量分布式服务,难度呈几何级增加。为了解决这个难题,各种应对分布式服务的配置中心应运而生,如何搭建一个高效合理的配置中心已经成为每个大型分布式系统必经的考验。而在服务“上云”的大趋势下,如何让配置中心在云平台顺利落地,更进一步,如何借助云计算的优势让配置中心如虎添翼,目前业内对这一块还处于探索阶段。本文将介绍FreeWheel核心业务系统在AWS云平台上搭建配置中心的实战,作为搭建云原生配置中心的参考,希望能给大家带来启发。

 

为什么需要配置中心

分布式系统兴起之后,配置管理变得尤为复杂。1984年,业界就有两位学者Kramer Jeff和Magee Jeff在IEEE发表了一篇文章《分布式系统的动态配置》,其中阐述到:

 动态系统配置是在系统运行时对其进行修改和扩展的能力。在大型分布式系统中,这是一个必不可缺的功能,因为如果需要停止整个系统来对其部分硬件或软件进行修改,在生产环境是难以接受的,或者会产生较大经济损失。另外,动态配置也有助于系统在生产环境上组件的增量集成,以达成系统演进的目的。

这篇文章指出了动态配置对于分布式系统的重要性,即在系统运行时,如何经济安全地对系统进行调整。现在分布式系统已经发展到了从前难以想象的复杂程度,除了动态配置,配置管理还面临更多挑战,例如:

  • 如何统一管理数量众多的服务配置
  • 如何在异地众多机器节点上部署配置
  • 如何实现灰度
  • 如何确定配置是否生效
  • 如何对配置进行灾备

为了解决这些挑战,配置中心应运而生。配置中心就是分布式服务中统一管理服务配置的系统,它具备高效和动态控制服务的能力。配置中心一般具备服务注册、服务配置管理、部署服务配置等基本功能。下面是一个微服务架构下配置中心的示意图:

 

配置中心一般会包含服务端、客户端和界面这三个组件:每个微服务启动时可以通过客户端进行服务注册;用户可以通过界面创建、修改和部署配置;动态配置功能可以通过服务端实时推送、客户端定期拉取或者两者推拉结合来实现。

 

FreeWheel云原生配置中心实战

痛点

  • 服务配置的数量大幅增加

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那么全面:

功能 AWS 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平台主页:

配置中心主页:

配置中心历史记录页面:

 

落地常见问题

在搭建配置中心的实战过程中,我们踩了不少的坑,也总结了一些经验。在这里分享给大家,希望能对大家有所帮助。

  • 如何合理使用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来避免凭证过期。

配置中心未来展望

配置驱动资源正在成为云计算的一个重要技术趋势,即认为不光是应用进程,与云计算相关的所有资源都可以通过配置去驱动。这将令配置中心的云端之路充满变化和挑战。未来我们也会继续思考配置中心的其他应用模式,比如如何在云服务平台上与其他服务整合,如何去独立支撑某些业务场景等等。

 

本篇作者

孙自然

Lead Software Engineer,FreeWheel。毕业于北京大学计算机系,目前就职于Comcast FreeWheel核心业务团队。研究方向为微服务架构、云计算、产品设计等领域,热衷于新技术的探索与分享。