下载本文的PDF版本 PDF

使用建模应对架构复杂性

组件模型可以帮助诊断新系统和现有系统中的架构问题。

Kevin Montagne

现代计算机日益增长的强大能力使得解决曾经被认为过于困难的问题成为可能。然而,对于这些功能复杂的难题领域,系统架构常常过于复杂。在本文中,我使用架构一词来指代系统的整体宏观设计,而不是各个部分如何实现的细节。系统架构是可用功能背后的东西,包括内部和外部通信机制、组件边界和耦合,以及系统将如何利用任何底层基础设施(数据库、网络等)。架构是对以下问题的“正确”答案:这个系统是如何工作的?

问题是,对于理解——或者更好的是,预防——系统中的复杂性,我们能做些什么?许多开发方法(例如,Booch1)考虑了非功能方面,但往往止步于图表阶段。“我们可以稍后解决[性能、可扩展性等]”的口头禅可能会造成严重的影响。系统中的单个组件(应用程序)通常可以迭代,但迭代架构通常要困难得多,因为涉及到所有的接口和基础设施影响。

现有系统建模

在本文的后面,我将描述一种在着手创建新系统时的架构设计方法。但是,如果系统已经以某种形式存在,您该怎么办?我的许多架构工作都是针对现有系统进行的——很多时候是以“局外人”的身份受邀(或派遣)来评估和改进系统的状态。当处理复杂系统时,这些任务可能非常具有挑战性。

对现有系统进行建模的一个优势是,一般的行为已经就位,因此您不是从空白状态开始。您可能也不必处理系统功能部分的创建。然而,这是有代价的。系统架构很可能很复杂且没有被很好地理解。此外,由于系统大修的高成本,许多解决方案可能不切实际。

对于任何类型的系统,目标都是尽可能地理解架构和系统行为。当一个大型系统已经存在多年时,这似乎是一项艰巨的任务。有很多技术可用于发现系统的工作原理以及可以改进的方法。您可以询问开发和维护团队的成员。诊断工具(例如,DTrace)可以帮助快速找到系统中性能或可扩展性的罪魁祸首。您可以梳理成堆的日志文件,看看开发人员认为值得注意的内容。在本文中,我重点介绍如何使用各种系统组件的建模来获得更深入的理解,并为评估可能的更改提供基础。

这种类型的建模不仅仅是白板或纸上谈兵。它是创建驱动程序和组件来模拟系统的各个方面。驱动程序用于调用系统的各个部分,以模仿其正常行为。其目的是在没有“确保功能正确性”的负担下,演练架构。有时,这些驱动程序可能是使用已建立的工具(例如,WinRunner、JMeter)编写的脚本,但我经常发现在开发特定于要驱动的组件的程序中更有价值。这些程序使我能够获得做出高质量决策所需的信息。重要的是要理解,模型组件和相关的驱动程序不仅仅是简单的测试程序,而是要用作探索和发现的基础。

系统建模的过程应该从一次检查一个或两个组件开始。最初的目标应该是怀疑对整个系统产生负面影响的组件。然后,您可以构建独立的驱动程序来与组件进行交互。如果问题组件得到确认,则可以开始对可能的更改进行实验。这些更改可能跨越从代码更改到基础设施更改再到硬件更改。有了正确的驱动程序和组件建模,重新设计某些组件可能会变得切实可行。

有时,组件中包含的功能与架构紧密交织在一起,以至于有必要创建一个轻量级副本。系统的某些功能方面掩盖底层技术或基础设施响应请求应用程序的行为并不罕见。在这些情况下,拥有一个轻量级模型可以探索和更好地理解架构交互。如果您发现了架构解决方案,那么您可以继续研究各种功能实现。

早期 Windows 系统建模

我的第一次建模经验涉及创建驱动程序和模拟组件,以探索一项新技术。我在 20 世纪 80 年代后期为一家大型金融机构工作,当时 Microsoft Windows 2.1 发布了。一群开发人员为电话客服代表创建了一套相当复杂的 Windows 应用程序。这些应用程序提供了从多个基于大型机的系统检索客户信息、余额等的能力(使用现在古老的“屏幕抓取”概念,即抓取旨在显示在 IBM 3270 哑终端上的数据),然后以聚合视图呈现数据。它还允许客服代表代表客户进行交易。

