OceaBase开发者大会落地上海!4月20日共同探索数据库前沿趋势!报名戳 了解详情
写点什么

配置管理的五项最佳实践

  • 2010-08-06
  • 本文字数:4857 字

    阅读完需:约 16 分钟

最近有很多关于应用程序的配置以及如何对其进行管理的讨论。 我在 ThoughtWorks 的一名同事 Tom Sulston 和我一起启动了名为 ESCAPE 的项目,它是从应用程序空间之外说明配置管理的一种方式。 它所做的是为多个处于各种环境中的应用程序以 REST 服务的方式提供配置选项。 尽管还没有真正应用 ESCAPE,但它并没有被放弃或者遗忘——只是我们(又一次)需要处理当前手头的工作。

现在我想带你一起看一下我们可以在代码中做些什么,使得他们以及所有需要管理和维护应用程序的人的工作变得更简单。 有些模式已经被我(和其他人)在 ThoughtWorks 的项目中应用过很多次,其价值已经得到了充分的验证。

单一配置源

在很多应用程序中,我都发现可以从所有的代码中以特定的实现方式来访问配置信息。 这不仅导致人们对于在哪里能够对应用程序特定的方面进行配置感到疑惑,并且这个疑惑经常会由于同样的配置参数名称(比方说database.host)根据它的位置而代表不同的意义而变的更加严重。 额外的副作用还包括:

  • 难于识别已用的或者不必要的配置选项
  • 不同部分的代码使用不同的机制来访问相同的配置源
  • 对相同的值使用不同的配置源

从运维的角度来看,这样的系统的最大副作用就在于,配置的源代码经常会拥有不同的格式,比方说,在一些文件中以 XML 形式存在,而在另一些文件中以 key/value 对的形式存在。 所有这些偶然的复杂性使得我们很难将应用程序部署到新环境中。

在这样的系统中,我们经常会发现对于这些配置代码没有进行测试,或者尽管存在测试,但它很脆弱且 / 或不切实际。

因此:

我们应该封装实际的将存储的配置信息放到提供程序中的机制,并将这个提供程序注入(inject)到需要值的地方。 这也让我们可以使用针对配置提供程序的特定实现的测试。 它让我们可以使用这样的方式:存储配置信息,使其可以很容易地根据系统的情况来改变。 例如,你可以在开始的时候使用硬编码的字符串,然后变为文件的形式,并最终变为某种容器中的值。

例如,看下这个简单的 Python 类,它是硬编码值的字典:

复制代码
class ConfigProvider(dict):
def __init__(self):
self['name'] = 'Chris'

class ConfigProvider(dict):

然后这个简单的类可以这样使用:

复制代码
from ConfigProvider import ConfigProvider
class ConfigProviderUser:
def __init__(self, cfg):
self.cfg = cfg
print "Hello, my name is %s" % self.cfg["name"]
if __name__ == "__main__":
ConfigProviderUser(ConfigProvider())

但是,之后我们决定必须停止使用硬编码,变为从.properties 读取配置信息。 这样,我们唯一需要改变的代码就是 ConfigProvider,最终它会像下面这样:

复制代码
class ConfigProvider(dict):
src = None
prop = re.compile(r"([\w. ]+)\s*=\s*(.*)")
def __init__(self, source = "config.properties"):
self.src = source
self.loadConfig()
def loadFileData(self):
data = ""
try:
input = open(self.src, 'r')
data = input.read()
input.close()
except IOError:
pass
return data
def loadConfig(self):
for (key, val) in self.prop.findall(self.loadFileData()):
self[key.strip()] = val

考虑一下,如果现在我们觉得.properties 不够好,想要切换到.yam 文件,那么需要做些什么呢? 再一次,你需要改变的代码就只有 ConfigProvider。 在这里它处于事务状态,在其中可以很好地处理这两种格式(基于文件的扩展名):

