设计不可知的配置服务

designing an agnostic configuration service

为了好玩,我正在设计一些使用微服务架构的 Web 应用程序。我正在尝试确定进行配置管理的最佳方法,但我担心我的配置方法可能存在一些巨大的缺陷 and/or 存在更好的方法。

为了解决这个问题,假设我有一个用 c++ 编写的身份验证服务、一个用 rust 编写的身份服务、一个用 haskell 编写的分析服务、一些用 scala 编写的中间层和一个用javascript。还将有相应的身份数据库、auth 数据库、分析数据库(可能是会话的 redis 缓存)等...我正在使用 docker swarm 部署所有这些应用程序。

无论何时部署其中一个应用程序,它都必须发现所有其他应用程序。由于我使用 docker 群,只要所有节点共享必要的覆盖网络,发现就不是问题。

However, each application still needs the upstream services host_addr, maybe a port, the credentials for some DB or sealed service, etc...

我知道 docker 有 secrets 允许应用程序从容器中读取配置,但我需要为每个服务用每种语言编写一些配置解析器。这看起来很乱。

我宁愿做的是拥有一个 configuration service,它维护有关如何配置所有其他服务的知识。因此,每个应用程序都将以一些旨在在运行时获取应用程序配置的 RPC 调用开始。像

int main() {
    AppConfig cfg = configClient.getConfiguration("APP_NAME");
    // do application things... and pass around cfg
    return 0;
}

AppConfig 将在 IDL 中定义,因此 class 将立即可用并且与语言无关。

这似乎是一个很好的解决方案,但也许我真的忽略了这里的重点。即使在规模上,也可以通过一些配置服务轻松地为数万个节点提供服务,所以我预计不会出现任何扩展问题。同样,这只是一个业余爱好项目,但我喜欢思考 "what-if" 场景 :)

如何在微服务架构中处理配置方案?这看起来是一种合理的方法吗? Facebook、Google、LinkedIn、AWS 等主要参与者在做什么?

我没有很好的解决方案给你,但我可以指出一些问题供你考虑。

首先,您的应用程序可能需要一些 bootstrap 配置,使它们能够找到并连接到配置服务。例如,您提到使用 IDL 为支持远程过程调用的中间件系统定义配置服务 API。我假设你的意思是 CORBA IDL 之类的东西。这意味着您的 bootstrap 配置将不仅仅是要连接的端点(可能指定为字符串化 IOR 或 path/in/naming/service),而且还是您正在使用的 CORBA 产品的配置文件。您不能从配置服务下载该 CORBA 产品的配置文件,因为那将是先有鸡还是先有蛋的情况。因此,您最终不得不为每个应用程序实例手动维护 CORBA 产品配置文件的单独副本。

其次,您的伪代码示例表明您将使用单个 RPC 调用一次性检索 所有 应用程序的配置。这种粗粒度级别很好。相反,如果应用程序使用单独的 RPC 调用来检索每个 name=value 对,那么您可能会遇到严重的可伸缩性问题。为了说明这一点,我们假设一个应用程序在其配置中有 100 个 name=value 对,因此它需要进行 100 个 RPC 调用来检索其配置数据。我可以预见以下可扩展性问题:

  • 如果应用程序和配置服务器在同一局域网中,则每个 RPC 可能需要 1 毫秒的往返时间,因此您的应用程序的启动时间为 1 毫秒100 次 RPC 调用 = 100 毫秒 = 0.1 秒。这似乎可以接受。但是,如果您现在在另一个大陆上部署另一个应用程序实例,例如,往返延迟为 50 毫秒,那么该新应用程序实例的启动时间将为 100 个 RPC 调用,每次调用延迟为 50 毫秒 = 5 秒。哎哟!

  • 需要进行 100 次 RPC 调用以检索配置数据假设应用程序将检索每个 name=value 配对一次并将该信息缓存在对象的实例变量中,然后稍后通过该本地缓存访问 name=value 对。然而,迟早有人会从 for 循环中调用 x = cfg.lookup("variable-name"),这意味着应用程序将在每次循环时进行 RPC。显然,这会减慢该应用程序实例的速度,但如果您最终有几十个或数百个应用程序实例这样做,那么您的配置服务将被每秒成百上千个请求淹没,这将成为一个集中的性能瓶颈。

  • 您可能会开始编写在启动时执行 100 个 RPC 以检索配置数据的长寿命应用程序,然后 运行 数小时或数天后终止。我们假设这些应用程序是 CORBA 服务器,其他应用程序可以通过 RPC 与之通信。迟早您可能会决定编写一些命令行实用程序来执行以下操作: "ping" 一个应用程序实例以查看它是否 运行ning; "query" 一个获取一些状态细节的应用程序实例;要求应用程序实例优雅地终止;等等。这些命令行实用程序中的每一个都是短暂的;当他们启动时,他们使用 RPC 来获取他们的配置数据,然后通过向服务器进程发送单个 RPC 来 ping/query/kill 来完成 "real" 工作,然后他们终止。现在有人会编写一个 UNIX shell 脚本,为您的数十个或数百个应用程序实例中的每一个每秒调用一次这些 ping 和查询命令。这个看似无害的 shell 脚本将负责每秒创建数十或数百个短暂的进程,并且这些短暂的进程将对集中配置服务器进行大量 RPC 调用以检索 name =value 一次一对。这种 shell 脚本会给您的集中式配置服务器带来巨大的负载。