该套件最初只是一个概念验证,但原型演示非常成功,以至于被匆忙投入生产。当我加入团队时,它已经部署到大约 150 名代表。随着程序开始整天使用,问题开始频繁发生。这些问题以各种形式表现出来:内存泄漏、访问冲突、虚假错误消息和机器锁定(又称死机)。

我们的小团队忙于添加功能以满足快速增长的需求清单,同时解决稳定性问题。我们浏览源代码,解决内存泄漏和访问冲突。我们努力追踪不断增长的新观察到的错误消息列表。最具挑战性的任务是“死机巡逻”,我们花费了大量时间来追踪那些机器锁定。问题是我们对 Windows 在幕后是如何工作的没有真正好的理解。

那些熟悉早期 Windows SDK 编程的人会记得,文档(更不用说稳定性)没有得到很好的开发。API 函数非常底层,而且似乎有无数个。(如果不是 Charles Petzold 的Programming Windows [Microsoft Press, 1988],我不确定在 20 世纪 80 年代,有多少 Windows 应用程序会在 Microsoft 之外开发完成。)应用程序的代码库已经相当庞大——至少对于当时的应用程序而言——而且每个应用程序的实现方式略有不同(毕竟它们是原型)。 Microsoft 提供了一些示例程序,但没有一个接近这些应用程序的复杂性。因此,我们决定构建模仿我们试图实现的 Windows 行为的组件(应用程序)。

这些组件大多没有功能,但从与实际应用程序类似的基本结构和接口机制开始。驱动程序向模型组件发送细粒度的 Windows 消息,以模拟按键和其他外部发起的动作。它们还在整个应用程序套件中发送 DDE (动态数据交换,一种在 Windows 程序之间通信数据的原始方式)消息。随着模型的成熟,我们开始合并实际程序中使用的更多 API 调用(例如,用户界面控件)。

许多死机被追踪到 Windows GDI(图形设备接口)调用的未记录的特性。示例包括对某些 API 调用顺序的敏感性、在同一上下文中进行的某些调用之间的不兼容性以及资源耗尽的可能性。在早期版本的 Windows 中,GDI 库与内核库紧密交织在一起。随着 Windows 的成熟,类似的困境变成了错误消息、异常或只是导致问题的应用程序锁定。

建模的结果是我们获得了足够多的关于这项新颖的 Windows 技术的信息,可以将程序改进到稳定性成为合理期望的程度。在 18 个月内,该系统被部署到 4,500 多个工作站,并在 Windows NT 的生命周期中良好地存活下来。

“从属”系统建模

并非我所有的建模经验都带来了如此积极的结果。一些建模经验暴露了架构设计中的根本缺陷,而对于少数几个建模经验,唯一的选择是放弃系统并重新开始。这些信息通常不受项目管理的欢迎。

一个更值得注意的例子发生在一个旨在成为“从属”的系统中,该系统从几个现有系统接收更新,并将它们应用于一个新的数据库。该数据库将供其他新系统使用,以构成替换旧系统的基础。这些系统将使用新的技术平台构建。这些技术非常不同,功能范围如此之广,以至于仅从属系统的开发团队就已发展到 60 多人。

在基本架构和大部分功能已经设计和开发出来之后,但在距离生产还有几个月的时间时,我加入了该项目。我们团队的任务是帮助充分利用基础设施,并优化应用程序之间的交互方式。仅仅几周后,我们就怀疑一些糟糕的初始假设已经影响了架构设计。(我并不想贬低我的例子中的任何团队,而只是想指出过分关注功能而牺牲坚实的架构基础的潜在问题。)因为看起来性能和可扩展性将成为主要问题,所以架构团队开始研究一些模型组件和驱动程序来调查设计。

我们对传入消息的速率和事务类型的组合进行了一些研究。我们还从已经构建的功能“处理器”中采样了时间。然后,使用与现有调度程序相同的消息传递基础设施,我们构建了一个组件,该组件将模拟传入消息调度程序。一些消息传递技术对公司来说是新的。在调度程序的一端,我们有驱动程序来模拟入站消息。在另一端,我们使用围绕采样时间聚类的伪随机数模拟了 FP(功能处理器)的性能。根据设计,模型组件或驱动程序中没有任何与系统中的功能处理相关的内容。