复制代码
class ConfigProvider(dict):
src = None
prop = re.compile(r"([\w. ]+)\s*=\s*(.*)")
def __init__(self, source = "config.properties"):
self.src = source
self.loadConfig()
def loadConfig(self):
if self.src.endswith(".properties"):
self.loadPropertiesConfig()
elif self.src.endswith(".yaml"):
self.loadYamlConfig()
def loadFileData(self):
data = ""
try:
input = open(self.src, 'r')
data = input.read()
input.close()
except IOError:
pass
return data
def loadPropertiesConfig(self):
for (key, val) in self.prop.findall(self.loadFileData()):
self[key.strip()] = val
def loadYamlConfig(self):
entries = yaml.load(self.loadFileData())
if entries:
self.update(entries)

单一配置规则集

太多的应用程序,甚至是很小很简单、不需要外部配置文件而将所有配置信息都包含在命令行中程序,都无法将配置规则充分地告诉用户。 这些规则可能包括(不仅限于):

  • 所有配置属性能够被设置为什么?
  • 需要哪个配置属性,哪些是可选的?
  • 是否可以检查提供给一个属性的值的有效性?
  • 是否有默认值,如果有的话,它们位于哪里?

通常这是因为这些规则只是代码行为的隐式副作用(implicit side effects)。 通常情况下,应用程序会正常启动并运行,但是如果用户试图执行某些功能,而该功能需要未设定或者无效的配置值,那么此时你就会得到不希望看到的结果。 这会让我们耗费大量时间来验证这样的应用程序部署是否成功,并且很容易产生错误。

因此:

定义单独的规则集,在其中定义所有上面所提到的问题。 然后我们可以使用这个单独的正确的源文件来生成配置模板,只要它适合你的应用程序。 这对于支持模式验证的格式(像 XML)会非常有效,但是仍然可以应用给像属性文件一样简单的系统,其中你只是生成形式化的示例文件。

单独的配置规则集可以被用作部署配置冒烟测试的一部分。 如果在应用程序初始化的时候缺少需要的配置元素,那么它就会立即崩溃,并且效果明显。 不要等到应用程序试图读取那些值的时候才发现问题。 如果我们知道更多关于如何检查所提供的值是否有效的信息(如果值是整型的,或者是已经存在的文件,或者是我们选择打开 socket 端口的主机名和端口,那么就易于测试),那么也要在此测试。

这些提供程序必须经过单元测试。 这些测试要针对所有模板执行,这些模板是被外部的配置应用程序的人员和 / 或系统所使用的。 部署配置冒烟测试应该在开发单元测试的时候就执行。 如果添加了新的配置选项,那么就应该添加针对该选项的单元测试。 当有人更新代码库的时候,如果他们没有在工作站上定义该值,那么测试就会失败,你就要大声地告诉他们“我需要你定义配置值sheep,但是没有!”

尽管单独的配置规则集必须大量使用单独的配置源文件,但是要记住它们的关注点是不同的。 我们需要注意避免二者之间的泄漏(leakage)。

在上个模式中我们开始使用的 Python 例子中执行,我们现在就会得到配置规则集,如下:

复制代码
class ConfigRuleset(dict):
defaults = {
'name': 'no name',
}
required = [
'name',
]
def __init__(self):
self.update(self.defaults)
def validate(self):
missing = []
for key in self.required:
if not self.has_key(key):
missing.append(key)
if len(missing):
raise KeyError("The following required config keys are missing: %s" % missing)

再一次,我们只需要改变配置提供程序。 它可能会像下面这样:

复制代码
class ConfigProvider(ConfigRuleset):
src = None
prop = re.compile(r"([\w. ]+)\s*=\s*(.*)")
def __init__(self, source = "config.properties"):
ConfigRuleset.__init__(self)
self.src = source
self.loadConfig()
self.validate()
….

然后,ConfigRuleset中的结构体defaultsrequired会成为我们代码中的正确值的唯一来源,它是针对我们的默认值以及所需要的那个键值的。

配置视图