不是试图阻止您设计集中式配置服务器。以上几点只是警告您需要考虑的可伸缩性问题。您的应用程序通过一次粗粒度 RPC 调用检索 所有 其配置数据的计划肯定会帮助您避免我上面提到的各种可伸缩性问题。

为了提供一些思路,您可能需要考虑一种不同的方法。您可以将每个应用程序的配置文件存储在网络服务器上。应用程序的 shell 启动脚本 "wrapper" 可以执行以下操作:

  • 使用wgetcurl从网络服务器下载"template"配置文件并将文件存储在本地文件系统中。 "template" 配置文件是一个普通的配置文件,但有一些值的占位符。占位符可能类似于 ${host_name}.

  • 还可以使用 wgetcurl 下载包含搜索和替换对的文件,例如 ${host_name}=host42.pizza.com.

  • 对所有下载的模板配置文件执行全局搜索和替换这些搜索和替换项,以生成可以使用的配置文件。您可以使用 UNIX shell 工具,如 sed 或脚本语言来执行此全局搜索和替换。或者,您可以使用模板引擎,例如 Apache Velocity.

  • 执行实际的应用程序,使用命令行参数指定path/to/downloaded/config/files.

我不会构建自定义配置管理解决方案,而是使用以下现有解决方案之一:

Spring 云配置

Spring Cloud Config 是一个用 Java 编写的配置服务器,提供 HTTP API 来检索应用程序的配置参数。显然,它附带了一个 Java 客户端和一个很好的 Spring 集成,但由于服务器只是一个 HTTP API,您可以将它与您喜欢的任何语言一起使用。配置服务器还具有配置值的对称/非对称加密功能。

配置源:外部化配置存储在 GIT 存储库中,Spring 云配置服务器必须可以访问该存储库。然后可以通过 HTTP API 访问该存储库中的属性,因此您甚至可以考虑为配置属性实施更新过程。

服务器位置:理想情况下,您可以通过域访问您的配置服务器(例如config.myapp.io),这样您就可以实现负载平衡和故障转移方案如所须。此外,您需要为所有服务提供的只是那个确切的位置(以及一些身份验证/解密信息)。

入门:你可以看看这个getting started guide for centralized configuration on the Spring docs or read through this Quick Intro to Spring Cloud Config

Netflix Archaius

Netflix Archaius 是 Netflix OSS 堆栈的一部分并且 "is a Java library that provides APIs to access and utilize properties that can change dynamically at runtime"。 虽然仅限于 Java(与您询问的上下文不完全匹配),但该库能够使用数据库作为配置属性的来源。

confd

confd 使用存储在外部源(etcd、consul、dynamodb、redis、vault 等)中的数据使本地配置文件保持最新。配置更改后,confd 重新启动应用程序,以便它可以获取更新的配置文件。

在您问题的上下文中,这可能值得一试,因为 confd 不对应用程序做出任何假设,也不需要特殊的客户端代码。大多数语言和框架都支持基于文件的配置,因此 confd 应该很容易添加到当前使用 env 变量并且没有预料到分散配置管理的现有微服务之上。