acm-header
登录

ACM通信

实践

用建模解决架构复杂性


说明模型

图片来源:Richard Almond

回到顶部

现代计算机日益强大的能力使得解决曾经被认为难以解决的问题成为可能。然而,用于这些功能复杂的问题空间的系统往往具有过于复杂的体系结构。在这里,我使用术语体系结构指的是系统的整体宏观设计,而不是个别部分如何实现的细节。系统体系结构是可用功能的幕后内容,包括内部和外部通信机制、组件边界和耦合,以及系统将如何利用任何底层基础设施(数据库、网络等)。架构是这个问题的“正确”答案:这个系统是如何工作的?

问题是:如何应对理解或更好地防止系统复杂性的挑战?许多开发方法(例如,Booch1)考虑非功能方面,但他们往往停留在图表阶段。“我们以后可以解决[性能、可伸缩性等]”的咒语可能会造成严重后果。系统中的单个组件(应用程序)通常可以迭代,但由于所有接口和基础设施的影响,迭代体系结构通常要困难得多。

在本文中,我将描述在着手创建新系统时的体系结构设计方法。但如果系统已经以某种形式存在了呢?我的很多体系结构工作都是作为被邀请(或派去)评估和改进系统状态的“局外人”与现有系统进行过多次接触。当处理复杂系统时,这些任务可能相当具有挑战性。

对现有系统建模的一个优点是,一般行为已经到位,所以您不会从空白状态开始。您可能也不必为创建系统的功能部分而苦恼。然而,这是有代价的。系统的架构很有可能是复杂的,而且不容易理解。此外,由于系统检修的高成本,许多解决方案可能并不实际。

对于任何类型的系统,目标都是尽可能多地理解体系结构和系统行为。当一个大型系统已经存在多年时,这似乎是一个巨大的努力。有许多技术可以用来发现系统的工作原理和改进方法。您可以询问开发和维护团队的成员。诊断工具(例如,DTrace)可以帮助快速查找系统中的性能或可伸缩性问题。您可以梳理堆积如山的日志文件,查看开发人员认为值得注意的内容。在本文中,我将重点讨论如何对各种系统组件进行建模,以获得更好的理解,并为评估可能的更改提供基础。

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

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

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

回到顶部

建模早期的Windows系统

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

套件一开始只是一个概念验证,但原型演示进行得非常顺利,所以匆忙投入生产。当我加入这个团队时,它已经有大约150名代表。随着程序开始整天使用,问题开始频繁发生。这些问题表现为多种形式:内存泄漏、访问违规、虚假错误消息和机器锁定(也称为冻结)。

我们的小团队忙于增加功能,以满足快速增长的愿望列表,同时解决稳定性问题。我们在源代码中导航,攻击内存泄漏和访问违规。我们努力追踪不断增长的新观察到的错误消息列表。最具挑战性的任务是“冻结巡逻”,我们花了大量时间寻找那些机器锁定。问题在于,我们对Windows的幕后工作原理并没有真正深入的了解。

那些熟悉早期Windows sdk编程的人会记得,文档(更不用说稳定性了)开发得并不好。API函数的级别很低,看起来有很多很多。(如果不是查尔斯·佩佐德编程窗口2我不确定在20世纪80年代有多少Windows应用程序是在微软之外开发出来的。)应用程序的代码库已经相当大了,至少在当时是这样,而且每个应用程序的实现都略有不同(毕竟它们都是原型)。微软提供了一些示例程序,但都无法与这些应用程序的复杂性相提并论。因此,我们决定构建模仿我们试图实现的Windows行为的组件(应用程序)。

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

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

建模的结果是,我们获得了关于这种新颖的Windows技术的足够的信息,以改变程序,使其稳定性成为合理的期望。在15个月内,该系统被部署到4500多个工作站上,并在Windows NT的生命中很好地存活了下来。

回到顶部

建模一个“从属”系统

