当前位置: 首页 > 科技观察

用RAID分析作为架构的驱动力

时间:2023-03-20 02:14:02 科技观察

一、寻找架构的驱动力自从人类学会了用智慧来擦亮眼睛去观察世界,就明白观察事物不应该停留在表面上现象,但必须看透本质。只有通过本质规律对世界进行建模,才能以“一”推论万物。所有的演绎过程都是为了寻找某种驱动力作为分析或建构的出发点。例如,当我们要分析一个运动的物体如何形成轨迹时,我们需要找到产生运动的力,包括初始功率、重力、摩擦力以及其他可能干扰物体运动的力。有些力会推动物体向前,比如初始力和与运动方向一致的力;有些力会阻碍物体的运动,例如摩擦力或空气阻力。通过分析这些力的方向和量度,可以大致描述出物体可能的运动轨迹。软件系统的复杂性远远超过物体的运动模型(当然,从确定论的角度来看,软件可能比物体的运动更简单),但是推导过程是相似的,因为一个软件系统不完全独立存在,但在更大的生态环境中,包括客户需求和体验、上游依赖系统、下游依赖系统、硬件和网络环境、团队技能水平等诸多因素纵横交错,或明或暗地影响着软件的走向建筑学。这些影响因素相当于影响“软件”对象运动的力。建筑师的工作就是从这些如蜘蛛网般错综复杂的力量中,敏锐地梳理出清晰的脉络。所谓“力”,其实是一种比喻。虽然观察软件系统的视角像万花筒一样五颜六色,但从“物理力学”的角度来分析架构似乎更为准确和直观。软件系统就像一个物体,在各种影响下不断变化(移动)。不同的影响因素会决定架构师的设计决策,这些决策之间相互影响,或相互吸引或相互排斥,绝不能孤立地看待。因此,架构分析和设计成为对软件系统影响的识别,而这种设计的驱动力就是我们所说的RAID分析。2、RAID分析法所谓RAID分析法就是识别软件系统的风险(Risk)、假设(Assumption)、问题(Issue)和依赖性(Dependency)。在JohnKlein中,DavidWeiss写道:软件架构师最关心的不是系统的功能。......你专注于需要满足的品质。质量问题指定了必须如何交付功能才能被系统利益相关者接受,他们的既得利益包含在系统的结果中。这里所谓的“质量”就是我们常说的质量属性(QualityAttribute)。对于架构师来说,业务需求带来的设计复杂度的增加只是量变;而质量属性对设计的要求可能随着复杂性的增加而产生质的变化。以分布式系统为例,随着消息队列、分布式存储、服务通信和集成的引入,在数据一致性、可靠性、安全性、运维管理等方面,由此产生的复杂度是不一样的单机系统的设计挑战和难度几乎随着规模呈指数级增长。系统的复杂性可能是无限的,但人力是有限的。当我们开始软件系统的构建和设计时,难免会有一些考虑。如果我们不掌握合理的设计方法,陷入各种需求的汪洋大海和各种利益相关者的纠缠中,我们就有可能迷失、迷茫,或者做出不适合自己的设计决策现在的情况。RAID分析在一定程度上可以帮助我们找回正确的方向,尤其是在处理质量属性方面,还是比较有效的。我的建议是以workshop的形式进行RAID分析,召集团队成员通过头脑风暴来完成。由于将所有软件系统可能面临的问题划分为RAID的四类,明确了讨论的范围和类别,让参会者参与的思路更加收敛,思路更加清晰。一个典型的RAID分析结果如下图所示:在进行RAID分析之前,我们需要先弄清楚这四个概念的区别。3.风险和问题风险(Risk)和问题(Issue)经常被混淆在一起,但两者在概念上是相关的。风险实际上是未来可能出现的问题。在软件设计的过程中,我们一直在未来与现实之间徘徊。满足现实,更需要预知未来。然而,未来是不可预测的,所有的预测其实都是一种想象;我们用花言巧语谈论预测未来,但我们只是在想象未来。于是,现实与未来开始了一场痛苦的拉锯战。我们既不能过多地预测和判断未来,但也不能只满足于现状。如何做到合适的架构设计,同时避免过度设计,也让我们的架构在未来的需求变化时,能够以最小的成本做出响应。我们真正想做的是展望未来。评估风险是让我们展望未来的镜子(这个世界上没有预测未来的魔法水晶球)。分析存在的问题,评估未来的风险,将是这场拉锯战的关键制高点。在确定优先级时,问题往往高于风险,需要在解决现有问题的前提下考虑未来风险的解决方案。比如系统目前存在的问题是性能堪忧,那么除了必要的调优手段外,我们还可以通过提高系统的可扩展性来提升性能。但为保证系统的可扩展性,需要保持服务无状态,在系统各层设计时支持水平扩展,这可能会引入数据不一致和系统不稳定的风险。4.Assumptions我们往往会忽略给予系统的假设(Assumption),但实际上,这样的假设往往代表着关键的架构约束。架构限制是一个非常重要的驱动因素。RoyFielding在他的论文架构风格和基于网络的软件架构设计(《架构风格与基于网络的软件架构设计》)中概述了约束的重要性:特性来自架构中的一组约束。约束通常是由软件工程原则在架构元素的某些方面的应用所驱动的。例如,统一的管道和过滤器样式通过在其组件接口上应用通用性原则,从应用程序中获取组件的可重用性和可配置性——强制组件实现单一接口类型的特性。因此,架构约束是由通用性原则驱动的“统一组件接口”,以实现两种理想的品质,当在架构中实现时,这两种品质将变得可重用和可用。配置组件的架构属性。当我们澄清假设时,我们需要将这些约束确定为架构设计的驱动力。例如,对于一款手机APP,我们明确假设用户在断网的情况下可以正常查看个人信息和产品信息。这种假设对软件架构做了约束,即APP客户端需要缓存数据信息,当用户连接WIFI时,客户端数据可以自动同步到服务端。某些假设是系统功能的重要协议,如合同,需要在整个设计和实施阶段遵循。例如,假设电商系统需要调用的推荐系统是第三方系统,那么需要明确推荐系统对外暴露的接口,系统之间如何集成,客户应该如何响应推荐系统的服务发生了变化。这些都直接影响我们的设计决策。5.依赖在软件设计中,我们无时无刻不在与依赖作斗争。依赖本身既不好也不坏。关键在于我们如何分解(内聚)和如何合作(耦合)。这就是我们需要遵循的高内聚低耦合的设计原则。在架构层面,情况更为复杂。除了系统内部的依赖,还需要考虑系统外部的上下游依赖。特别是,跨越物理边界的通信(可以看作是一个过程)将直接影响许多质量属性,例如可靠性、性能和可扩展性。DDD的ContextMap定义了九个BoundedContext之间的映射关系,包括防腐层、开放主机服务和发布语言之间的集成关系,用BoundedContext来表达。如果我们能够在架构中识别出系统的依赖关系,再结合Cockburn提出的六边形架构,更直观的形象化,找出依赖的端口(Ports)和适配器(Adapters),进而确定依赖关系。它们之间的通信(集成)方式几乎可以画出整个软件系统的应用逻辑架构和物理架构的雏形。下图结合了六边形架构和标识的依赖关系:6.实现RAID分析的案例在多系统的架构设计或者Inception阶段,我使用RAID分析的方法来驱动系统的软件架构设计,以及效果还是比较不错的,虽然在细节上还有些不足,但是从大局着手可以帮助我们从高层次上对整个系统进行分析和架构。下面是某个版本升级系统的RAID分析案例。7.评估风险一般来说,风险的识别可以引导我们思考系统质量的属性,利益相关者可以充分表达他们对这些属性的关注,从而驱使我们寻找解决方案。1.稳定性在这次RAID分析中,一些利益相关者明确提出了对稳定性的担忧。系统的多个模块驻留在不同节点,部分模块嵌入主控板。由于业务需要,模块之间的通信比较频繁,主要通信协议有Telnet和SSH。从旧系统的性能来看,跨界点之间的通信在稳定性方面并不好。基于这个问题,我们在后续的架构设计中进行了深入分析。除了保证通信实现本身的健壮性和异常处理,我们还决定在主控板侧设计一个粗粒度的接口。传递版本升级所需的信息,减少不必要的沟通。2.可扩展性风险可扩展性的识别帮助我们建立了一个架构原则,即版本规范包的结构不应该影响主控板的系统。这是因为主控板系统的版本升级是最受限制的,我们不希望产品变更时影响到整个版本管理系统。3、性能当升级的系统数量较多时,系统版本升级过程会变慢。但业务需求要求系统不能长时间处于关机状态,否则会增加运营成本。因此,升级过程通常选择在凌晨进行,要求在较短的时间内完成整个升级工作,因此性能是重中之重。我们考虑使用并发的方式对每个待升级的系统进行升级。升级过程是一个独立的过程,但涉及到更复杂的业务流程和跨节点通信。由于部署限制,后台只能部署在一个JVM上,升级业务通过开启多个并发线程来处理。升级时需要将配置文件加载到内存中。如果同时启动的线程过多,可能会出现OutOfMemory异常。这一风险的识别及时为我们敲响了警钟。我们为此安排了技术秒杀,以便找到合适的配置项,在性能和可靠性之间做出最佳权衡。8.清晰的假设假设可以是关键的架构约束或系统功能约定。架构限制既可以是设计的阻力,也可以是动力。经过讨论,我们基本确定了两个最重要的假设:系统必须支持双向兼容。这个假设的提出要求在开发过程中只要接口已经发布,我们就不能修改接口。除了修复错误,我们不能删除旧功能,只能添加新功能。即使旧功能已被新功能取代,我们也无法将其删除以保持兼容性,但我们可以将其标记为@deprecated。版本升级过程中,如果前后操作存在依赖关系,则必须保证事务的一致性,要么全部成功,要么全部失败。事实上,这种假设也是对质量属性“可靠性”的回应。九、分析问题整个RAID的识别是针对技术层面的,而不是管理层面的。因此,我们发现的问题也仅限于技术范围。在我们发现的问题中,最致命的一个与模块NVUM的加载有关。NVUM是一个JAR包。它不是一个独立的系统,而是由管理系统动态加载的。选择动态加载而不是静态依赖的原因包括:NVUM是我们项目组维护的,而管理系统是另一个项目的,双方的版本计划完全不一致。网管系统是客户端-服务器系统,比较成熟,已经独立部署到全球多个现场站点。如果使用静态依赖,我们需要将它们纳入网管系统。但是NVUM的版本更新比较频繁,现场不可能因为NVUM的一个模块的调整而付出频繁更新管理系统的代价。管理系统负责监控现场各设备的运行状态。系统重启(耗时数十分钟)虽然不会影响设备的功能,但在重启过程中,由于无法控制设备的状态,可能无法及时发现问题。必须避免此类事故。也就是说,管理系统的重启成本太高,不能频繁重启。JAR包的动态加载可以通过URLClassLoader实现,也可以选择OSGI。前者需要充分验证其稳定性,而后者太重,成本太高。另外,动态加载方式对模块设计有设计约束,即我们需要将NVUM分为interface和impl两个模块,并且必须保证接口的稳定性。另一种选择是使用脚本,例如选择可以在JVM上运行的Groovy脚本语言。我们只需要在Java中调用Groovy提供的GroovyShell即可直接读取groovy脚本文件;然后调用run()方法来执行脚本。10、识别依赖关系除了NVUM与管理系统、NVUM与主控板、主控板与其他设备的依赖关系外,还涉及到很多其他的依赖关系。有些依赖于输入,而另一些则依赖于输出。此外,还有版本控制工具等系统也受到NVUM的影响。同时,NVUM还需要访问内置的文件系统,通过FTP读取很多外部文件。通信可以使用Telnet、SNMP、SSH和其他协议。识别这些依赖关系有助于确定系统对其他系统的可能影响。预先识别有助于我们及时沟通。同时,需要就一些架构约定和接口定义达成一致。依赖关系的识别也有助于我们设计系统的物理架构,并考虑系统将如何部署。【本文为专栏作家“张艺”原创稿件,转载请联系原作者】点此阅读更多该作者好文