写点什么

配置管理的五项最佳实践

  • 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:008966
用户头像

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

关注

评论

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

在加密货币交易所开发中使用人工智能和机器学习

区块链软件开发推广运营

交易所开发 数字藏品开发 dapp开发 区块链开发 NFT开发

什么是隧道代理、http代理,隧道代理较于http代理有什么优势?

巨量HTTP

代理IP http代理

快手发布文生图大模型“可图”,探索AI新玩法

Geek老T

短视频 AIGC

文心一言 VS 讯飞星火 VS chatgpt (96)-- 算法导论9.3 1题

福大大架构师每日一题

福大大架构师每日一题

数字化转型与架构-架构设计篇|什么是架构风格和架构模式?

数字随行

数字化转型

市面上支持信创的堡垒机哪家好?为什么?

行云管家

网络安全 信创 数据安全 堡垒机

FIL NEW算力挖矿系统开发

l8l259l3365

软件测试/测试开发丨利用人工智能ChatGPT批量生成测试数据

测试人

人工智能 程序员 软件测试 ChatGPT 测试数据

选择渲染农场的几个标准

Finovy Cloud

游戏制作 影视制作 渲染 云渲染 渲染农场

修旧利废,提升净资产收益率

用友BIP

资产管理

fastposter 新版本 v2.17.0 强势发布!让海报开发更简单

物有本末

图片处理 海报生成器 海报生成 海报小程序

Web3.0时代的全新合作模式:DAO | 京东云技术团队

京东科技开发者

区块链 DAO Web3.0 企业号9月PK榜

百度集团副总裁吴甜:大语言模型面临三大技术挑战

飞桨PaddlePaddle

文心一言 文心大模型

主动写入流对@ResponseBody注解的影响 | 京东云技术团队

京东科技开发者

spring 注解 企业号9月PK榜 @ResponseBody

软通咨询杨念农:咨询2.0是企业数字化转型的大脑

软通咨询

数字化转型 #人工智能 管理咨询 数字化转型咨询

集成学习方法——随机森林

小魏写代码

国庆机酒预订又快又便宜?内附华为Mate60负一屏抢购攻略

最新动态

行云管家支持信创吗?是真的吗?

行云管家

信创 国产化 行云管家

Rocketmq并发和顺序消费的失败重试机制

石臻臻的杂货铺

RocketMQ

中国机械总院张红新:强化集团级数据治理 业财融合助力企业降本增效

用友BIP

2023全球商业创新大会

探索以太坊 Layer 2 解决方案的后起之秀——Starknet

Footprint Analytics

区块链 以太坊 Layer 2

中国广核集团智慧司库系统票据业务成功上线!打造票据数智管理新标杆!

用友BIP

全球司库

教你用API插件开发一个AI快速处理图片小助手

华为云开发者联盟

人工智能 华为云 华为云开发者联盟 企业号9月PK榜

慢SQL治理实践及落地成果分享 | 京东物流技术团队

京东科技开发者

数据库 sql 慢SQL 企业号9月PK榜

Microsoft word 2019 for Mac v16.78 beta中文激活版

mac

windows 办公软件 苹果mac Word 2019 文字处理软件

聚焦企业开放OpenAPI痛难点,华为云API Explorer助力构建API门户

华为云开发者联盟

软件开发 华为云 华为云开发者联盟 企业号9月PK榜

如何提高技术领导力?与你分享 5 个心得

LigaAI

程序人生 技术管理 成长与思考 技术领导力 企业号9月PK榜

JDK8升级JDK11最全实践干货来了 | 京东云技术团队

京东科技开发者

Java jdk8 JDK11 企业号9月PK榜

对线面试官 - Java基础面试题【一】

派大星

Java 面试题

专业级PDF编辑和管理 Acrobat Pro DC 2023 for Mac

胖墩儿不胖y

Mac软件 pdf编辑器 编辑pdf pdf工具

万能音视频转换器 Permute 3 for mac免激活中文版

mac大玩家j

Mac软件 音频格式转换器 音频转换

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