配置管理的五项最佳实践
最近有很多关于应用程序的配置以及如何对其进行管理的讨论。我在ThoughtWorks的一名同事TomSulston和我一起启动了名为ESCAPE的项目,它是从应用程序空间之外说明配置管理的一种方式。它所做的是为多个处于各种环境中的应用程序以REST服务的方式提供配置选项。尽管还没有真正应用ESCAPE,但它并没有被放弃或者遗忘——只是我们(又一次)需要处理当前手头的工作。
现在我想带你一起看一下我们可以在代码中做些什么,使得他们以及所有需要管理和维护应用程序的人的工作变得更简单。有些模式已经被我(和其他人)在ThoughtWorks的项目中应用过很多次,其价值已经得到了充分的验证。
单一配置源
在很多应用程序中,我都发现可以从所有的代码中以特定的实现方式来访问配置信息。这不仅导致人们对于在哪里能够对应用程序特定的方面进行配置感到疑惑,并且这个疑惑经常会由于同样的配置参数名称(比方说database.host)根据它的位置而代表不同的意义而变的更加严重。额外的副作用还包括:
难于识别已用的或者不必要的配置选项
不同部分的代码使用不同的机制来访问相同的配置源
对相同的值使用不同的配置源
相关厂商内容
HelloApollo!无人驾驶公开课
阿里巴巴故障治理领域的智能运维实践
腾讯海量业务中挖掘AI运维的价值实践
微博应对突发热点事件的弹性调度实践
获取面向人工智能、机器学习和深度学习的最新工具、框架
相关赞助商
从运维的角度来看,这样的系统的最大副作用就在于,配置的源代码经常会拥有不同的格式,比方说,在一些文件中以XML形式存在,而在另一些文件中以key/value对的形式存在。所有这些偶然的复杂性使得我们很难将应用程序部署到新环境中。
在这样的系统中,我们经常会发现对于这些配置代码没有进行测试,或者尽管存在测试,但它很脆弱且/或不切实际。
因此:
我们应该封装实际的将存储的配置信息放到提供程序中的机制,并将这个提供程序注入(inject)到需要值的地方。这也让我们可以使用针对配置提供程序的特定实现的测试。它让我们可以使用这样的方式:存储配置信息,使其可以很容易地根据系统的情况来改变。例如,你可以在开始的时候使用硬编码的字符串,然后变为文件的形式,并最终变为某种容器中的值。
例如,看下这个简单的Python类,它是硬编码值的字典:
classConfigProvider(dict):
def__init__(self):
self['name']='Chris'
classConfigProvider(dict):
然后这个简单的类可以这样使用:
fromConfigProviderimportConfigProvider
classConfigProviderUser:
def__init__(self,cfg):
self.cfg=cfg
print"Hello,mynameis%s"%self.cfg["name"]
if__name__=="__main__":
ConfigProviderUser(ConfigProvider())
但是,之后我们决定必须停止使用硬编码,变为从.properties读取配置信息。这样,我们唯一需要改变的代码就是ConfigProvider,最终它会像下面这样:
classConfigProvider(dict):
src=None
prop=re.compile(r"([\w.]+)\s*=\s*(.*)")
def__init__(self,source="config.properties"):
self.src=source
self.loadConfig()
defloadFileData(self):
data=""
try:
input=open(self.src,'r')
data=input.read()
input.close()
exceptIOError:
pass
returndata
defloadConfig(self):
for(key,val)inself.prop.findall(self.loadFileData()):
self[key.strip()]=val
考虑一下,如果现在我们觉得.properties不够好,想要切换到.yam文件,那么需要做些什么呢?再一次,你需要改变的代码就只有ConfigProvider。在这里它处于事务状态,在其中可以很好地处理这两种格式(基于文件的扩展名):
classConfigProvider(dict):
src=None
prop=re.compile(r"([\w.]+)\s*=\s*(.*)")
def__init__(self,source="config.properties"):
self.src=source
self.loadConfig()
defloadConfig(self):
ifself.src.endswith(".properties"):
self.loadPropertiesConfig()
elifself.src.endswith(".yaml"):
self.loadYamlConfig()
defloadFileData(self):
data=""
try:
input=open(self.src,'r')
data=input.read()
input.close()
exceptIOError:
pass
returndata
defloadPropertiesConfig(self):
for(key,val)inself.prop.findall(self.loadFileData()):
self[key.strip()]=val
defloadYamlConfig(self):
entries=yaml.load(self.loadFileData())
ifentries:
self.update(entries)
单一配置规则集
太多的应用程序,甚至是很小很简单、不需要外部配置文件而将所有配置信息都包含在命令行中程序,都无法将配置规则充分地告诉用户。这些规则可能包括(不仅限于):
所有配置属性能够被设置为什么?
需要哪个配置属性,哪些是可选的?
是否可以检查提供给一个属性的值的有效性?
是否有默认值,如果有的话,它们位于哪里?
通常这是因为这些规则只是代码行为的隐式副作用(implicitsideeffects)。通常情况下,应用程序会正常启动并运行,但是如果用户试图执行某些功能,而该功能需要未设定或者无效的配置值,那么此时你就会得到不希望看到的结果。这会让我们耗费大量时间来验证这样的应用程序部署是否成功,并且很容易产生错误。
因此:
定义单独的规则集,在其中定义所有上面所提到的问题。然后我们可以使用这个单独的正确的源文件来生成配置模板,只要它适合你的应用程序。这对于支持模式验证的格式(像XML)会非常有效,但是仍然可以应用给像属性文件一样简单的系统,其中你只是生成形式化的示例文件。
单独的配置规则集可以被用作部署配置冒烟测试的一部分。如果在应用程序初始化的时候缺少需要的配置元素,那么它就会立即崩溃,并且效果明显。不要等到应用程序试图读取那些值的时候才发现问题。如果我们知道更多关于如何检查所提供的值是否有效的信息(如果值是整型的,或者是已经存在的文件,或者是我们选择打开socket端口的主机名和端口,那么就易于测试),那么也要在此测试。
这些提供程序必须经过单元测试。这些测试要针对所有模板执行,这些模板是被外部的配置应用程序的人员和/或系统所使用的。部署配置冒烟测试应该在开发单元测试的时候就执行。如果添加了新的配置选项,那么就应该添加针对该选项的单元测试。当有人更新代码库的时候,如果他们没有在工作站上定义该值,那么测试就会失败,你就要大声地告诉他们“我需要你定义配置值sheep,但是没有!”
尽管单独的配置规则集必须大量使用单独的配置源文件,但是要记住它们的关注点是不同的。我们需要注意避免二者之间的泄漏(leakage)。
在上个模式中我们开始使用的Python例子中执行,我们现在就会得到配置规则集,如下:
classConfigRuleset(dict):
defaults={
'name':'noname',
}
required=[
'name',
]
def__init__(self):
self.update(self.defaults)
defvalidate(self):
missing=[]
forkeyinself.required:
ifnotself.has_key(key):
missing.append(key)
iflen(missing):
raiseKeyError("Thefollowingrequiredconfigkeysaremissing:%s"%missing)
再一次,我们只需要改变配置提供程序。它可能会像下面这样:
classConfigProvider(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中的结构体defaults和required会成为我们代码中的正确值的唯一来源,它是针对我们的默认值以及所需要的那个键值的。
配置视图
当你试图诊断运行的应用程序中所发生的问题时,通常就需要查看当前运行的配置值是怎样的。只查看当前的配置源文件无法得到精确的信息,因为应用程序早最后一次载入之后,配置信息可能会发生改变。
因此:
我们需要为所有人提供一种简单的并且是众所周知的方法,用来找出运行的系统从何处载入配置信息,以及载入的值是什么。这在启动时可能和打印配置树(以及源文件的位置)一样简单,尽管这在长期运行的系统中很快就会产生变化。更健壮的方法是要具备某种网页/关于页面/远程过程调用,它会返回当前运行时的配置信息,其中带有这些值是从何处载入的信息(特别是在有多个可能的配置源的情况下)。
对于这个视图来说,通常为系统提供版本/构建/发布的信息也是很有用的。关于它的价值的更多信息,可以参见我之前在SelfIdentifyingSoftware发布的文章。
在我们一直使用的Python示例中,实现这一点只需要返回代表ConfigProvider的字符串。
DNS服务名称
当前人们普遍认为,当配置服务端点时,使用原始的IP地址是很不好的实践。几乎全球(尽管很遗憾不是所有)都在使用DNS名称。尽管如此,当这些DNS条目指向特定的服务器主机名的时候,人们还是有些问题需要处理。这在初始部署应用程序的时候会很容易解决,但是如果你需要对其中一个服务进行硬件上的升级,那么会发生什么情况呢?让我么考虑一下这些非常单纯的情况:
你拥有一个非常忙碌的站点,它使用中心数据库来存储客户信息。这个数据库还被市场团队使用,为某些应用程序和其他的报表工具存储数据。业务进展得很好,但是这台服务器(让我们称之为db02)现在有一些性能问题,并且需要用一台更好的,崭新的服务器(让我们称之为db04)来升级。这会是一个漫长而痛苦的过程,因为你需要找出所有使用这台服务器的应用程序,并且明确当开始切换的时候,要如何重新对它们进行配置以使用新的服务器。
因此:
我们应该对所有的服务使用DNS服务名称。最简单的解决方案是为你的服务端点使用DNSCNAME记录。在上述的示例中,我们会创建叫做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)拥有单独的配置值,它会在所有环境中正确地解析。这种技术还会带来额外的好处,它仍然拥有在一般的顶级域中所定义的全局服务。
Python培训,就选光环大数据Python培训机构!python学习地址:http://hadoop.aura.cn/python/
大数据培训、人工智能培训、Python培训、大数据培训机构、大数据培训班、数据分析培训、大数据可视化培训,就选光环大数据!光环大数据,聘请专业的大数据领域知名讲师,确保教学的整体质量与教学水准。讲师团及时掌握时代潮流技术,将前沿技能融入教学中,确保学生所学知识顺应时代所需。通过深入浅出、通俗易懂的教学方式,指导学生更快的掌握技能知识,成就上万个高薪就业学子。 更多问题咨询,欢迎点击------>>>>在线客服!