并不是我所有的模特经历都有这样积极的结果。有几个暴露了架构设计的基本缺陷,有几个的唯一选择是放弃系统重新开始。这些信息通常不会被项目管理人员很好地接受。

一个比较值得注意的例子发生在一个打算作为“奴隶”的系统中,该系统从多个现有系统接收更新,并将它们应用到一个新数据库中。其他新系统将使用该数据库作为取代旧系统的基础。这些系统将使用一种新的技术平台来建造。技术如此不同,功能范围如此广泛,以至于仅从系统的开发团队就超过了60人。

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

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

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

建模结果并不令人鼓舞。延迟将是一个挑战,但总体吞吐量需求将埋没系统。我们开始探索解决性能问题的方法。系统已经瞄准了所选平台的最快硬件,所以这个选项已经不存在了。我们推迟了对提高单个功能处理器性能的研究;这被认为是更昂贵的,因为已经写的数字。我们认为,将重点放在公共基础设施上,可以增加我们快速成功的机会。


如何应对理解系统复杂性的挑战,或者更好地防止系统的复杂性?


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

鉴于结果仍然很差,我们需要检查功能算法和数据库访问。我们使用了一些中等和较长的运行处理器,并插入了一些日志记录,以获得各个步骤的分割时间。由于数据映射和重构所需的复杂性,许多函数式算法都相对昂贵。数据库操作花费的时间似乎比我们逻辑上认为的要长。(随着时间的推移,架构师应该根据他或她以前最大化性能的类似功能的抽象视图,发展出一种性能预算的概念。)

然后我们检查了逻辑数据库模型。这种设计不是一种适合系统中程序类型的性能模式。提取了一些算法的SQL,并将其放在独立的模型组件中。这个想法是为了看看哪些类型的性能提升是可能的。一些增加来自于更改一些SQL语句,这占用了过多的时间,因为所选的分区方案意味着读取核心表通常涉及扫描所有分区。随着模拟数据库规模的增长,这对可伸缩性是不利的。然而,主要的问题不是单个语句的时间延长,而是调用的数量太多。这是正常化走得太远的结果。有许多表,列上的索引经常变化。此外,使用多列键代替人工键(有时称为代理键)。 The system generates them (typically as integers) to represent the "real" keys. This can improve performance and maintenance when dealing with complex key structures and/or when the actual key values can change.

我们确定,如果重新构造数据库设计并更改相关的SQL语句,就有可能实现重大改进。然而,程序是用这样一种方式编写的,这将使更改非常昂贵。我们的结论是,如果这个系统想要成功,就需要进行一次大的改革。由于这个项目已经花费了1000多万美元,这个建议很难说服别人。

在获得额外的500万美元资金后,这个项目被取消了,我的团队将注意力转移到了其他工作上。建模过程只花了大约六周的时间。这里要说明的一点是,在提交大型支出之前,可以使用建模来审查主要的体系结构决策。如果发现一个设计无法执行或无法伸缩,那么成本将大大降低之前系统是建立起来的,而不是在它投入生产之后。

回到顶部

对新系统建模

研究新系统的体系结构选项,或者在对现有系统进行实质性的大修时,应该成为标准的实践。实验应该使用轻量级模型而不是完整的系统,但是这些模型准确地捕获系统的演化行为是至关重要的。否则,建模过程的价值就会降低,并可能导致错误的结论。

我通常从尝试以抽象的方式理解功能问题空间开始。主要功能是一个用户请求的操作,后面跟着一个系统应答(例如,请求/应答)吗?它是一个请求后的通知流(例如,滴答的引号)还是位(例如,音乐或视频)?是处理一些输入数据并将结果发送到另一个进程或系统(例如,流)吗?是通过大量数据集搜索信息(决策支持系统)?是这些因素的结合,还是完全不同的原因?


研究新系统的体系结构选项,或者在对现有系统进行实质性的大修时,应该成为标准的实践。


