























在80年代末和90年代初长大的我,对电脑的接触几乎仅限于游戏机(我认为是Atari 800和Commodore 64游戏机,因为我只看到过在它们上面运行的游戏)或早期的X86系统。直到2000年我上了大学,我才掌握了一台Sun Microsystems SPARC工作站、UNIX和Slackware Linux,我可以在家里的Intel 486机器上安装。
那时,软件开发主要是指在你的机器上运行的软件,或者,如果你有机会的话,在共享时间的计算机上运行的软件,其处理能力明显高于你,可以……做商业相关的事情。在大学里,我记得听说过一个计算机科学家使用的程序,它需要一个多核处理器来生成数千个学生的课程表;生成和打印课程表需要数周时间。直到今天,我仍然不确定哪个时间更长 — 程序的运行和打印到纸上。
今天,大多数正在开发的软件要么在云上运行,要么在需要访问云的设备上运行,要么为同样在云上运行的其他软件提供动力。在密闭空间内工作的软件系统(如嵌入式软件系统),如果不能在其他地方获得更强大的计算平台,那是非常罕见的。会计系统现在压缩了大量的数据,这些数据被托管在公司内部或外部的数据仓库的服务器群中。现在,销售系统的客户关系由第三方管理,其插件由更多的第三方或内部开发人员开发。
但是,今天这些软件系统是如何建立的,以服务于数以百计到数以百万计的用户,同时还能保持我们今天使用的软件所期望的性能和响应性?
作为一个有20年之久的软件工程师,我见过许多系统从堆栈的各个层面被开发出来。从DOS时代的中断处理程序到JavaScript驱动的动画,甚至是无代码的报告生成。几周前,我甚至让ChatGPT-4根据我给它的一些描述来生成一些我想要的Python代码!但这是另一个故事!但这是另一个故事了。
在这篇文章中,我写的是系统设计,它如何成为现代软件工程实践的关键部分,以及它将是人类软件工程师在中短期内仍能提供价值的关键领域之一。
很久以前,我是一家公司的软件工程师,这家公司在处理他们自己带来的成功的负荷方面存在问题。我把这家公司称为Friendster。当我加入这家公司时,我所负责的项目已经很晚了,而且有许多与内存管理有关的错误。他们的核心服务(是的,在2007年我们这样称呼它之前,它是一个微服务)是用C++编写的,但有内存泄漏,处理请求的时间太长,而且被设计为在自己的内存中缓存和提供数据。它需要是无状态的,但最后却变成了有状态的。
在项目进行了几周后,我恳求高级工程领导层放弃这个服务的迭代,转而从头开始写一些符合要求的东西;这将是对现有实现的一个直接替换。我们有一个最后期限,因为该服务只能再处理几个月的增长,然后它就不能再以重新水化的方式处理缓存的大小。
重新启动服务所花的时间比它在内存泄漏之前所能保持的时间还要长。这是一个 “赌上我的职业生涯 “的时刻,但我几乎没有任何机会。我们必须让它运转起来。
系统设计开始了。我们做的第一件事是列出系统必须满足的要求,在依赖服务(PHP前端代码)和这个核心服务之间的合同是什么,以及一个关于我们如何满足三个关键的非技术要求的计划:性能、效率和弹性。
系统设计涉及到了解系统必须执行其功能的约束条件,所需的功能是什么,以及相对于所有其他属性而言,系统的哪些属性是重要的。一旦你有了这些定义,你就可以开始设计一个符合要求的系统,并系统地规划出解决方案的交付。
当我们谈及系统设计时,通常有几个组成部分,这需要的:
首先要了解一些事情,比如:系统是自成一体的(即,将不会访问外部资源),还是分布式的?它是否会有一个用户界面,或者是非交互式的(例如,它是否会生成一份打印出来的报告,或者它在运行过程中是否需要人类或其他系统的输入)?它是否需要处理大量的流量?它是要在任何时候只有十个人使用,还是要在任何时候有一千万个用户使用它?
一旦你对其中一些问题有了答案,通过系统设计原则做出决定就会更容易。
在这个现代社会中,设计软件系统的几个关键原则直到系统需要扩展时才完全出现 — 从一个单用户系统到一个应该能够同时处理成千上万甚至数百万用户的系统。以下是我们将在本文中介绍的一些内容:
当一个系统可以在资源成比例增长的情况下被部署来处理负载的增长时,它就是可扩展的。一个系统的扩展系数被定义为服务于系统负载增长所需的资源量的增长。我们在软件系统中会遇到两种典型的扩展情况:垂直扩展和水平扩展。
垂直扩展是指为软件系统提供更多的空间或单机资源来处理需求的增长。考虑一下网络附加存储设备的情况。你通过设备提供的存储越多,它能存储的数据就越多。如果你需要它处理更多的并发连接和I/O操作(IOPs),你通常需要添加更多的计算能力和网络接口来处理增加的负载。
横向扩展是指用软件的副本复制一个系统或多台机器,以处理需求的增长。考虑一下隐藏在负载均衡器后面的静态网络内容服务器的情况。增加更多的服务器可以让更多的客户连接并从网络服务器上下载内容,当负载减弱后,网络服务器的数量可以缩减到适合当前需求的规模。
有些系统可以处理混合或对角线的扩展。例如,一些分布式数据库架构允许分割计算和存储节点,以便计算量大的工作负载可以使用具有更多计算资源的节点。相反,IOPs的重度工作负载可以在存储+计算节点上运行。例如,流处理应用程序可能会分离出需要更多内存和计算的工作负载(例如,事件源或分析工作负载),并适当地扩展这些工作负载,并独立于IOPs的重型工作负载(例如,压缩和归档)。
当一个系统能够容忍部分故障和恢复而不严重降低服务质量时,它就是可靠的。系统可靠性的一部分包括其操作的可预测性,即延迟、吞吐量和遵守商定的操作范围。
确保系统可靠性的通常方法包括以下几个方面:
制作可靠的系统需要记住的关键一点是,以一种定义明确的方式处理潜在的故障,使依赖系统能够做出反应。这意味着如果有输入可能导致系统对所有人都可用,那么它就不是一个可靠的系统。同样地,如果系统依赖于另一个可能不可靠的系统,那么它应该用策略来处理不可靠的问题,以确保可靠性。
当以相应的努力来改变一个系统,并以最小的用户干扰来部署时,这个系统是可维护的。这就要求在实施系统的时候,假定需求会发生变化,并且系统有足够的灵活性来处理可预见的方向变化。这也意味着要确保代码的可读性,以便下一组维护者(可能是同一个团队,但在未来用新的眼光来看待它)能够维护软件,并使其进化以满足未来的需求。
没有人希望被卡在维护那些僵化的、难以改变的、没有良好组织的、文档化程度低的、设计不良的、未经测试的、胡乱拼凑的软件。
确保代码质量高是卓越工程的一部分,体现了专业精神和优秀的工艺。这不仅是一件好事,而且众所周知,它可以让高功能和高性能的工程团队提供可以改变和扩展的软件,以持续提供价值。
如果你的服务不可用,它就可能不存在。
系统设计应该解决一个系统应该如何保持可用性,以保持对客户和系统用户的相关性。这意味着:
在我职业生涯的早期,我了解到,一个不稳定和不可用的系统有时会成为失去客户信任的最大原因。一旦你失去了客户的信任,就很难重新获得信任。
系统设计应该把安全作为一个关键环节来解决,特别是在互联网连接系统的时代,安全威胁和漏洞会对我们的客户和系统的使用者造成实际伤害。构建安全软件的目标并不是要达到完美,而是要了解漏洞和攻击所涉及的风险。拥有一个适当的安全威胁模型和一个系统的方法来理解风险所在,以及哪些类型的威胁值得优先考虑和设计缓解措施,是安全设计和工程实践的开始。
今天,随着我们的软件系统成为现代社会更多部分的关键任务服务的一部分,安全不再是可有可无的了。在我们设计的系统中,从一开始就认真对待安全问题,使我们更接近于能够更好地依赖我们建立和部署的软件,以满足我们用户的需求。赢得客户的信任已经很不容易了,只需要一个漏洞就可以失去很好的一部分信任。
鉴于以上几个方面,现代分布式系统的一些模式已经出现,以不同的方式解决了这些方面的一些问题。让我们来探讨一下我们今天看到的关于系统设计的五个方面的一些比较流行的设计模式。
随着分布式系统的兴起,其重点是通过冗余建立可靠性和规模,通过横向扩展建立效率和性能,以及通过将系统的部分解耦为独立运行的服务来建立弹性,”微服务 “一词通过实现以下几点而得到普及:
通过我们的方面来看,微服务有吸引人的特性,如果适用于用例的话,这使它成为一个好的模式:
微服务是分解大型应用的一个好方法,在这里可以确定需要自己的扩展和可靠性域的逻辑分区。不过,当从头开始时,从一开始就设计微服务是不太理想的,因为有可能将服务分解成太小的碎片。微服务之间的通信成本 — 通常为HTTP或gRPC请求 — 是很重要的,只有在必要时才应该产生。确定功能是否适合于一个服务的一个好方法是遵循领域驱动设计或功能分解这样的做法。
如同在基于微服务的解决方案中,使用无服务器实现进一步将服务请求的关键功能部分委托给底层基础设施。如果在微服务中,服务是由一个持久化进程提供的,那么无服务器解决方案通常只实现一个入口点来处理对端点的请求(通常是通过HTTP或gRPC的URI)。在无服务器部署中,没有实际的服务器被配置,而是由部署环境根据需要启动资源,以处理进来的请求。有时,这些资源会停留一段时间,以摊销启动它们的成本,但这只是一个实施细节。
让我们来看看系统设计的各个方面,看看无服务器解决方案是如何叠加的:
无服务器解决方案,或功能即服务,是一种非常有吸引力的方式,通过关注业务逻辑和价值,让底层基础设施处理服务的可扩展性、可靠性和可用性,来进行原型设计甚至部署生产级解决方案。这是一个典型的起点,可以让一个具有最小运营负担的解决方案启动和运行,对于大多数原型来说,这是证明我们假设的好方法。这也是一个典型的经验,一旦这些解决方案达到了扩展的极限,与运行这些相关的成本就变得足够高。这些都变成了根据所需规模调整的更优化的微服务实现。
然而,有些问题领域不需要在线交易处理,而微服务和无服务器的实现并不完全符合要求。考虑到可以在后台或在有资源的情况下处理事务的情况。另一种情况是后台处理活动,其结果不一定是互动的。
事件驱动的系统遵循的模式是有一个事件源和事件汇,事件(消息)分别来自和被发送。处理是由订阅者和发布者分别对这些源和汇进行的。事件驱动系统的一个例子是一个聊天机器人,它可以参与许多对话(事件源和汇),并在它们进来时处理消息。
分布式事件驱动系统可以有多个并发的消息处理程序等待相同的源,可能会发布太多的汇,作为其他消息处理程序的源。这种通过汇和源将处理器连锁起来的模式被称为事件管道。通常情况下,汇和源有一个单一的实现,提供一个消息队列接口,并根据通过系统的消息需求来进行扩展。许多分布式队列管理系统也可以有效地从对角线扩展中受益,比如Apache Kafka、RabbitMQ等。
让我们通过我们的五个方面来研究分布式事件驱动系统:
现代软件工程需要设计可扩展、可靠、可维护、可用和安全的系统。设计分布式系统需要非常严格的要求,因为现代系统的现实复杂性随着社会对更好的软件服务的需求而增长。我们回顾了分布式系统的三种现代设计模式,并研究了设计良好的系统的五个方面。
作为软件工程师,我们有责任设计系统,解决现代分布式系统的关键问题。
在本系列的下一篇文章中,我将写到测试和它在现代软件工程中的作用。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。