DDIA 第一部分-2定义非功能性需求

在你的需求列表中,最重要的可能是应用程序必须提供的功能:需要哪些界面和按钮,以及每个操作应该做什么,以实现软件的目的。这些是你的 功能性需求

此外,你可能还有一些 非功能性需求:例如,应用程序应该快速、可靠、安全、合规,并且易于维护。这些需求可能没有明确写下来,因为它们看起来有些显而易见,但它们与应用程序的功能同样重要。

案例研究:社交网络首页时间线

表示用户、帖子与关注关系

假设我们的社交网络必须支持的主要读取操作是 首页时间线,它显示你关注的人最近发布的帖子。我们可以编写以下 SQL 查询来获取特定用户的首页时间线:

sql 复制代码
SELECT posts.*, users.* FROM posts

JOIN follows ON posts.sender_id = follows.followee_id

JOIN users ON posts.sender_id = users.id

WHERE follows.follower_id = current_user

ORDER BY posts.timestamp DESC

LIMIT 1000

帖子应该是及时的,所以假设在某人发布帖子后,我们希望他们的粉丝能够在 5 秒内看到它。一种方法是让用户的客户端每 5 秒重复上述查询(这称为 轮询)。如果我们假设有 1000 万用户同时在线登录,这意味着每秒运行 200 万次查询。即使增加轮询间隔,这也是很大的负载。

时间线的物化与更新

首先,与其轮询,不如服务器主动向当前在线的任何粉丝推送新帖子。其次,我们应该预先计算上述查询的结果,以便可以从缓存中提供用户的首页时间线请求。

这种方法的缺点是,现在每次用户发布帖子时我们需要做更多的工作,因为首页时间线是需要更新的派生数据。当一个初始请求导致几个下游请求被执行时,我们使用术语 扇出 来描述请求数量增加的因子。

以每秒 5,700 条帖子的速率,如果平均帖子到达 200 个粉丝(即扇出因子为 200),我们将需要每秒执行超过 100 万次首页时间线写入。与我们本来需要的每秒 4 亿次每个发送者的帖子查找相比,这仍然是一个显著的节省。即使在这种负载峰值期间,时间线仍然可以快速加载,因为我们只是从缓存中提供它们。

这种预先计算和更新查询结果的过程称为 物化 ,时间线缓存是 物化视图 的一个例子。

描述性能

大多数关于软件性能的讨论都考虑两种主要的度量类型:

**响应时间,**从用户发出请求到收到请求应答所经过的时间。测量单位是秒(或毫秒,或微秒)。

**吞吐量,**系统正在处理的每秒请求数,或每秒数据量。对于给定的硬件资源分配,存在可以处理的 最大吞吐量。测量单位是"每秒某物"。

吞吐量和响应时间之间通常存在联系;当请求吞吐量较低时,服务具有较低的响应时间,但随着负载增加,响应时间也会增加。这是因为 排队:当请求到达高负载系统时,CPU 很可能已经在处理先前的请求,因此传入请求需要等待先前请求完成。随着吞吐量接近硬件可以处理的最大值,排队延迟急剧增加。

如果系统接近过载,吞吐量被推到极限附近,它有时会进入恶性循环,变得效率更低,从而更加过载。例如,如果有很长的请求队列等待处理,响应时间可能会增加到客户端超时并重新发送请求的程度。这导致请求率进一步增加,使问题变得更糟------重试风暴。即使负载再次降低,这样的系统也可能保持过载状态,直到重新启动或以其他方式重置。这种现象称为 亚稳态故障(Metastable Failure),它可能导致生产系统的严重中断。

为了避免重试使服务过载,你可以在客户端增加并随机化连续重试之间的时间(指数退避),并暂时停止向最近返回错误或超时的服务发送请求(使用 熔断器或 令牌桶 算法)。服务器还可以检测何时接近过载并开始主动拒绝请求(负载卸除),并发送响应要求客户端减速(背压)。排队和负载均衡算法的选择也可能产生影响。

复制代码
以上是一些常用过载应对策略,记录一下。

如果吞吐量可能会增长超出当前硬件可以处理的范围,则需要扩展容量;如果系统的最大吞吐量可以通过添加计算资源显著增加,则称系统为 可伸缩的。

延迟与响应时间

  • 响应时间 是客户端看到的;它包括系统中任何地方产生的所有延迟。
  • 服务时间 是服务主动处理用户请求的持续时间。
  • 排队延迟 可能发生在流程中的几个点:例如,在收到请求后,它可能需要等待直到 CPU 可用才能被处理;如果同一台机器上的其他任务通过出站网络接口发送大量数据,响应数据包可能需要在发送之前进行缓冲。
  • 延迟 是一个涵盖请求未被主动处理时间的总称,即在此期间它是 潜在的。特别是,网络延迟 或 网络延迟 指的是请求和响应在网络中传输所花费的时间。