有些人可能会问:我如何知道要对系统的哪些部分建模,以及在这个过程中应该花费多少时间和精力?这是一个简单的风险管理案例。建模应该集中在出错代价最大的领域。这个过程应该继续下去,直到高风险的决定能够得到证明。尽可能多地对决策进行重新测试。

建模中最具挑战性的方面之一是在捕获足够的系统行为和防止模型变得过于复杂(和昂贵)而难以实现之间找到正确的平衡。对于现有的系统,这更容易。在您进行建模迭代的过程中,如果观察结果开始模拟系统的各个方面,那么您可能已经非常接近了。您可以开始更改建模驱动程序和组件,以探索更多的行为。对于一个新系统,我通常寻找可以用作实际组件外壳的模型组件。目标是为负责任的开发人员提供一个起点,允许他们将注意力集中在功能上,而不是不得不探索底层技术和基础结构的关键细微差别。

在设计或评估体系结构时,有许多技术模式需要考虑:性能、可用性、可伸缩性、安全性、可测试性、可维护性、易开发性和可操作性。这些模式的优先顺序在不同的系统中可能不同,但每一个都必须考虑到。如何处理这些模式及其相应的技术考虑可能因系统组成部分而异。例如,对于请求/应答和流更新,延迟是一个关键的性能因素,而吞吐量对于流消息处理或大容量请求功能可能是一个更好的性能因素。一个可能很微妙但仍然很重要的信息是避免在同一个组件中混合使用不同的模式实现。如果不能遵循这一教训,就会使体系结构走上一条通往复杂性的道路。

听到这样的借口太常见了:“系统(将)太大了,没有时间来建模它的行为。”我们只需要开始建设它。”如果认为建模的繁琐工作过于繁重,那么实现可预测的性能、可伸缩性和其他理想的技术属性可能会非常具有挑战性。一些开发项目非常关注单元测试,但以我的经验来看,很少发现相应的关注作为一个整体测试系统架构。

回到顶部

对示例组件建模

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

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

发布者模型组件应该使用建议的目标语言构建。它应该使用可能会影响模型结果的任何框架、库等,尽管其中哪一个可能会产生影响可能并不明显。在这种情况下,您应该采取一种风险管理方法,将那些对组件的操作来说是核心的内容包括进来。任何尚未完全理解行为的新技术也应该包括在内。任何可靠的基础结构都可以在以后的迭代中添加。重要的是不要过早地陷入试图构建功能的泥潭。尽可能多地消灭掉。

在某些系统中,发布服务器等组件可能是最大的可伸缩性障碍。在这种情况下,我们需要知道可以处理什么类型的消息流,可以预期什么类型的延迟,可以支持多少客户机,以及客户机应用程序可以处理什么类型的流。

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

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

下一步是评估使用驱动程序和接收器的发布者模型。为了描述性能,需要在客户机接收器中添加某种类型的检测来计算吞吐量。使用任何类型的仪器都必须小心,以免影响测试结果。例如,用时间戳记录收到的每条消息可能会影响性能。相反,可以将汇总统计信息保存在内存中,并定期或在测试结束时写入。

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

这个模型应该以不同的消息速率执行,包括完全超过发布者及其可用资源的速率。除了观察吞吐量和延迟之外,还应该分析系统资源利用率(CPU、内存、网络等)。这些信息稍后可以用于确定探索基础结构调优是否有可能的好处。

如前所述,当消息通过时,需要发布者进行某种类型的数据充实。吞吐量、延迟和内存消耗可能会受到这种充实的影响。应该估计这种影响,并将其纳入模型发布者中。如果无法获得现实的估计,则故意估计高(或遵循本文的哲学,构建另一个模型并对其进行描述)。如果充实的成本因消息类型而异,则可以在模型发布者中插入围绕预期平均值聚集的伪随机延迟和内存分配。

回到顶部

建模的其他用途