当你试图诊断运行的应用程序中所发生的问题时,通常就需要查看当前运行的配置值是怎样的。 只查看当前的配置源文件无法得到精确的信息,因为应用程序早最后一次载入之后,配置信息可能会发生改变。

因此:

我们需要为所有人提供一种简单的并且是众所周知的方法,用来找出运行的系统从何处载入配置信息,以及载入的值是什么。 这在启动时可能和打印配置树(以及源文件的位置)一样简单,尽管这在长期运行的系统中很快就会产生变化。 更健壮的方法是要具备某种网页 / 关于页面 / 远程过程调用,它会返回当前运行时的配置信息,其中带有这些值是从何处载入的信息(特别是在有多个可能的配置源的情况下)。

对于这个视图来说,通常为系统提供版本 / 构建 / 发布的信息也是很有用的。 关于它的价值的更多信息,可以参见我之前在Self Identifying Software 发布的文章

在我们一直使用的Python 示例中,实现这一点只需要返回代表 ConfigProvider的字符串。

DNS 服务名称

当前人们普遍认为,当配置服务端点时,使用原始的 IP 地址是很不好的实践。 几乎全球(尽管很遗憾不是所有)都在使用 DNS 名称。 尽管如此,当这些 DNS 条目指向特定的服务器主机名的时候,人们还是有些问题需要处理。 这在初始部署应用程序的时候会很容易解决,但是如果你需要对其中一个服务进行硬件上的升级,那么会发生什么情况呢? 让我么考虑一下这些非常单纯的情况:

你拥有一个非常忙碌的站点,它使用中心数据库来存储客户信息。 这个数据库还被市场团队使用,为某些应用程序和其他的报表工具存储数据。 业务进展得很好,但是这台服务器(让我们称之为db02)现在有一些性能问题,并且需要用一台更好的,崭新的服务器(让我们称之为db04)来升级。 这会是一个漫长而痛苦的过程,因为你需要找出所有使用这台服务器的应用程序,并且明确当开始切换的时候,要如何重新对它们进行配置以使用新的服务器。

因此:

我们应该对所有的服务使用DNS 服务名称。 最简单的解决方案是为你的服务端点使用 DNS CNAME 记录。 在上述的示例中,我们会创建叫做clientdb的 CNAME 记录,它会指向db02,而所有使用该服务器的应用程序都会被配置为使用clientdb作为服务端点的主机名。 当我们需要将数据库迁移到新的服务器上时,在切换计划中的最终步骤只是更新 CNAME 值,使其指向db04。 这使得我们不仅不需要改变各个应用程序的配置,并且它还提供了非常便利的取消策略,如果由于某些原因,新的db04出现了问题,那么我们只需要将 CNAME 改回去,指向db02,直到问题解决。

基于 DNS 的环境设定

使用上述的DNS 服务名称会有少许副作用。 如果你拥有大量不同的针对开发和测试的客户端服务器,那么就会有很多 CNAME 值,它们看起来只有细微的差别。 比方说下面的情况:

  • 生产环境使用clientdb.example.com
  • 性能测试服务器使用clientdb-perf.example.com
  • 质量保证服务器使用clientdb-qa.example.com
  • 开发环境使用clientdb-dev.example.com

这种混乱会导致我们需要为每个环境都设置配置文件。

因此:

我们应该为你的服务器使用基于 DNS 的环境设定。 想要达到这个目的,首先你需要将顶级域切分成大量依赖于功能的子域,并且在每个子域中创建DNS 服务名称,指向与该服务相关的服务器。 基于上面的列表,我们可能会这样设置:

  • 生产环境使用clientdb.prod.example.com
  • 性能测试环境使用clientdb.perf.example.com
  • 质量保证环境使用clientdb.qa.example.com
  • 开发环境使用clientdb.dev.example.com

然后服务器会在子域之内解析,而这些子域是根据功能划分的。 也就是说,所有 QA 服务器首先会解析qa.example.com中的项目,如果检索失败,才会尝试在example.com中解析。 这让你可以针对客户端服务器主机名 (clientdb_)_ 拥有单独的配置值,它会在所有环境中正确地解析。 这种技术还会带来额外的好处,它仍然拥有在一般的顶级域中所定义的全局服务。