一旦模型完全发挥作用,我们就可以使用与传入消息速率和模拟 FP 时间相关的各种参数。然后,我们开始根据传入消息类型组合中处理成本的变化来权衡 FP 时间。在此建模工作之前,该设计(错误地)假设最重要的性能方面是单个事务的延迟。几秒钟的延迟对于所有相关人员来说都是可以接受的。毕竟,这将需要相当长的时间,这个从属系统才能成为记录系统并反向驱动事务。

建模结果并不令人鼓舞。延迟将是一个挑战,但总体吞吐量要求将淹没系统。我们开始探索解决性能问题的方法。该系统已经针对所选平台可用的最快硬件进行了定位,因此该选项被排除在外。我们推迟了研究改进单个功能处理器的性能;这被认为成本更高,因为已经编写了许多功能处理器。我们认为,专注于通用基础设施组件可能会增加我们快速成功的机会。

我们研究了新的调度算法,但这并没有带来足够的改进。我们研究了优化消息传递基础设施,但仍然不足。然后,我们开始基准测试一些其他消息格式和基础设施,结果稍微令人鼓舞。我们检查了现有程序,看看更改消息格式和技术有多容易。程序过于依赖消息结构,无法在合理的时间范围内进行更改。

鉴于结果仍然不佳,我们需要检查功能算法和数据库访问。我们选取了一些中等长度和较长运行时间的处理器,并插入了一些日志记录以获得各个步骤的分割时间。由于数据映射和重组所需的复杂性,许多功能算法相对昂贵。数据库操作似乎比我们逻辑上认为的要花费更长的时间。(随着时间的推移,架构师应该根据他对类似功能的抽象视图(他或她以前已最大限度地提高了性能)来培养对性能预算的感知。)

然后,我们检查了逻辑数据库模型。该设计不是系统中程序类型的性能模式。从一些算法中提取的 SQL 被放置在独立的模型组件中。目的是看看哪种类型的性能提升是可能的。一些提升来自于更改一些 SQL 语句,这些语句花费了过多的时间,因为选择的分区方案意味着读取核心表通常涉及扫描所有分区。随着我们模拟的数据库大小增长,这对可扩展性造成了惩罚。然而,主要问题不是单个语句的扩展时间长度,而是调用次数过多。这是过度规范化的结果。有许多表在频繁更改的列上带有索引。此外,正在使用多列键而不是人工键,有时称为代理键。系统生成它们(通常为整数)以表示“真实”键。当处理复杂的密钥结构和/或实际密钥值可以更改时,这可以提高性能和维护。

我们确定,如果我们重组数据库设计并更改相关的 SQL 语句,则可以实现实质性的改进。然而,程序的编写方式使得更改成本非常高昂。我们的结论是,如果该系统要取得成功,则需要进行重大改造。由于该项目已经花费了超过 1000 万美元,因此这个建议很难推销。

在额外花费 500 万美元之后,该项目被取消,我们团队的重点被转移到其他工作上。建模过程仅花费了大约六周时间。这里要说明的重点是,可以使用建模来审查主要的架构决策,然后再投入大量资金。在系统构建之前而不是在投入生产之后发现设计将无法执行或扩展,成本要低得多。

新系统建模

对于新系统——或在对现有系统进行重大改造时,研究架构选项应该是标准做法。实验应该使用轻量级模型而不是完整系统,但至关重要的是,这些模型要准确捕捉系统不断演进的行为。否则,建模过程的价值就会降低,并可能导致错误的结论。

我通常从尝试以抽象的方式理解功能问题空间开始。主要功能是用户请求的操作,然后是系统回复(即,请求/回复)吗?是请求,然后是一连串的通知(例如,跳动的报价)或比特(例如,音乐或视频)吗?是为了处理一些输入数据并将结果发送到另一个进程或系统(即,流通过)吗?是为了在海量数据集中进行搜索以查找信息(决策支持系统)吗?是这些的组合,还是完全不同的东西?

有些人可能会问:我如何知道应该对系统的哪些部分进行建模,以及应该在这个过程中花费多少时间和精力?这是一个简单的风险管理案例。建模应侧重于最容易出错的领域。该过程应持续到高风险决策可以被证明是合理的时候。努力尽可能经常地重新测试决策。