建模是一个迭代的过程。它不应该仅仅被认为是某种类型的性能测试。以下是可以添加到进一步评价过程中的项目列表。

  • 使用该模型来评估各种基础设施的选择。这些可能包括消息传递中间件、操作系统和数据库调优参数、网络拓扑和存储系统选项。
  • 使用该模型为一组硬件创建性能概要文件,并使用该概要文件推断其他硬件平台上的性能。如果模型在多个硬件平台上进行分析,那么任何外推都将更加准确。
  • 使用性能概要来确定随着系统的增长,是否可能需要发布服务器的多个实例(水平扩展)。如果是这样,应该将此功能构建到设计中并对其进行适当的建模。转换设计为单例的组件可能非常昂贵。
  • 使用该模型来探索可能的故障场景集。可用性是质量体系的主要属性之一。在系统构建完成后等待解决它可能会花费更多的数量级。

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

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

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

回到顶部

仪器仪表和维护

无论驱动程序/组件组合的类型是什么,仪器检测对于建模和系统的长期健康都是至关重要的。这不仅仅是一种奢侈品。不建议盲目地谈论性能。目视飞行规则(即没有仪器)只能在天空晴朗的情况下使用。在现代系统中,这种情况有多常见?功能和技术的复杂性通常会让人看不清发生了什么。系统性能可以像木筏顺流而下一样。如果你不定期观察水流的速度,那么你可能不会注意到即将到来的瀑布,直到木筏绝望地从边缘掉下去。如前所述,当检测数据量过大时,考虑使用“示踪器”和/或统计抽样。

随着系统的发展,保持驱动程序和模型组件的更新有很多好处:

  • 当提出更改时,它们可以用于性能、可用性或可伸缩性的一般回归测试。
  • 通过从更小的资源集推断性能,它们可以用于容量规划。要做到这一点,唯一实际的方法是充分理解资源使用特性。
  • 他们可以为基础设施或其他可能需要对现有系统进行的大规模更改建模。
  • 有时会有维护/开发团队无法控制的因素(例如,基础设施更改)。这些驱动程序可以用来测试系统的一个孤立部分。如果任何降级是由外部因素引起的,那么结果可以提供“防御”数据来更改或回滚更改。
  • 当出现某种类型的性能、可用性、可伸缩性或其他基础结构问题时,取出模型和驱动程序要比在排除生产问题的压力下承担可能繁重的更新它们的任务要快得多。

建模是理解和提高系统整体质量的一种非常强大的方法。对于预计使用数年的系统来说,这种改进转化为实际的资金节省。然后开发组织可以将他们的预算用于提供功能。如果模型和相关的驱动是持续的,那么这个功能焦点可以得到广泛的庆祝。

ACM队列的q戳相关文章
queue.acm.org

隐藏在显而易见的地方
布莱恩Cantrill
http://queue.acm.org/detail.cfm?id=1117401

可视化系统延迟
布伦丹·格雷格
http://queue.acm.org/detail.cfm?id=1809426

性能反模式
巴特Smaalders
http://queue.acm.org/detail.cfm?id=1117403

回到顶部

参考文献

1.Booch, G。面向对象分析和应用程序设计(第二版)。Benjamin Cummings, Redwood City, CA, 1993。

2.风格的作品,C。编程窗口。微软出版社,1988年。

回到顶部

作者

凯文Montagne:在处理性能和可用性非常关键的大型系统的IT领域有超过25年的经验。其中,他在金融行业工作了20年,其中有10多年是前台交易系统的架构师。

回到顶部

脚注

DOI: http://doi.acm.org/10.1145/1831407.1831424

回到顶部

数据

F1图1。带有驱动程序和接收器的发布者模型组件。

回到顶部


©2010 acm 0001-0782/10/1000 $10.00

允许为个人或课堂使用本作品的全部或部分制作数字或硬拷贝,但不得为盈利或商业利益而复制或分发,且副本在首页上附有本通知和完整的引用。以其他方式复制、重新发布、在服务器上发布或重新分发到列表,需要事先获得特定的许可和/或付费。

数字图书馆是由计算机协会出版的。版权所有©2010 ACM, Inc。


没有找到条目

Baidu
map