查看英文原文: 5 Configuration Management Best Practices


给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。

2010-08-06 00:008673
用户头像

发布了 340 篇内容, 共 126.0 次阅读, 收获喜欢 13 次。

关注

评论

发布
暂无评论
发现更多内容

又一大模型技术开源!有道自研RAG引擎QAnything正式开放下载

有道技术团队

阿里云云原生专场精彩内容集锦丨2023 云原生产业大会

阿里巴巴云原生

阿里云 云原生

华为发布2024数据中心能源十大趋势

财见

Peplink 成为首家授权 Starlink 技术服务商

财见

首个云原生、分布式、全栈国产化银行核心业务系统投产上线 XSKY 助力构建存储基础设施

XSKY星辰天合

操作系统寒武纪 :鸿蒙来了企业IT高兴吗?

FN0

鸿蒙 操作系统 HarmonyOS

移动应用崛起新契机—超级app+轻应用!

Geek_2305a8

《机器人流程自动化能力评估体系 第1部分:系统和工具》行标发布

王吉伟频道

RPA 机器人流程自动化 信通院 超自动化 行业标准

如何使用Postman导入Swagger API文档

Liam

后端 Postman 接口文档 API swagger

Pixels:重新定义游戏体验的区块链农场游戏

Footprint Analytics

区块链游戏 链游 Pixels

管理型人格特质揭秘:适合测试管理的人应具备何种素质

测吧(北京)科技有限公司

测试

测试管理者的晋升秘籍:稳步提升职业地位的关键步骤

测吧(北京)科技有限公司

测试

金融行业首个海量数据处理技术报告发布,“五化”技术助力金融数据潜能释放

腾讯云大数据

大数据

BAT程序员常用的开发工具,建议收藏!

快乐非自愿限量之名

腾讯 阿里 开发工具 BAT

Hologres V2.1版本发布,新增计算组实例构建高可用实时数仓

阿里云大数据AI技术

阿里云云原生助力安永创新驱动力实践探索

阿里巴巴云原生

阿里云 云原生

记一次go应用在k8s pod已用内存告警不准确分析

EquatorCoco

Kubernetes go语言

数据安全应急响应政策汇总:一份从无到有的应急预案实战指南(附下载)

极盾科技

构建智算时代的云原生应用平台,2023 云原生产业大会,阿里云在这里!

阿里巴巴云原生

阿里云 容器 云原生

优化-Spring Boot项目服务端接口超时设置

alexgaoyh

Spring Boot Callable接口 @Transactional 接口超时

祝贺!我的同事丁宇获“2023 年度云原生产业领军人物”荣誉称号

阿里巴巴云原生

阿里云 云原生

大白话—70个你必须知道的AI重要概念(二)

Geek_2305a8

大白话—70个你必须知道的AI重要概念(三)

Geek_2305a8

只需三步,开发文心一言应用帮你建立情感纽带!

飞桨PaddlePaddle

人工智能 深度学习 #飞桨# 飞桨AI

掌握测试管理的能力图谱:方法论、技术与资源管理全解析

测吧(北京)科技有限公司

测试

华为云医疗智能体eiHealth的高性能--AI助力医疗智慧化升级

华为云PaaS服务小智

人工智能 医疗 华为云

SHQ Response 重新定义网络风险可视化与协作规则

财见

JNPF低代码开发平台总体架构介绍

互联网工科生

软件开发 低代码 JNPF

测试管理的核心知识点:岗位与职责全面解析

测吧(北京)科技有限公司

测试

GreptimeDB v0.6 发布 | 支持 Datanodes 间迁移数据表 Region

Greptime 格睿科技

数据库 时序数据库 版本发布

面试官:分库分表后如何生成全局ID?

王磊

Java 面试

配置管理的五项最佳实践_技术管理_Chris Read_InfoQ精选文章