平均值、中位数与百分位数

图 2-5. 说明平均值和百分位数:100 个服务请求的响应时间样本。

响应时间指标的应用

图 2-6. 当需要几个后端调用来服务请求时,只需要一个慢的后端请求就可以减慢整个最终用户请求。

百分位数通常用于 服务级别目标(SLO)和 服务级别协议(SLA),作为定义服务预期性能和可用性的方式。

可靠性与容错

我们可以将 可靠性 大致理解为"即使出现问题也能继续正确工作"。为了更准确地说明出现问题,我们将区分 故障 和 失效。

**故障,**故障是指系统的某个特定 部分 停止正确工作:例如,如果单个硬盘驱动器发生故障,或单台机器崩溃,或外部服务(系统所依赖的)发生中断。

**失效,**失效是指 整个 系统停止向用户提供所需的服务;换句话说,当它不满足服务级别目标(SLO)时。

容错

如果系统在发生某些故障时仍继续向用户提供所需的服务,我们称系统为 容错的。如果系统不能容忍某个部分变得有故障,我们称该部分为 单点故障(SPOF),因为该部分的故障会升级导致整个系统的失效。

在这种容错系统中,通过故意触发故障来 增加 故障率是有意义的------例如,在没有警告的情况下随机杀死单个进程。这称为 故障注入。许多关键错误实际上是由于错误处理不当造成的;通过故意引发故障,你确保容错机制不断得到锻炼和测试,这可以增加你对故障自然发生时将被正确处理的信心。混沌工程 是一门旨在通过故意注入故障等实验来提高对容错机制的信心的学科。

硬件与软件故障

在大规模系统中,硬件故障发生得足够频繁,以至于它们成为正常系统运行的一部分。

通过冗余容忍硬件故障

当组件故障独立时,冗余最有效,即一个故障的发生不会改变另一个故障发生的可能性。我们在本书中讨论的容错技术旨在容忍整个机器、机架或可用区的丢失。它们通常通过允许一个数据中心的机器在另一个数据中心的机器发生故障或变得不可达时接管来工作。

能够容忍整个机器丢失的系统也具有运营优势:如果你需要重新启动机器(例如,应用操作系统安全补丁),单服务器系统需要计划停机时间,而多节点容错系统可以一次修补一个节点,而不影响用户的服务。这称为 滚动升级。

软件故障

这种故障比不相关的硬件故障更难预料,并且它们往往导致比硬件故障更多的系统失效。

软件中的系统故障没有快速解决方案。许多小事情可以帮助:仔细考虑系统中的假设和交互;彻底测试;进程隔离;允许进程崩溃和重新启动;避免反馈循环,如重试风暴;测量、监控和分析生产中的系统行为。

人类与可靠性

一项对大型互联网服务的研究发现,操作员的配置更改是中断的主要原因,而硬件故障(服务器或网络)仅在 10-25% 的中断中发挥作用。

越来越多的组织正在采用 无责备事后分析 的文化:事件发生后,鼓励相关人员充分分享发生的事情的细节,而不用担心惩罚,因为这允许组织中的其他人学习如何在未来防止类似的问题发生。这个过程可能会发现需要改变业务优先级、需要投资于被忽视的领域、需要改变相关人员的激励措施,或者需要引起管理层注意的其他一些系统性问题。

越来越多的组织正在采用 无责备事后分析 的文化:事件发生后,鼓励相关人员充分分享发生的事情的细节,而不用担心惩罚,因为这允许组织中的其他人学习如何在未来防止类似的问题发生。这个过程可能会发现需要改变业务优先级、需要投资于被忽视的领域、需要改变相关人员的激励措施,或者需要引起管理层注意的其他一些系统性问题。

复制代码
更多的时候这是个管理问题

可伸缩性

可伸缩性 是我们用来描述系统应对负载增加能力的术语。讨论可伸缩性意味着考虑诸如以下问题:

  • "如果系统以特定方式增长,我们有什么选择来应对增长?"
  • "我们如何增加计算资源来处理额外的负载?"
  • "基于当前的增长预测,我们何时会达到当前架构的极限?"

描述负载

通常这将是吞吐量的度量:例如,对服务的每秒请求数、每天到达多少千兆字节的新数据,或每小时购物车结账的数量。数据库中的读写比率、缓存的命中率或每个用户的数据项数量。

如果你可以将资源翻倍以处理两倍的负载,同时保持性能不变,我们说你有 线性可伸缩性,这被认为是好事。

复制代码
通常是边际收益递减

共享内存、共享磁盘与无共享架构

无共享架构 (也称为 横向伸缩 或 向外扩展)已经获得了很大的流行。在这种方法中,我们使用具有多个节点的分布式系统,每个节点都有自己的 CPU、RAM 和磁盘。节点之间的任何协调都在软件级别通过传统网络完成。