建模最具挑战性的方面之一是在捕捉足够的系统行为和防止模型变得过于复杂(且成本高昂)以至于无法实现之间找到正确的平衡。对于现有系统,这更容易。当您逐步完成建模迭代时,如果观察结果开始模仿系统的各个方面,那么您可能已经非常接近了。您可以开始更改建模驱动程序和组件,以探索更多的行为。对于新系统,我通常希望对可以用作真实组件外壳的模型组件进行建模。目标是为负责的开发人员提供一个起点,使他们能够专注于功能,而不是必须探索底层技术和基础设施的关键细微之处。

在设计或评估架构时,有许多技术模式需要考虑:性能、可用性、可扩展性、安全性、可测试性、可维护性、易于开发和可操作性。这些模式的优先级排序可能因系统而异,但每个模式都必须考虑。如何解决这些模式以及它们相应的技术考虑因素可能因系统组件而异。例如,对于请求/回复和流式更新,延迟是一个关键的性能因素,而吞吐量对于流通过消息处理或批量请求功能可能是一个更好的性能因素。一个可能微妙但仍然重要的信息是避免在同一组件中混合不同的模式实现。不遵守这一教训会使架构走上一条通往复杂性的道路。

听到这样的借口太常见了:“系统[将要]太大,无法花时间对其行为进行建模。我们只需要开始构建它。” 如果建模的繁琐工作被认为过于繁重,那么可能很难实现可预测的性能、可扩展性和其他理想的技术属性。一些开发项目非常注重单元测试,但在我的经验中,很少发现有相应的重点关注对整个系统架构进行测试。

示例组件建模

描述示例组件的建模可能会为我提倡的方法提供额外的见解。假设一个新系统需要接收一些数据项流(例如,股票报价),丰富数据,并将其发布给最终用户。架构师可能会建议构建某种类型的发布者组件来执行此核心需求。如何在围绕它构建系统之前对这个组件进行建模?数据吞吐量和延迟可能是主要关注点。理想情况下,我们对这些有某些目标要求。可扩展性和可用性也是可以在模型后续迭代中解决的问题,但在继续进行功能开发之前。

基于这个简单的例子,模型应该包含至少两个与发布者组件不同的构建块。需要模拟传入数据馈送。应该构建一个驱动程序来将数据泵入发布者。此外,还需要某种类型的客户端接收器来验证消息流并实现吞吐量和延迟的测量。图 1 显示了一个简化的图纸,其中包含用于建议的发布者的驱动程序和接收器。

发布者模型组件应该使用建议的目标语言构建。它应该使用任何可能影响模型结果的框架、库等,尽管可能不清楚其中哪些可能会产生影响。在这种情况下,您应该采取风险管理方法,并包含那些对组件操作至关重要的框架、库等。任何行为尚未完全理解的新技术也应包括在内。任何非可疑的基础设施都可以在以后的迭代中添加。重要的是不要过早地陷入尝试构建功能。应尽可能多地进行存根。

在某些系统中,发布者之类的组件可能会带来最大的可扩展性障碍。在这种情况下,我们需要知道可以处理哪种类型的消息流,可以预期哪种类型的延迟,可以支持多少客户端,以及客户端应用程序可以处理哪种类型的流。

数据馈送驱动程序应接受允许将消息速率拨到任意级别的参数。任何驱动程序都应该能够将其目标推到远远超过任何预期的最高水位线。消息不必与预期的格式匹配,但它们的大小应该相对接近。由于驱动程序与发布者紧密耦合,因此应该为相同类型的平台(语言、操作系统等)编写并在其上运行。这使得同一个开发人员可以构建组件和驱动程序。(我强烈建议,每个负责系统级组件的开发人员也创建一个不同的驱动程序和一个可能的接收器作为标准做法。)客户端接收器也是如此,因此所有三个都可以打包在一起。这提供了凝聚力,这将允许模型在未来用于其他目的。

随着建模的进行,应该为目标客户端平台构建另一个模型接收器,使用其预期的框架和通信机制。两个不同平台接收器/接收器的原因是允许在不涉及另一个平台(例如,可扩展性测试)的情况下测试发布者模型组件。客户端平台模型接收器可用于确定发布者是否与客户端平台正确交互。在未来的故障排除会话中,这些单独的接收器将提供一种隔离问题区域的方法。所有驱动程序和接收器都应作为发布者开发和维护的一部分进行维护。

