配置管理的五项最佳实践

最近有很多关于应用程序的配置以及如何对其进行管理的讨论。 我在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


This entry was posted in Best Practices. Bookmark the permalink.

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 更改 )

Twitter picture

You are commenting using your Twitter account. Log Out / 更改 )

Facebook photo

You are commenting using your Facebook account. Log Out / 更改 )

Google+ photo

You are commenting using your Google+ account. Log Out / 更改 )

Connecting to %s