无共享的优点是它有线性伸缩的潜力,它可以使用提供最佳性价比的任何硬件。它可以随着负载的增加或减少更容易地调整其硬件资源,并且它可以通过在多个数据中心和地区分布系统来实现更大的容错。缺点是它需要显式分片,并且它会产生分布式系统的所有复杂性

可伸缩性原则

可伸缩性的一个良好通用原则是将系统分解为可以在很大程度上相互独立运行的较小组件。这是微服务背后的基本原则、分片、流处理和无共享架构。然而,挑战在于知道在哪里划分应该在一起的事物和应该分开的事物之间的界限。

另一个好原则是不要让事情变得比必要的更复杂。如果单机数据库可以完成工作,它可能比复杂的分布式设置更可取。自动伸缩系统(根据需求自动添加或删除资源)很酷,但如果你的负载相当可预测,手动伸缩的系统可能会有更少的操作意外。具有五个服务的系统比具有五十个服务的系统更简单。良好的架构通常涉及多种方案的务实混合。

复制代码
事件驱动的微服务编排和业务逻辑编排框架

可维护性

如果我们今天创建的每个系统都足够有价值以长期生存,它有一天将成为遗留系统。为了最小化需要维护我们软件的未来几代人的痛苦,我们应该在设计时考虑维护问题。尽管我们不能总是预测哪些决定可能会在未来造成维护难题。

**可运维性(Operability)**使组织容易保持系统平稳运行。自动化是必不可少的。然而,自动化可能是一把双刃剑: 总会有边际场景(如罕见的故障场景)需要运维团队的手动干预。由于无法自动处理的情况是最复杂的问题,更大的自动化需要一个 熟练的运维团队来解决这些问题 。

**简单性(Simplicity)**通过使用易于理解、一致的模式和结构来实施它,并避免不必要的复杂性,使新工程师容易理解系统。陷入复杂性的软件项目有时被描述为 大泥团。

推理复杂性的一种尝试是将其分为两类,本质复杂性偶然复杂性 。 这个想法是,本质复杂性是应用程序问题域中固有的,而偶然复杂性仅由于我们工具的限制而产生。 不幸的是,这种区别也有缺陷,因为本质和偶然之间的边界随着我们工具的发展而变化。我们管理复杂性的最佳工具之一是 抽象。应用程序代码的抽象,旨在降低其复杂性,可以使用诸如 设计模式和 领域驱动设计(DDD)等方法创建。

**可演化性(Evolvability)**使工程师将来容易对系统进行更改,随着需求变化而适应和扩展它以用于未预料的使用场景。在组织流程方面,敏捷 工作模式为适应变化提供了框架。敏捷社区还开发了在频繁变化的环境中开发软件时有用的技术工具和流程, 例如测试驱动开发(TDD)和重构。

你可以修改数据系统并使其适应不断变化的需求的容易程度与其简单性及其抽象密切相关:松耦合、简单的系统通常比紧耦合、复杂的系统更容易修改。 由于这是一个如此重要的概念,我们将使用一个不同的词来指代数据系统级别的敏捷性:可演化性。

复制代码
DDD TDD 设计模式下的事件驱动的微服务编排和业务编排框架。
相关推荐
mCell6 小时前
为什么 Memo Code 先做 CLI:以及终端输入框到底有多难搞
前端·设计模式·agent
阿里巴巴淘系技术团队官网博客8 小时前
设计模式Trustworthy Generation:提升RAG信赖度
人工智能·设计模式
牛奶10 小时前
《前端架构设计》:除了写代码,我们还得管点啥
前端·架构·设计
苏渡苇11 小时前
Java + Redis + MySQL:工业时序数据缓存与持久化实战(适配高频采集场景)
java·spring boot·redis·后端·spring·缓存·架构
麦聪聊数据12 小时前
如何用 B/S 架构解决混合云环境下的数据库连接碎片化难题?
运维·数据库·sql·安全·架构
2的n次方_12 小时前
CANN HCOMM 底层架构深度解析:异构集群通信域管理、硬件链路使能与算力重叠优化机制
架构
技术传感器12 小时前
大模型从0到精通:对齐之心 —— 人类如何教会AI“好“与“坏“ | RLHF深度解析
人工智能·深度学习·神经网络·架构
小北的AI科技分享13 小时前
万亿参数时代:大语言模型的技术架构与演进趋势
架构·模型·推理
一条咸鱼_SaltyFish16 小时前
从零构建个人AI Agent:Node.js + LangChain + 上下文压缩全流程
网络·人工智能·架构·langchain·node.js·个人开发·ai编程
码云数智-园园16 小时前
解决 IntelliJ IDEA 运行 Spring Boot 测试时“命令行过长”错误
架构