第6章 测试云原生基础设施

    我们需要能够信任我们的基础设施。本章旨在开放信任和验证基础设施的意识形态。我们将描述的实践旨在增加对应用程序和基础设施工程的信心。

    软件测试在当今的软件工程领域非常普遍。然而,如何测试基础设施的还没很明确的最佳实践。

    这意味着在本书中的所有章节中,这一节应该是最令人兴奋的!像您这样的工程师有空间该领域发挥出色的影响力。

    软件测试是一种证明软件可以正常工作的有效做法,软件不会失败,并且在各种特殊情况下仍然有效。因此,如果我们将相同的范例应用于基础设施测试,测试目标如下:

    1. 证明基础设施按预期运行。
    2. 证明基础设施不会失败。
    3. 证明这两种情况在各种边缘情况下都是正确的。

    衡量基础设施是否有效需要我们先定义什么叫有效。现在,您应该对使用基础设施和工程代表应用程序的想法感到满意。

    定义基础设施API的人应该花时间构思一个可以创建有效基础设施的理智的API。例如,如果创建了一个定义虚拟机的API但没有使之可以运行的网络信息,这种做法就很愚蠢。您应该在API中创建有用的抽象,然后使用第3章和第4章中提出的想法来确保API创建正确的基础设施组件。

    我们已经开始开发一个心智模型,通过定义API来对我们基础设施的健全性进行检查。这意味着我们可以翻转逻辑并想象出相反的情况,这将是除了原始心智模型中的东西之外的所有东西。

    为基础设施定义基本的完整性测试的做法很值得努力。因此测试基础设施的第一步是证明您的基础设施是按照预期存在的,并且没有任何东西存在与原意相反。

    在本章中,我们将探索基础设施测试,并为新的测试工具顺序奠定基础。

    在开始编写代码之前,我们必须首先确定需要测试哪些方面。

    测试驱动开发是测试优先的常见做法。测试是为了证明测试的关注的方面而编写的,并且从一开始就会隐含失败。经过开发周期旨在使测试通过;也就是说,该软件是为满足每个测试中定义的要求而开发的。这是一个强大的实践,可以帮助软件保持专注,并帮助工程师对他们的软件以及测试结果产生信心。

    这是一个可以以多种方式回答的哲学问题。对我们需要证明真实而非虚假的想法有一个好的想法对于建立值得信赖的基础设施是必不可少的。例如,如果存在依赖基础设施的业务问题,则应该对其进行测试。更重要的是,许多业务不仅依赖基础设施,还会在出现问题时自行修复。

    确定您的基础设施需要填充的问题空间代表了需要编写的第一轮测试。

    远景规划是基础设施测试的另一个重要方面,但应该谨慎。在足够的前瞻性和过度工程化之间有一条看不见的界限。如果有疑问,坚持最少量的测试逻辑。

    在我们完全了解需要的测试之后,就可以考虑实施测试套件。

    编写可测试代码

    协调者模式的规则不仅旨在创建一个干净的基础设施应用程序,而且还旨在鼓励可测试的基础设施代码。

    这意味着,在应用程序的每一个重要步骤中,我们总是会重新创建一个相同类型的新对象,也就是说基础系统的每个主要组件都会使用相同的输入和输出。使用相同的输入和输出可以更轻松地以编程方式测试软件的小型组件。测试将确保您的组件按预期工作。

    然而,在编写基础设施测试代码时,还有许多其他有价值的经验值得借鉴。我们将在假设情景下看看测试基础设施的具体示例。在我们浏览场景时,您将学到测试基础设施代码的经验教训。

    我们还会提出一些工程师在开始编写可测试基础设施代码时可以遵守的规则。

    采用非常基础的基础设施定义,如示例6-1。

    例6-1. infrastructure.json

    这些数据的目的应该是显而易见的:确保一个名为的虚拟机的大小为large,IP地址为192.168.1.111。这些数据也暗示确保一个名为my-subnet的子网将容纳虚拟机my-vm

    希望你注意到了这个数据有什么问题。虚拟机的IP地址超出了子网的可用CIDR范围。

    应用程序运行此数据应该会导致失败,因为虚拟机的网络设置无效。如果我们的应用程序的构建是为了盲目地允许部署任何数据,我们将创建可联网的基础设施。尽管我们应该编写测试以确保新的虚拟机能够在网络上进行路由,但我们还可以做更多事情来帮助强化我们的应用程序并使测试更加轻松。

    在应用程序处理输入之前,我们可以首先尝试验证输入。这在软件工程中是很常见的做法。

    想象一下,如果不是盲目部署这个基础设施,我们首先试会验证输入。在运行时,我们的应用程序可以容易的检测到虚拟机的IP地址在虚拟机所连接的子网中不起作用。这将阻止输入到达我们的基础设施环境。由于知道应用程序将故意拒绝无效的基础设施表示,我们可以编写happy和sad测试来确保实现此行为。

    Happy测试可以对条件进行正面处理。换句话说,它是一种向应用程序发送有效API对象并确保应用程序接受有效输入的测试。Sad测试,可以对相反的情况或负面情况进行分析。例6-1是一个sad测试的例子,它将一个无效的API对象发送给应用程序,并确保应用程序拒绝无效输入。

    这种新模式使测试基础设施非常快速,而且通常不用费什么力气。一个工程师就可以开发大量的happy和sad测试,即使是最奇怪的应用程序输入也是如此。此外,测试集合可以随着时间的推移而增长;在欺骗API对象流入环境场景中时,工程师可以快速添加测试以防止再次发生。

    输入验证是测试最基本的事情之一。通过在我们的应用程序中编写简单的验证来检查理智的值,我们可以开始过滤应用程序的输入。这也给了我们一个很容易定义有意义的错误并快速返回错误的途径。

    验证提供信心,而不会让您等待基础设施发生变异。这为面向API开发的工程师创建了更快的反馈循环。

    输入您的代码库

    编写易于测试的代码非常重要。容易出错的问题可能会导致成本上升,因此需要围绕专有输入设计应用程序。专有输入是仅与程序中的一个点相关的输入,获得所需输入的唯一方法是线性执行程序。以这种方式线性编写代码对于人类大脑来说是有意义的,但这也是有效测试的最难的模式之一,特别是当涉及到测试基础设施时。

    专有输入陷入困境的例子如下:

    1. 函数DoSomething()的调用返回Something {}
    2. Something {}传递给函数并返回SomethingElse {}
    3. SomethingElse {}被传递给函数FinalStep (something else),返回true或false。

    这里的问题是,为了测试FinalStep()函数,我们首先需要遍历步骤1和2。在测试的情况下,这会引入复杂性和更多的失败点;它甚至可能不会在测试执行的环境中工作。

    更优雅的解决方案是以这样一种方式构造代码,即可以在程序的其余部分使用相同的数据结构上调用最后一的step():

    1. 代码初始化GreatSomething {},它实现了方法great.DoSomething()
    2. GreatSomething {}实现方法。
    3. GreatSomething {}实现了something.FinalStep()方法。

    从测试的角度来看,我们可以为我们希望测试的任何步骤填充GreatSomething {},并相应地调用这些方法。这个例子中的方法现在负责处理它们扩展的对象中定义的内存。这与最后一种方法不同,在这种方法中,特殊的内存中的结构被传递到每个函数中。

    这是一个更加优雅的设计,因为测试工程师可以轻松地为任何步骤合成存储器,并且只需要关注学习同一数据的一个表示。这是更加模块化的,如果它们很快被发现,我们可以回到任何故障。

    当您开始编写构成您的应用程序的软件时,请记住,在传统运行时间线期间,您需要在许多点上跳转到代码库。构建你的代码以便于在任何时候轻松地输入代码库,因此在内部测试系统至关重要。在这样的情况下,你可以成为自己最好的朋友或最大的敌人。

    在编写代码和测试时注意自己的置信度。自我意识是软件工程中最重要的部分之一,也是最容易被忽视的部分之一。

    测试的最终目标是增加对应用程序的信心。就基础设施领域来说,我们的目标就是增强对基础设施的信心。

    据说,测试基础设施的方法没有对错之分。在应用程序中可以通过代码覆盖率和单元测试来建立信心,但是对于基础设施来说这样做可能会存在误导。

    代码覆盖率是以程序化方式衡量代码可以达到预期的行为。这个度量标准可以用作原始数据点,但我们要知道即使是覆盖率达到100%的代码库仍然可能会出现极端中断,这一点至关重要。

    如果你以代码覆盖率来衡量测试结果,那么工程师就会编写更容易被测试覆盖的代码,而不是编写更适应该任务的代码。Dan Ariely在他刊登于哈弗商业评论的文章“衡量标准决定一切” :

    我们应该衡量的唯一指标是信心,即我们的基础设施可以按预期工作,并且我们可以证明这一点。

    衡量信心几乎是不可能的。但是有些方法可以从工程师的心理和情绪中抽取有意义的数据集。

    问自己以下几个问题,记录下答案:

    • 我担心这行不通吗?
    • 如果有人更改此文件,会发生什么情况?

    一个从问题中提取数据的最强大技术是比较以前的经验水平。例如,工程师可以做出如下陈述,团队的其他成员很快就会明白他想要传达的内容:

    比起上个季度,这次代码发布更令人担忧。

    现在,根据团队以前的经验,我们可以开始为我们的信心水平制定一套标准,从0开始表示完全没有信心,随着时间的流逝然后增加到非常自信。当我们了解了我们担心应用程序的哪些问题之后,再为了增加信心而制定测试内容就很简单了。

    测试类型

    了解测试的类型以及测试方式将有助于工程师增加其对基础设施应用程序的信心。这些测试不需要编写,而且没有正确或错误之分。唯一的问题是我们相信应用程序会做我们想要它做的事情。

    在软件工程中,有一个重要的概念是断言,这是一种强制的方式——完全确定条件是否成立。目前已经有许多成功的框架使用断言来测试软件。断言是一个微小的函数,它将测试条件是否为真。这些功能可以在各种测试场景中使用,以证明概念正在发挥作用和增加我们的信心。

    在本章的其余部分中,我们将提到基础设施断言。您需要对这些断言的内容以及他们希望完成的内容有基本的了解。您还需要对Go语言有基本的了解,才能充分认识这些断言正在做什么。

    在基础设施领域需要声明我们的基础设施有效。构建这些断言功能的库对于您的项目来说是一个值得的练习。开源社区也可以从这个工具包测试基础设施中受益。

    例6-2显示了Go语言中的断言模式。假设我们想测试虚拟机是否可以解析公共主机名,然后路由到它们。

    例6-2. assertNetwork.go

    在这个例子中,我们将两个断言作为VirtualMachine {}结构体上的方法来存储。方法签名是我们将在此演示中关注的内容。

    第一种方法AssertResolvesHostname()演示了一种将用于检查给定主机名是否解析为预期IP地址的方法。第二种方法AssertRouteable()演示了一种用于检查给定主机名是否可在特定端口上路由的方法。

    注意VirtualMachine {}结构体是如何定义成员本地IP的。另请注意,VirtualMachine {}结构体具有Connect()函数以及Close()函数。这是因为断言框架可以在虚拟机的上下文中运行这个断言。测试可以在基础设施环境之外的系统上运行,然后连接到环境中的虚拟机以运行基础设施断言。

    在例6-3中,我们演示了工程师该如何在本地系统上编写Go测试。

    例6-3. network_test.go

    该示例使用Go语言中的内置测试标准,这意味着该函数将作为应用程序中Go测试的正常运行测试的一部分执行。测试框架将测试名称以_test.go结尾的所有文件,并使用以TestXxx开头的签名名称测试所有函数。该框架还将* test.T指针传递给以这种方式定义的每个函数。

    这个简单的测试将使用我们之前定义的断言库来完成以下几个步骤:

    1. 尝试连接到应在10.0.0.17上可访问的虚拟机。
    2. 在虚拟机上尝试断言虚拟机可以解析google.com并且可返回一些IP地址。
    3. 在虚拟机上尝试声明虚拟机可以通过端口443路由到google.com。
    4. 关闭与虚拟机的连接。

    这是一个非常强大的程序。它为我们的基础设施按预期工作建立了信心。它还引入了一个优雅的脚手架,供工程师定义测试,而不必担心它们将如何运行。

    开源社区迫切需要这样的基础设施测试框架。基础设施测试的标准化和可靠方法将成为开发者工具箱中的有益补充。

    集成(integration)测试

    集成测试也被称为端到端(e2e)测试。这些是长期运行的测试。按照预生产的方式来运行系统,这些证明可靠性和增加信心的最有价值的测试。

    编写集成测试套件可能很有趣,也很有意义。在集成测试基础设施管理应用程序的情况下,测试将执行基础设施生命周期的大扫除。

    线性集成测试套件的一个简单例子如下:

    1. 定义一个常用的基础设施API。
    2. 将数据保存到应用程序的数据存储区。
    3. 运行该应用程序并创建基础设施。
    4. 针对基础设施运行一系列断言。
    5. 从应用程序的数据存储中删除API数据。
    6. 确保基础设施已成功销毁。

    在此过程中的任何一步,测试都可能失败,并且测试套件应该清理发生变异的基础设施。这是测试可以按照预期销毁基础设施重要的原因之一。

    测试使我们相信,该应用程序将创建并销毁预期的基础设施,并按预期工作。随着时间的推移,我们可以增加步骤4中运行的断言的数量,并继续强化套件。

    集成测试工具可能是我们测试基础设施最强大的环境。没有集成测试工具,运行像单元测试这样的小测试有多大价值。

    单元测试是测试系统并单独运行其组件的基本部分。单元测试的责任是小而谨慎。单元测试是软件工程中的常见做法,因此将成为基础设施工程的一部分。

    在编写基础设施测试的情况下,测试系统的一个组件是困难的。基础设施的大多数组件都建立在彼此的基础之上。相应地测试软件通常需要改变基础设施来测试并查看其是否工作。这个过程通常涉及大部分系统。

    但这并不意味着为基础设施管理系统编写单元测试是不可能的。事实上,前面例子中定义的大部分断言在技术上将都是单元测试!单元测试只测试一个小组件,但在大型集成测试系统环境中使用时,它们可能非常有用。

    在测试基础设施时鼓励进行单元测试,但请记住,它们运行的上下文通常需要相当大的开销。这种开销通常以集成测试的形式出现。将单元测试的小而谨慎的检查与更大的整体测试模式相结合,使基础设施工程师对其基础设施按照预期工作具有高度的信心。

    模拟(Mock)测试

    在软件工程中,综合系统的常见做法是模拟测试。在模拟测试中,工程师编写或使用旨在欺骗或伪造系统的软件。

    一个简单的例子就是使用一个旨在与API通信并以“mock”模式运行的SDK。SDK不会将任何数据发送到API,而是合成SDK认为API在各种情况下应该执行的操作。

    确保模拟软件准确地反映它正在合成的系统的责任在于开发模拟软件的工程师手中。在某些情况下,模拟软件也是由开发其正在模拟的系统的工程师开发的。

    尽管可能有一些模拟工具保持最新并且比其他工具更稳定,但使用模拟系统合成您计划测试的基础设施时存在一个普遍的道理:虚假系统只会给您带来虚假信心。

    现在,这条规则可能看起来很苛刻。但它的目的是鼓励工程师不要轻易走出去,并通过构建真正的集成套件来运行测试的实践。虽然模拟系统功能强大,但将它作为基础设施测试的核心(因此也是您的信心)是非常危险的。

    大多数公有云提供商对其资源实施配额限制。想象一下与一个对资源有严格限制的系统进行交互的测试。模拟系统可能会尽最大努力限制资源,但是如果不在运行时审核实际系统,模拟系统将无法确定您的基础设施是否实际部署。在这种情况下,您的模拟测试会成功。但是,当代码在真实环境中运行时,它会中断。

    这只是许多实例中的一个例子,这些实例证明了为什么变异实际基础设施和发送实际网络数据包比使用模拟系统更可靠。请记住,测试的目标是增强您的基础设施在真实环境中按照预期工作的信心。

    这并不是说所有的模拟测试都不好。了解模拟正在测试的基础设施与为了方便而模拟另一部分系统之间的差异非常重要。

    工程师需要决定什么时候适合使用模拟系统。我们只是告诫工程师不要对这些系统有太大的信心。

    混沌测试可能是我们将在本书中介绍的测试基础设施中最令人兴奋的方法。它正在进行测试,以证明在基础设施中发生不可预知的事件,而不会影响基础设施的稳定性。我们通过故意破坏基础设施并衡量系统如何应对灾难来做此演示。与我们所有的测试一样,我们将以基础设施工程师的身份来应对这个问题。

    我们将编写旨在以意想不到的方式打破生产系统的软件。建立对系统的信心的一部分是理解他们如何以及为什么会破坏。

    测量混乱

    我们再来看一下例6-3中的AssertRouteable()函数。想象一下,我们有一个服务,将连接到虚拟机,并尝试保持连接打开。服务每秒都会调用函数并记录结果。来自此服务的数据是虚拟机在其网络上路由的能力的准确表示。只要虚拟机可以路由,数据就会在图形上产生一条直线,如图6-1所示。

    图6-1. 随着时间推移的AssertRoutable测试图

    如果在任何时候连接断开或者虚拟机不再能够路由,那么图形数据会发生变化,并且我们会看到图形上的线条发生变化。随着基础设施自行修复,线路开启该图将再次稳定下来,如图6-2所示。

    f-6-2

    图6-2. 失败并且随着时间的推移修复AssertRoutable测试

    这里考虑的重要方面是时间。随着时间的推移,测量混乱将伴随着混沌的测量。

    我们可以快速扩展测量。想象一下,名为AssertRouteable()的服务现在正在虚拟机上调用一组100个基础结构断言。另外,假设我们有100台虚拟机正在测量。

    这将对我们的基础设施产生大约每秒1.0×104个断言。来自我们的基础设施断言的大量数据使我们能够创建强大的图形表示基础设施。以可查询的格式记录数据也可以进行高级混沌调查。

    随着混沌的测量,拥有可靠的测量工具和服务非常重要。以有意义的方式存储来自服务的数据也很重要,以便稍后可以引用它们。强烈建议将数据存储在日志聚合器或其他容易索引的数据存储中。

    系统的混乱与系统的可靠性成反比。因此,它直接反映了我们正在评估的基础设施的稳定性。这意味着,当事情发生中断或引入变化时,将信息随时间绘制成分析是非常有价值的,以了解是否降低了稳定性。

    引入混沌

    将混沌引入系统的另一种说法:“故意破坏系统”。我们希望总结出我们可能在野外看到的意想不到的基础设施问题组合。如果我们不会故意注入混沌,那么云提供商、互联网或某个系统会为我们做这件事。

    此时,您应该有一个准备好使用的基础设施管理应用程序,或者至少有一个。用于部署,管理和稳定基础设施的基础设施管理应用程序也可用于引入混乱。

    想象一下两个非常相似的部署。

    第一个示例6-4代表有效(或happy)基础架构。

    例6-4. infrastructure_happy.json

    我们可以使在环境中设置的方式来部署此基础设施。这个基础设施应该部署且运行稳定。就像以前一样,随着时间的推移记录您的基础设施测试非常重要;图6-3就是一个例子。理想情况下,您运行的测试数量应该随着时间的推移而增加。

    我们决定引入混乱。因此,我们创建了原始基础设施管理应用程序的副本,但这次我们采取了更加险恶的方式部署基础设施。我们利用我们的部署工具的能力来审计基础设施,并对已经存在的基础设施进行更改。

    图6-3. 成功的测试

    第二次部署将代表有意故障的基础设施,并仍使用与原始基础设施相同的标识符(名称)。基础设施管理工具将检测现有基础设施并进行更改。在第二个示例(示例6-5)中,我们将虚拟机大小更改为较小,并且意图将虚拟机的静态IP地址192.168.1.111分配到10.0.100.0/24范围之外。

    我们知道虚拟机上的工作负载不会在小型虚拟机上运行,并且我们知道虚拟机将无法在网络上路由。这是我们将要介绍的混乱情况。

    例6-5. infrastructure_sad.json

    由于第二个基础设施管理应用程序默默地对基础设施进行了更改,因此我们可以预料会看到事态发展。我们图中的数据将开始波动,如图6-4所示。

    f-6-4

    图6-4. 包含网络故障的图形

    如果虚拟机上的任何应用程序未完全中断,则应该缓慢地失败。虚拟机的内存和CPU现在已经过载。该shell无法fork新进程。负载平均值远高于20。系统正在接近死锁,我们甚至无法访问虚拟机来查看错误,因为没有任何方式可以路由到冒名顶替者的IP地址。

    正如预期的那样,初始系统将检测到底层基础设施中的某些内容发生了变化,并会相应地进行调整。冒名顶替者系统脱机是非常重要的,否则两个系统之间可能会有永无休止的和解,而这两者将会按照指示的方式相互竞争以纠正基础设施。

    这种引入混沌的方法之美在于,我们不需要开发任何额外的工具或花费任何工程时间编写混沌框架。我们以巧妙的方式滥用了原有的基础设施管理工具,引发了一场灾难。

    当然,这可能并不总是一个完美的解决方案。与您的生产基础设施应用程序不同,您的混沌应用程序应该有一定的限制,以确保它们有益。一些常见的限制是能够根据标签或元数据排除某些系统,不能在非工作时间运行混沌测试,并将混沌限制在特定的百分比或系统类型。

    现在引入随机混沌的主要负担在于基础设施工程师随着时间推移而随机化探索的工作流程的能力。当然,基础设施工程师还需要确保从实验中收集的数据以可消化的格式提供。

    除了测试基础设施外,我们不能忘记监控正在运行的系统。测试和熟悉的失败模式可以让您对基础设施充满信心,但要测试系统可能出现的所有故障是不可能的。

    监测可以检测到在测试期间未识别的异常并执行正确的操作是非常重要的。通过积极监控站点基础设施还可以增强我们的信心,即当发生的事情没有被认为是“正常”时,我们会收到警报。明确什么时候以及如何提醒人类这些异常是一个很有争议的话题。

    在云原生环境中监控基础设施的实践中涌现出许多优秀的资源。我们不会在这里讨论这些主题,但您应该先阅读Rob Ewaschuk的“监控分布式系统:Google的SRE团队的案例研究”(O’Reilly),并观看MonitoramaConference上的视频。两者都可以在线免费观看。

    无论您实施哪种监控解决方案,都要记住用云原生方法来创建您的监控规则。规则应声明并存储为代码。监控规则应与您的应用程序代码放在一起,并以自助服务的方式提供。当测试和遥测可能满足您的大部分需求时,不要过度补偿监控。

    结论

    测试可以将强我们对基础设施的信心,我们所支持的应用程序也获得了信心和信任。如果一个测试套件不能提供信心,它的价值应该是有问题的。记住,本章中提出的工具和模式是出发点,旨在激发和吸引在这个领域中工作的工程师。无论测试类型或运行它们的框架如何,最重要的一点是工程师可以开始相信他们的系统。作为工程师,我们通常会通过观察实践证明事情按预期工作的动手演示获得信心。

    而且,在生产中进行实验不仅是可以的,而且是值得鼓励的。您需要确保环境是为了进行这种实验而建立的,并且实施了适当的跟踪,以便不会浪费测试!

    现实测量是基础设施开发和测试的重要组成部分。能够从工程角度和运营角度来封装现实是运用基础设施的重要组成部分,因此可以确信它能够按预期运行。