下一步是在驱动程序和接收器的操作中评估发布者模型。为了表征性能,需要在客户端接收器中添加某种类型的仪表,以计算吞吐量。必须小心对待任何类型的仪表,以免它影响测试结果。例如,记录接收到的每条消息以及时间戳可能会对性能产生不利影响。相反,可以将摘要统计信息保存在内存中,并在定期或测试结束时写出。

数据馈送驱动程序应以可配置的速率输出数据,而客户端接收器应计数消息并计算接收到的数据速率。另一种仪表方法可用于采样延迟。在指定的消息计数间隔,数据馈送驱动程序可以记录消息编号和原始时间戳。然后,客户端接收器可以在同一间隔记录接收时间戳。如果在适当的频率记录,样本可以很好地代表延迟,而不会影响整体性能。可能需要高分辨率计时器。跨多台机器进行测试,延迟要求低于时钟同步漂移将需要更复杂的计时方法。

应该以各种消息速率(包括完全淹没发布者及其可用资源的速率)来运行此模型。除了观察吞吐量和延迟之外,还应该分析系统资源利用率(CPU、内存、网络等)。此信息稍后可用于确定探索基础设施调整是否可能带来好处。

如前所述,发布者需要在消息传递过程中进行某种类型的数据丰富。吞吐量、延迟和内存消耗可能会受到这种丰富的影响。应该估计这种影响并将其纳入模型发布者中。如果无法获得真实的估计值,则高估(或者,按照本文的理念,构建另一个模型并对其进行表征)。如果丰富成本因消息类型而异,则可以将围绕预期平均值的伪随机延迟和内存分配插入到模型发布者中。

建模的其他用途

建模是一个迭代过程。不应将其视为仅仅是某种类型的性能测试。以下是可以添加到进一步评估过程的项目列表。

• 使用模型来评估各种基础设施选择。这些可能包括消息传递中间件、操作系统和数据库调整参数、网络拓扑以及存储系统选项。

• 使用模型为一组硬件创建性能配置文件,并使用该配置文件推断其他硬件平台上的性能。如果在多个硬件平台上对模型进行分析,则任何推断都将更准确。

• 使用性能配置文件来确定随着系统的增长,是否可能需要发布者的多个实例(横向扩展)。如果是这样,则应将此功能构建到设计中并进行适当建模。转换设计为单例的组件可能会非常昂贵。

• 使用模型来探索可能的故障场景集。可用性是高质量系统的主要属性之一。在系统构建完成后才解决它可能会花费高出一个数量级。

本文中使用的示例可以在许多系统的抽象中看到。对于任何重要的组件,都应进行类似的建模方法。当构建和测试了相互关联的模型后,可以将它们组合起来进行更全面的系统建模。一次构建一个模型的方法允许逐步获得系统行为知识,而不是试图理解——更不用说构建——一个包罗万象的模型。

几乎所有系统中都存在的一个关键要素是某种类型的数据存储。评估数据库设计可能很复杂。但是,有一些步骤与已经讨论的系统建模类似。一旦数据库模型(列、表等)的草稿可用,就可以用足够生成的数据填充它,以进行一些性能测试。为此目的编写数据生成器所需的工作量将使您了解在开发过程中使用数据库的容易程度。如果这个生成器看起来太难处理,那么这可能表明数据库模型已经过于复杂。

在表填充完毕后,下一步是创建驱动程序,该驱动程序将执行预期最昂贵和/或最频繁的查询。这些驱动程序可用于改进底层关系模型、存储组织、调整参数等。执行这种类型的建模可能是无价的。在所有查询都已编写且系统正在生产环境中运行时,发现应用程序级数据模型中的缺陷是痛苦的。我曾致力于改进数十个系统的数据库性能。在开发后优化查询、存储子系统和其他数据库相关项确实具有挑战性。如果系统已在生产环境中运行一段时间,那么这项任务就更加困难了。许多时候,低级基础设施的更改可以通过早期建模来确定。有了适当的设计,更标准的配置可能就足够了。

仪表和维护

