动态配置能够在不重新启动应用程序的情况下更改正在运行的系统的行为和功能。理想的动态配置系统使服务开发人员和管理员能够方便地查看和更新配置,并高效可靠地向应用程序提供配置更新。它使组织能够快速、大胆地迭代新特性,并提供工具,减少与更改现有系统相关的风险。
在Twitter的早期,应用程序管理并分发自己的配置,通常存储在ZooKeeper中。但是,我们以前使用ZooKeeper的经验表明,它在用作通用键值存储时不能伸缩。其他团队转而使用Git进行存储,并结合自定义的工具来更新、分发和重新加载配置。随着Twitter的发展,很明显需要一个标准的解决方案来提供可伸缩的基础设施、可重用的库和有效的监控。
本文将介绍Twitter的动态配置系统ConfigBus。ConfigBus包括存储配置的数据库、将配置分发到Twitter数据中心中的机器的管道、读取和更新配置的API和工具。
架构
在一个较高的层次上,你可以将ConfigBus看作一个Git存储库,它的内容被推送到Twitter数据中心的所有机器上。配置更改经过一系列步骤到达目的地:
- 开发人员向Git存储库提交更改。经过身份验证的应用程序也可以通过ConfigBus服务提交到存储库。
- 预接收钩子验证更改。如果验证通过,则在服务器上接受并提交推送。(我们将在“特点”部分更详细地讨论这种验证。)
- 一旦服务器上的提交过程完成,Git接收后钩子就会使用提交信息(SHA、时间戳等)更新ZooKeeper中的特定节点。
- 接下来会触发ZooKeeper watches,引发下游过程的动作:
a. 在作为配置准备区的“ConfigStore”机器上,watch事件会调用一个回调,从Git服务器获取最新提交。它还用当前的SHA更新ZooKeeper中的条目。
b.在目标机器上,watch事件触发镜像任务,该任务轮询ZooKeeper,找出与最新提交具有相同SHA的ConfigStore机器。一旦找到源机器,镜像任务将运行rsync把更改同步到本地机器上。 - 最后,使用这些配置文件的应用程序将看到文件系统上的更改(通过ConfigBus客户端库),并启动进程内重新加载。
最后,系统停止工作,以便把第1步中更改的文件同步到所有目标机器,并由依赖这些机器的所有客户端应用程序重新加载。
特点
配置即代码
使用Git允许开发人员重用源存储库提供的许多相同的命令和工作流。Git及其周边的生态系统主要提供了以下功能:
- 具有更改日志的源代码控制:能够检查过去的配置更改以查看更改了什么(以及由谁更改、何时更改或为什么更改)是非常有价值的。Git自然允许这样做。开发人员完全有信心,当前和过去的版本在版本控制中是安全的。
- 自动部署:ConfigBus中的配置文件会自动复制到所有目标机器。目前,配置传播的平均延迟为80-100秒,而p99延迟大约为300秒。
- 分析验证:ConfigBus使用预接收钩子来运行验证程序,它们可以检查配置文件中的语法错误,进行模式验证,以及执行任何类型的自定义验证。它针对JSON等流行配置格式为Twitter开发人员提供了开箱即用的语法验证。它还提供了指定模式和验证兼容性的能力。用户还可以编写自定义验证,并在Git中添加和更新配置时执行这些验证。
- 代码审核:让配置更改通过代码评审有助于减少错误,并在将其投入生产环境之前发现问题。
- ACL:我们在Git存储库中强制执行配置所有权,以确保配置文件只被应该管理它们的团队和应用程序修改。当推送一个更改时,一个预接收钩子会验证是否允许执行该推送的用户对这些文件进行更改。
- 编程访问:ConfigBus服务支持对ConfigBus的编程访问。该服务实现了Git“智能”HTTP协议,并充当配置存储库的前端。它通过HTTP和Thrift API提供读取和“compare-and-set”写入功能。这使得编写多用户应用程序向单个文件推送更改变得简单了,不需要它们具有存储库的本地克隆。该服务还内置了乐观并发控制,当推送由于对存储库的并发更新而失败时会自动重试。
大规模持续交付
一旦配置被安全存储,我们需要一种方法把它们提供给运行在Twitter基础设施上的软件,包括运行在Mesos云以及直接运行在裸机上的服务。这是通过rsync将文件推送到所有的机器上来实现的。需要访问配置的应用程序只需从本地文件系统读取。这样做的好处是:
- 简单:使用文件系统作为API使得任何语言编写的应用程序都可以使用ConfigBus。可以在本地文件系统读取配置还有助于减少服务启动时间,特别是在云环境中,应用程序实例可以在一个节点上宕掉,然后在另一个节点上运行。
- 可容错:将配置数据推送到每台机器上的本地文件系统中,这样,即使ConfigBus管道的某些部分失败,应用程序也可以继续运行。例如,如果Git服务器宕机,团队将无法进行新的配置更改,但是运行中的应用程序不会受到太大影响。同样,如果ZooKeeper出现故障,分发管道也会受到影响,但机器上现有的配置仍然可用。相反,如果配置获取服务宕掉,需要按需获取配置数据的系统将会失败。
- 可扩展:ConfigBus的多层架构允许系统根据需求的增加进行伸缩。ConfigStore层将Git服务器与直接流量隔离开来。在这一层增加容量以适应Twitter不断增长的机器数量所带来的需求增加,这在操作上是非常简单的。
“热”重载
动态配置系统的主要优点之一是能够独立于使用它的软件部署重新加载配置更改。此外,一个完全动态的配置系统应该能够在不重新启动应用程序进程的情况下重新加载更改,从而最小化对整个应用程序的影响。ConfigBus提供了一些库,允许客户端订阅特定的文件,并在这些文件更改时调用回调。虽然应用程序也可以直接从文件系统读取,但是使用经过良好测试和封装的客户端库具有以下优点:
- 避免检测配置更改并触发重新加载的代码重复;
- 允许嵌入发布配置新鲜度指标用于问题检测的代码。
监控
ConfigBus是一个复杂的分布式系统,有许多活动部件。我们在各个层级对系统进行监控,以便收集统计信息,并在发现异常行为时发出警报。
- 独立部件:我们监控各个子系统(如Git和ZooKeeper服务器)的健康状况。例如,我们收集与Git存储库的功能和性能相关的统计信息(包大小、提交延迟、钩子延迟等等),并发出警报。
- 版本跟踪:Git服务器将期望的配置版本发布到ZooKeeper。下游消费者使用此版本监控配置数据的新鲜度。
- 端到端监控:运行在客户端机器上的监控应用程序每隔几分钟就会更新配置存储库中的特定文件,并等待更新传播到其本地机器。这有助于度量ConfigBus管道的关键特征,例如提交成功率、提交延迟和配置同步延迟。
用例
流量路由:在Twitter,ConfigBus用于存储服务路由参数。这可以用于控制请求路由逻辑(例如,如果开发人员希望将1%的服务请求路由到运行软件自定义版本的一组实例)。
元服务发现:在Twitter,服务是通过一个服务发现服务发现彼此。但是,它们必须首先发现服务发现服务本身。这是通过ConfigBus实现的。使用ConfigBus与类似于ZooKeeper这样的东西,其优点是,在每台机器的本地文件系统上都有可用的信息,这使得系统更能抵御故障(也就是说,如果ConfigBus或ZooKeeper出现故障,服务发现仍然可以运行)。
Decider:在Twitter,Decider是服务使用的特性标识系统,用于在运行时动态启用和禁用单个特性。该系统位于ConfigBus之上。Decider是面向键-值的(“cool_new_feature的值是多少?”),而ConfigBus是面向文件的(“文件application/config.json的内容是什么?”)。单个特性的标识称为“决策器(decider)”。一旦嵌入到代码中,决策器就可以更改正在运行的应用程序的行为,而不需要更改代码或重新部署。除此之外,决策器可以用来:
- 有选择性地启用代码:可以在代码中使用决策键来选择性地启用代码块。Decider提供的主要方法是“isAvailable”,它接受一个决策键参数,如果针对当前的调用打开特性,则返回“true”;如果针对当前的调用关闭特性,则返回“false” 。“isAvailable”方法使得开发人员可以像下面这样切换代码路径:
- 切换后台存储系统:有些应用程序使用决策器选择写入或读取的后台存储系统。例如,应用程序从旧数据库迁移到新数据库,它可能会临时将数据写入两个系统,并在迁移完成后动态关闭旧数据库。或者,如果新系统有问题,应用程序可能会选择关闭它。决策器使得这些更改安全而快速。
- 作为止血带断开过载系统:在Twitter,监控系统有时会在检测到负载过重时更新决策器,以便禁用某些代码路径。这可以防止系统过载,并允许它们进行恢复。
- 区域间故障转移:Decider用于存储某些路由参数,这些参数控制着Twitter服务跨区域的流量分布。其中许多是通过监控软件自动更新的,后者可以观察每个区域的故障率。
“特性切换(Feature Switches)”:在Twitter,特性切换为控制应用程序的行为提供了一个复杂而强大的基于规则的系统。特性切换控制特性在初步开发、团队测试、内部测试、Alpha测试、Beta测试、发布以及最终淘汰的过程中的可用性。与Decider的配置一样,特性切换配置存储在ConfigBus中。不过,在移动设备上,最终配置会有一个关键区别。在发布的最后一个阶段,移动应用程序会通过运行在Twitter数据中心的服务定期拉取这些配置更新。与Decider相比,特性切换还提供更细粒度的控制。典型的Decider配置很简单,例如,“允许数据中心X中70%的请求写入新数据库”。特性切换配置更高级,也更复杂,例如,“为X团队中的所有人以及这个平台上的这些特定用户启用这个新特性。”
“库切换(Library toggles)”:特性切换和Decider是为了帮助应用程序开发人员安全发布特性而设计的。库开发人员在推出更改时有时需要类似的开关机制。Finagle是Twitter面向JVM的开源RPC框架,它提供了一种切换机制,库开发人员可以使用该机制安全地发布更改,同时也为服务所有者提供了某种程度的控制。Twitter在内部实现这个API时使用ConfigBus来动态控制这些切换。
执行A/B测试:要有效地运行产品试验需要快速迭代和易于调整的功能。Twitter的试验框架使用了ConfigBus,允许应用程序开发人员轻松地设置和扩展试验,并在需要时快速关闭它们。
一般应用程序配置:ConfigBus最典型的用法是存储一般应用程序配置文件,并在更改提交时动态地重新加载它们。
经验教训
我们在生产环境中运行ConfigBus已经将近四年了。以下是我们学到的一些东西。
近实时分发
尽管ConfigBus的目标是近实时分发,但是,签入存储库的糟糕配置更改会迅速传播到任何地方。为了最小化这种更改的影响,ConfigBus最近添加了一个可选特性,提供了“分阶段(staged )”滚动功能,从而可以增量地滚动推出更改。这是通过同时推送新旧版本的配置以及一些关于推出阶段的元数据来实现的。然后,各个应用程序实例使用阶段元数据来动态地加载合适版本的配置。
Git存储库规模
随着Git存储库年龄的增长,它的规模也在增大。较大的存储库会减慢诸如“git clone”和“git add”之类的操作。存储库大小不仅受检入的大文件影响,而且还受到重大更改的影响。下面是我们用来解决这个问题的一些策略。
- “浅推送(Shallow pushes)”:我们升级到一个比较新的Git版本,该版本允许开发人员从存储库的浅克隆版本推送更改。这意味着最初的克隆操作要快得多,因为它只传输HEAD提交以及提交和推送更改所需的最小元数据。
- 归档:在极少数情况下,我们会对Git存储库进行归档,将所有历史记录移动到一个归档中,然后重新开始。这使得我们可以删除旧的、潜在的大文件,减少存储库的大小。我们会避免经常这样做,因为这会迫使开发人员重新克隆存储库。
- 延长delta链:我们目前正在研究,是否要积极地对Git对象进行重新打包以延长delta链,从而帮助减少存储库的大小,同时又保持更改提交性能不变。
分区
我们不允许Git存储库上的“非快进(non-fast-forward)”推送,以保护主分支中的提交不会被强制推送覆盖。这项设置的效果是要求对存储库的任何推送都必须使用存储库的最新副本。如果两个提交者在将数据推送到存储库时产生竞争,那么其中一个将胜出,另一个将不得不拉取最新的更改并重试。这会增加配置更新操作的延迟。对于频繁提交者来说,延迟的增加带来了一个巨大的问题。我们是通过在后台将频繁更新的名称空间划分到单独的专用存储库来解决这个问题。对于使用API进行配置更新的客户端来说,这没有任何差异。
文件级线性化
实际上,不允许非快进推送意味着ConfigBus在存储库级上是线性化的。如果两个开发人员因同时推送更改而发生竞争,则其中一个将“获胜”,另一个必须拉取最新的更改并重试。即使这两个开发人员正在更新完全不同的文件,情况也是如此。对于不断更新的存储库,这会给客户端带来不必要的负担。因此,我们把ConfigBus服务设计成自动拉取更新,并在失败时重试推送。这提供了表面上的文件级线性化,确保客户端只在文件级更新冲突时才看到失败。
git fetch
与git pull
“git pull”实际上是“git fetch”+“git merge”。如果ConfigStore机器上的克隆站点损坏或与远程服务器不同步,合并步骤可能会失败。以自动方式从服务器获取更新最安全、最简洁的方法是运行“git fetch”+“git reset——hard FETCH_HEAD”,以便覆盖克隆站点上存在的任何本地状态。
rsync导致的缓慢
我们选择让少量的ConfigStore机器从Git上获取信息,并将它们作为其他机器通过rsync进行同步的源。我们使用-c选项运行rsync,迫使它忽略时间戳,并为大小相同的文件计算校验和。这是CPU密集型的,因此限制了每台ConfigStore机器可以提供的并发rsync操作的数量。反过来,这又增加了整个端到端传播的延迟。将名称空间分区成单独的存储库可以减少rsync在每次提交时需要比较的文件数量。另一种可能的选择是在每台ConfigStore机器上运行一个Git服务器,并让所有的目标机器运行“Git fetch”,这只需要下载最新的“HEAD”,而不需要任何比较开销(因为Git服务器确切地知道发生了什么变化)。
非原子同步
ConfigBus使用rsync,这意味着文件可以单个地同步到目标机器上。因此,如果提交碰巧更改了多个文件,那么目标机器上的文件系统可能会暂时包含新旧文件的混合。一个可能的解决方案是同步到一个临时位置,然后使用原子重命名操作来完成更改。但是,由于需要以向后兼容的方式支持分区名称空间,部署位置上存在符号链接,这使得问题变得复杂。一个更可行的解决方案是继续像现在这样分发主Git存储库,但是把将来的分区存储库切换到原子部署。
未来展望
在Twitter,我们构建ConfigBus是为了提供一个健壮的动态配置平台。随着现有用例的发展和新用例的出现,ConfigBus必须进行更改以适应它们。以下是我们特别关注的几个领域。
Git
对于终端用户而言,Git有很多优点,但是,它代表了一个持续的操作挑战。我们对它今后是否仍然是正确的解决办法持开放态度。替代方法包括键-值存储,如Consul,但我们必须解决历史太少这个相反的问题。
重新设计分发
使用rsync从一个很小的ConfigStore机器池进行分发限制了分发管道的速度。探索点对点分发模型将是一件很有趣的事情,在这种模型中,每台机器在拥有部分或全部数据后都充当进一步传输的源。
支持大对象
目前,我们不鼓励使用ConfigBus处理大型Blob,这主要是因为Git,也因为在每台机器上存储大型Blob的效率很低。一个潜在的解决方案是将Blob存储在一个常规的Blob存储中,并将活动版本存储在ConfigBus中,然后根据需要下载它们。