无论驱动程序/组件组合的类型如何,仪表对于建模和系统的长期健康都至关重要。它不仅仅是一种奢侈品。不了解性能情况的盲目飞行是不可取的。只有在晴空万里时才能使用目视飞行规则(即,没有仪表)。对于现代系统来说,这种情况有多常见?功能和技术复杂性通常会使清晰地看到正在发生的事情的能力变得模糊。系统性能可能就像乘坐木筏顺流而下。如果您不定期观察水流速度,那么您可能不会注意到即将到来的瀑布,直到木筏无可救药地坠落到边缘。

随着系统的发展,保持驱动程序和模型组件的最新状态有很多优势

• 当提出更改时,它们可用于性能、可用性、可扩展性等的一般回归测试。

• 它们可以通过从较小的一组资源中推断性能来用于容量规划。做到这一点的唯一可行方法是充分了解资源使用特征。

• 它们可以为可能需要对现有系统进行的基础设施或其他大规模更改建模。

• 有时,存在开发/维护团队无法控制的因素(例如,基础设施更改)。驱动程序可用于测试系统的隔离部分。如果外部因素造成任何降级,则结果可以提供“防御性”数据来更改或回滚更改。

• 当出现某种类型的性能、可用性、可扩展性或其他基础设施问题时,拉出模型和驱动程序会比承担可能压倒性的任务(在排查生产问题的压力下更新它们)要快得多。

建模是一种极其强大的方法,可以理解和提高系统的整体质量。对于预期寿命为多年的系统,这种改进转化为真正的资金节省。开发组织可以将他们的预算资金用于提供功能。如果模型和相关的驱动程序得到持续维护,那么这种对功能的关注就可以得到广泛的赞扬。

参考文献

1. Booch, G. 1993. Object-oriented Analysis and Design with Applications (第二版). Redwood City: Benjamin Cummings.


喜欢它,讨厌它?请告诉我们

[email protected]

Kevin Montagne 在 IT 领域拥有超过 25 年的经验,致力于性能和可用性至关重要的大型系统。他在金融行业度过了其中的 20 年,其中包括十多年担任前台交易系统的架构师。

© 2010 1542-7730/10/0900 $10.00

acmqueue

最初发表于 Queue 杂志第 8 卷,第 9 期
数字图书馆 中评论本文





更多相关文章

Catherine Hayes, David Malone - 质疑评估非加密哈希函数的标准
虽然加密和非加密哈希函数无处不在,但在它们的设计方式上似乎存在差距。加密哈希存在许多由各种安全要求驱动的标准,但在非加密方面,存在一定程度的民间传说,尽管哈希函数历史悠久,但尚未得到充分探索。虽然针对真实世界数据集的均匀分布非常有意义,但在面对具有特定模式的数据集时,这可能是一个挑战。


Nicole Forsgren, Eirini Kalliamvakou, Abi Noda, Michaela Greiler, Brian Houck, Margaret-Anne Storey - DevEx 在行动
随着领导者寻求在财政紧缩和人工智能等变革性技术的背景下优化软件交付,DevEx(开发者体验)在许多软件组织中越来越受到关注。技术领导者凭直觉接受,良好的开发者体验能够实现更有效的软件交付和开发者幸福感。然而,在许多组织中,为改进 DevEx 而提出的倡议和投资难以获得支持,因为业务利益相关者质疑改进的价值主张。


João Varajão, António Trigo, Miguel Almeida - 低代码开发生产力
本文旨在通过展示使用基于代码、低代码和极端低代码技术进行的实验室实验结果,研究生产力差异,从而为该主题提供新的见解。低代码技术已清楚地显示出更高的生产力水平,为低代码在短期/中期内主导软件开发主流提供了强有力的论据。本文报告了程序和协议、结果、局限性和未来研究的机会。


Ivar Jacobson, Alistair Cockburn - 用例至关重要
虽然软件行业是一个快节奏且令人兴奋的世界,在这个世界中,新的工具、技术和方法不断涌现,以服务于商业和社会,但它也是健忘的。在它急于求成的发展步伐中,它容易受到时尚潮流的变幻莫测的影响,并且可能会忘记或忽视那些针对它所面临的一些长期存在的、行之有效的解决方案。用例,最早于 1986 年被引入,并在之后得到普及,正是这些行之有效的解决方案之一。





© 版权所有。

© . All rights reserved.