第十二章 非抽象大系统介绍设计

由于负责生产运营和产品工程,SRE工程师处于使业务用例需求和操作成本保持一致的独特地位。产品工程团队可能不知道他们设计的系统的维护成本,特别是当产品团队正在构建一个单独的组件从而产生更大的生态系统时这种可能性更会增加。

基于谷歌开发系统的经验,我们认为可靠性是任何生产系统的最关键特性。我们发现延迟可靠性设计过程中的问题类似于以更高的成本换取更少的特性。通过遵循作为系统设计和实现的迭代风格,我们得到了健壮的易于升级的低运营成本的系统设计。我们称之为非抽象大系统设计(NALSD)。

什么是NASLD?

这一章介绍了NALSD方法:我们从问题陈述开始,收集需求,而且使迭代设计变得越来越复杂直到我们找到一个可行的解决方案。最终,我们针对多种失效模式得出了一个稳定的系统设计方法,满足我们在迭代时的初始要求和附加条件。

NALSD描述了SRE的关键技能:评估、设计和评估大型系统的能力。实际上,NALSD将容量规划、组件隔离和优雅的系统降级等元素组合在一起,这些元素对生产系统的高可用性至关重要。谷歌SRE被希望能够从系统的基本白板图开始资源规划,考虑各种扩展和故障领域,并将其设计集中到资源的具体建议中。因为这些系统会随着时间的推移而变化,所以SRE能够分析和评估系统设计的关键方面是至关重要的。

为什么非抽象

所有系统最终都必须在使用真实网络的真实数据中心的真实计算机上运行。谷歌已经认识到(艰难的)设计分布式系统的人员需要开发并不断地锻炼将白板设计转换为过程中多个步骤的具体资源估计的能力。如果没有这种严格性,创建在现实世界中不能完全转换的系统就太有诱惑力了。

这种额外的前期工作通常会减少最后一刻的系统设计更改,以应对一些不可预见的物理约束。

请注意,当我们将这些练习推进到离散结果(例如,机器的数量)时,声音推理和假设的例子比任何最终值都重要。早期的假设对计算结果有很大的影响,而做出完美的假设并不是NALSD的必要条件。这个练习的价值在于将许多不完美但合理的结果结合在一起,从而更好地理解设计。

AdWords的例子

谷歌AdWords服务在谷歌Web搜索中显示文本广告。点击率(CTR)指标告诉广告商他们的广告表现有多好。CTR是点击广告的次数与显示广告的次数之比。

这个AdWords示例旨在设计一个能够为每个AdWords ad测量和报告准确的CTR的系统。我们需要计算CTR的数据记录在搜索和ad服务系统的日志中。这些日志分别记录每个搜索查询显示的广告和点击的广告。

设计过程

谷歌使用迭代的方法来设计满足我们目标的系统。每次迭代都定义一个潜在的设计,并检查它的优缺点。这种分析要么提供给下一个迭代,要么指出什么时候设计足够好以至于被推荐。

总的来说,NALSD过程有两个阶段,每个阶段有两到三个问题。

在基本设计阶段,我们试图设计出一种符合原则的设计。我们问两个问题:

这个是可能的吗?
这个设计可能吗?如果我们不需要担心足够的内存、CPU、网络带宽等等,我们会设计什么来满足需求呢?

我们可以做的更好吗?
对于这样的设计,我们会问:“我们能做得更好吗?”例如,我们能让这个系统有意义地更快、更小、更高效吗?如果设计在O(N)时间内解决了这个问题,我们能否更快地解决这个问题,比如O(ln(N))?

在下一个阶段中,我们尝试扩展我们的基本设计—例如,通过显著地增加需求。我们问三个问题:

它是可行的吗?
在资金、硬件等方面受到限制的情况下,是否有可能扩展这种设计?如果有必要,什么样的分布式设计才能满足需求?

它是弹性的吗? 设计能优雅地失效吗?当这个组件失效时会发生什么?当整个数据中心失效时,系统如何工作?

我们如何做的更好?
虽然我们通常以大致的顺序来讨论这些阶段和问题,但在实践中,我们在问题和阶段之间来回切换。例如,在基本的设计阶段,我们经常在我们的头脑中有成长和扩展。有了这些概念,让我们来回顾一下NALSD迭代过程。

初始需求

每个广告客户可能有多个广告。每个广告都由ad_id键入,并与广告商选择的搜索词列表相关联。

当向广告客户展示仪表盘时,我们需要知道以下每个信息:

  • 这个搜索词触发这个广告的频率是多少
  • 看到广告的人点击了多少次

有了这些信息,我们可以计算CTR:点击次数除以查看次数。

我们知道广告商关心两件事:仪表盘能否快速展示并且数据是最新的。因此,在迭代设计时,我们将根据SLOs来考虑我们的需求(详见第2章):

  • 99.9%的仪表板查询在< 1秒内完成
  • 99.9%的时间,显示的CTR数据少于5分钟。

这些SLOs提供了一个合理的目标,我们应该能够始终如一地满足。

他们还提供了错误预算(参见站点可靠性工程中的第4章),我们将在每次设计迭代中对我们的解决方案进行比较。

我们的目标是创建一个能够满足我们的SLOs的系统,并支持数以百万计希望在仪表盘上看到他们点击率的广告客户。对于事务率,我们认为每秒有500,000个搜索查询和10,000个广告点击。

一台机器时

最简单的起点是考虑在一台计算机上运行整个应用程序。

对每个搜索查询,我们记录如下信息:

1
2
3
4
5
6
7
8
time:
       查询发生的时间
query_id:
       唯一的查询标识符(查询ID)
search_term:
       查询的内容
ad_id:
       搜索显示的所有AdWords广告的id

这些信息一起形成查询日志。用户每次点击广告时,我们都会记录的时间、查询ID和单击日志中的ad_id。

你可能想知道为什么我们不简单地将search_term添加到单击日志中以减少复杂性。在我们提供的例子任意缩减的范围内,这可能是可行的。然而,在实践中,CTR实际上只是从这些日志中计算出的许多结论之一。单击日志来自url, url具有固有的大小限制,这使分离查询日志成为一个更可扩展的解决方案。我们不会通过在练习中添加额外的ctr-like需求来证明这一点,而是简单地承认这个假设并继续前进。

仪表盘展示需要来自两个日志的数据。我们需要保证,我们可以实现我们所展示SLO数据的更新时间在一秒钟以内。要实现这种SLO,需要在系统处理大量单击和查询时保持计算CTR的速度不变。

为了满足在一秒钟内显示仪表盘所要求SLO数据,我们需要快速查找每个给定ad_id的search_term中被单击和显示的query_id的数量。我们可以从查询日志中提取每个search_term和ad_id所显示的query_id的详细信息。CTR仪表盘需要来自ad_ids的查询日志和单击日志的所有记录。

如果我们有更多的广告商,扫描查询日志和单击日志来生成仪表盘的效率会非常低。因此,我们的设计要求我们的一台机器创建一个合适的数据结构,以便在接收日志时能够进行快速的CTR计算。在一台机器上,使用具有query_id和search_term索引的SQL数据库应该可以在一秒钟内提供答案。通过在query_id上连接这些日志并按search_term分组,我们可以报告每个搜索的CTR。

计算

我们需要计算需要多少资源来解析所有日志。为了确定缩放限制,我们需要做一些假设,从查询日志的大小开始:

1
2
3
4
5
6
7
8
9
10
time:
      64位整数,8个字节
query_id:
      64位整数,8字节
ad_id:
      3个64位整数,8字节
search_term:
      一个长度为500字节的字符串
其它元数据:
   500 - 1000字节的信息,比如哪台机器提供广告,搜索用的哪种语言,以及搜索词返回的结果

为了确保我们不会过早地达到一个阈值,我们将每个查询日志条目整理为2kb。单击日志卷应该比查询日志卷小得多:因为平均CTR为2%(10,000次单击/ 500,000次查询),所以单击日志将拥有2%与查询日志一样多的记录。请记住,我们选择了大数字来说明这些原则可以实现任意大的扩展。这些估计似乎很大,因为它们本应该如此。

最后,我们可以使用科学的表示法来限制由单位上的不一致引起的算术错误。24小时内生成的查询日志量为:

1
(5 × 105 queries/sec) × (8.64 × 104 seconds/day) × (2 × 103 bytes) = 86.4 TB/day

因为我们收到的点击量是查询的2%,而且我们知道数据库索引将增加一些合理的开销,所以我们可以将86.4 TB/天的存储空间增加到100 TB,以存储一天的日志数据。 由于总存储需求约为100 TB,我们需要作出一些新的假设。 这种设计是否仍适用于单台机器? 虽然有可能 将100 TB的磁盘连接到一台机器上,但是我们可能会受到机器从磁盘读取和写入磁盘的能力限制。

例如,常见的4 TB HDD可能能够支持每秒200次输入/输出操作(IOPS)。 如果每个日志条目可以存储并以每个日志条目平均一个磁盘写入为索引,我们会看到IOPS是我们查询日志的限制因素:

1
    (5 × 105 queries/sec) / (200 IOPS/disk) = 2.5 × 103 disks or 2,500 disks

即使我们可以以10:1的比例批量查询以限制操作,但在最佳情况下,我们需要数百个HDD。 考虑到查询日志写入只是设计IO要求的一个组成部分,我们需要使用比传统HDD更好地处理高IOPS的解决方案。

为简单起见,我们将直接评估RAM并跳过评估其它存储介质,例如固态磁盘(SSD)。单台机器无法完全在RAM中处理100TB的占用空间:假设我们的标准机器占用空间为16核,64 GB RAM和1 Gbps网络吞吐量,我们需要:

1
    (100 TB) / (64 GB RAM/machine) = 1,563 machines

评估

暂时忽略我们的计算并想象我们可以将这个设计放在一台机器上,我们真的想要这样实现吗? 如果我们通过询问当该组件发生故障时会发生什么来测试我们的设计,我们会找出一长串单点故障列表(例如,CPU,内存,存储,电源,网络,冷却)。 如果其中一个组件出现故障,我们是否可以合理地支持我们的SLO? 几乎可以肯定的是,即使是一个简单的电力循环也会对我们的用户产生极大影响。

回到我们的计算,我们的单机设计看起来是不可行,但这一步并不浪费时间。我们已经发现了有关如何推理系统约束及其初始要求的有价值的信息。我们需要将设计发展为使用多台机器。

分步式系统

我们需要的search_terms位于查询日志中,ad_ids位于单击日志。 既然我们知 道我们需要多台机器,那么什么是连接它们的最佳设计?

MapReduce

我们可以使用MapReduce处理和连接日志。 我们可以定期获取累积的查询日志和单击日志,MapReduce将生成由ad_id组织的数据集,显示每个search_term接收到的单击次数。

MapReduce作为批处理器工作:它的输入是一个大型数据集,它可以使用许多机器通过进程处理该数据并产生结果。一旦所有机器处理完他们的数据,他们的输出就可以合并 - MapReduce可以直接为每个AdWords广告和搜索字词创建每个CRT的摘要。我们可以使用此数据创建我们需要的dashboard。

评价

MapReduce是一种广泛使用的计算模型,我们相信它可以横向扩展。 无论我们的查询日志和单击日志输入有多大,添加更多的机器总是能够在不耗尽磁盘空间或RAM的情况下成功完成流程。

不幸的是,这种类型的批处理过程无法在收到日志的5分钟内满足我们的加入日志可用性的SLO。 要在5分钟内提供结果,我们需要小批量运行MapReduce作业 —— 每次只需几分钟的日志。

批次的任意和非重叠性质使小批量不切实际。 如果已记录的查询在批处理1中,并且其单击位于批处理2中,则单击和查询将永远不会被连接。 虽然MapReduce可以很好地处理独立批次,但它并未针对此类问题进行优化。 此时,我们可以尝试使用MapReduce找出可能的解决方法。 然而,为简单起见,我们将继续研究另一种解决方案。

LogJoniner

用户点击的广告数量明显小于所投放广告的数量。 直观地说,我们需要专注于扩展两者中的较大者:查询日志。 我们通过引入新的分布式系统组件来实现此目的。

而不是像我们的MapReduce设计那样小批量查找query_id,如果我们创建了一个所有查询的存储,我们可以按需查询query_id,该怎么办? 我们称之为QueryStore。 它包含查询日志的完整内容,query_id为主键。为了避免重复,我们假设我们从单个机器设计的计算将应用于QueryStore,我们将QueryStore的审查限制为我们已经涵盖的内容。 有关这样的组件如何工作的深入讨论,我们建议您阅读有关Bigtable的内容。

因为点击日志也有query_id,所以我们的处理循环的规模比现在要小得多:它只需要遍历点击日志并引入所引用的特定查询。 我们将此组件称为LogJoiner。

如果我们没有找到点击查询(接收查询日志的速度可能会减慢),我们将其搁置一段时间并重试,直到时间限制。 如果我们在该时间限制内找不到查询,我们会放弃该点击日志。

点击率仪表盘对每个ad_id和search_term需要两个组件:展示次数和点击的广告数量。 ClickMap需要合作伙伴来保存查询,由ad_id组织。 我们称之为QueryMap。 QueryMap直接从查询日志中提供所有数据,并通过ad_id索引条目。

图12-1描述了数据如何流经系统。

LogJoiner设计引入了几个新组件:LogJoiner,QueryStore,ClickMap和QueryMap。 我们需要确保这些组件可以扩展。

图12-1 基本的LogJoiner设计; 处理并存储点击数据,以便仪表板可以检索它

计算

根据我们在之前的迭代中执行的计算,我们知道QueryStore将为一天的日志提供大约100 TB的数据。 我们可以删除太旧而不具有价值的数据。

LogJoiner应该在进入时处理点击并检索相应的点击从QueryStore的日志。

1
    (104 clicks/sec) × (2 × 103 bytes) = 2 × 107 = 20 MB/sec = 160 Mbps

QueryStore查找会产生额外的网络开销。 对于每个单击日志记录,我们查找query_id并返回完整的日志记录:

1
2
    (104 clicks/sec) × (8 bytes) = 8 × 104 = 80 KB/sec = 640 Kbps
    (104 clicks/sec) * (2 × 103 bytes) = 2 × 107 = 20 MB/sec = 160 Mbps

LogJoiner还会将结果发送到ClickMap。 我们需要存储query_id,ad_id和time。 search_term、time和query_id都是64位整数,因此数据将小于1 KB:

1
    (104 clicks/sec) × (103 bytes) = 107 = 10 MB/sec = 80 Mbps

总计约400 Mbps是我们机器可管理的数据传输速率。

ClickMap必须为每次单击存储时间和query_id,但不需要任何其他元数据。我们将忽略ad_id和search_term,因为它们是一个小的线性因素(例如,广告商数量×广告数量×8字节)。 即使是拥有10个广告的1000万广告客户也只有约800 MB。 一天的ClickMap值是:

1
2
    (104 clicks/sec) × (8.64 × 104 seconds/day) × (8 bytes + 8 bytes) 
    = 1.4 × 1010 = 14 GB/day for ClickMap

我们将ClickMap最多为20 GB /天,以计算任何开销和我们的ad_ids。

在我们填写QueryMap时,我们需要为显示的每个广告存储query_id。我们需要增加存储空间,因为每个搜索查询可能会点击三个ad_id,因此我们需要记录query_id 最多三个条目:

1
2
    3 × (5 × 105 queries/sec) × (8.64×104 seconds/day) × (8 bytes + 8 bytes) 
    = 2 × 1012 = 2 TB/day for QueryMap

2 TB足够小,可以使用硬盘驱动器托管在一台机器上,但我们从单机迭代中知道,单个小写操作太频繁将无法存储在硬盘驱动器上。 虽然我们可以计算使用更高IOPS驱动器(例如SSD)的影响,但我们的工作重点是证明系统可以扩展到任意大的尺寸。在这种情况下,我们需要围绕单个机器的IO限制进行设计。因此,缩放设计的下一步是对输入和输出进行分片:将传入的查询日志和单击日志分成多个流。

Sharded LogJoiner

我们在此迭代中的目标是运行多个LogJoiner实例,每个实例位于数据的不同分片上。 为此,我们需要考虑几个因素:

数据管理
为了加入查询日志和单击日志,我们必须将每个单击日志记录与query_id上的相应查询日志记录进行匹配。 该设计应该防止网络和磁盘吞吐量在我们扩展时限制我们的设计。

可靠性
我们知道机器可能随时出现故障。 当一台机器运行LogJoiner时故障了,我们如何确保我们不会失去正在进行的工作?

效率
我们可以做扩容而没有浪费吗? 我们需要使用最少的资源满足我们的数据管理和可靠性需求。

我们的LogJoiner设计表明我们可以加入查询日志和点击日志,但产生的数据量非常大。 如果我们将工作划分为基于query_id的分片,我们可以并行运行多个LogJoiner。

提供了合理数量的LogJoiner实例,如果我们均匀地分发日志,则每个实例仅通过网络接收一小撮信息。 随着点击流量的增加,我们通过添加更多LogJoiner实例来水平扩展,而不是通过使用更多的CPU和RAM来垂直扩展。

如图12-2所示,为了使LogJoiners收到正确的消息,我们引入了一个名为日志分片器的组件,它将每个日志条目定向到正确的目的地。 对于每条记录,我们的点击日志分片器执行以下操作:

  1. 哈希记录的query_id。
  2. 用N(分片数)模数结果并加1以得到介于1和N之间的数字。
  3. 在第2步中将记录发送到分片编号。

图12-2 分片应该如何工作)

现在,每个LogJoiner将获得分解的传入日志的一致子集 query_id,而不是完整的点击日志。

QueryMap也需要进行分片。 我们知道需要很多硬盘来支持QueryMap所需的IOPS,并且一天的QueryMap(2TB)的大小对于我们的64 GB机器来说太大而无法存储在RAM中。 但是,我们将在ad_id上进行分片,而不是像LogJoiner那样使用query_id进行分片。 ad_id在任何读取或写入之前都是已知的,因此使用与LogJoiner和CTR仪表盘相同的散列方法将提供一致的数据视图。

为了保持实现的一致性,我们可以将ClickMap的相同日志分片设计重用为QueryMap,因为ClickMap比QueryMap小。

现在我们知道我们的系统将扩展,我们可以继续解决系统的可靠性问题。 我们的设计必须能够适应LogJoiner故障。 如果LogJoiner在收到日志消息后但在加入日志消息之前失败,则必须重做其所有工作。 这会延迟准确数据到达仪表板,这将影响我们的SLO。

如果我们的日志分片器进程将重复的日志条目发送到两个分片,则系统可以继续全速执行并处理得到准确的结果,即使LogJoiner失败(可能是因为它所在的机器失败)。

通过以这种方式复制工作进程,我们减少(但不消除)丢失这些连接日志的机会。 两个分片可能会同时中断并丢失中继日志。 通过分配工作负载以确保在同一台计算机上没有重复的分片,我们可以减轻大部分风险。 如果两台机器同时发生故障并且我们丢失了分片的两个副本,则系统的错误预算(参见第一本SRE手册中的第4章)可以涵盖剩余的风险。 当灾难发生时,我们可以重新处理日志。 仪表板将仅显示短时间内超过5分钟的数据。

图12-3显示了我们对分片及其副本的设计,其中LogJoiner、ClickMap和QueryMap都构建在两个分片上。

从连接的日志中,我们可以在每个LogJoiner机器上构建ClickMap。 要显示我们的用户仪表盘,需要组合和查询所有ClickMaps。

结论

在一个数据中心中托管分片组件会产生单点故障:如果刚好不幸一对机器或数据中心断开连接,我们将丢失所有ClickMap工作,并且用户仪表盘完全停止工作!我们需要改进我们的设计以使用多个数据中心。

图12-3。 使用相同的query_id对日志进行分片以复制分片)

多数据中心

跨不同地理位置的数据中心复制数据使我们的服务基础架构能够承受灾难性故障。如果一个数据中心关闭(例如,由于电源或网络中断),我们可以故障转移到另一个数据中心。要使故障转移起作用,必须在部署系统的所有数据中心中提供ClickMap数据。

这样的ClickMap甚至可能存在吗?我们不希望将计算需求乘以数据中心的数量,但我们如何才能有效地同步站点之间的工作以确保充分复制而不会产生不必要的重复?

我们刚刚描述了分布式系统工程中众所周知的共识问题的一个例子。有许多复杂的算法可以解决这个问题,但基本思路是:

  1. 一个服务要有三个或五个副本(如ClickMap)。
  2. 用像Paxos等一致性算法,以确保在发生数据中心级别故障时我们可以可靠地存储计算状态。
  3. 在参与节点之间实现至少一个网络往返时间以接受写入操作。此要求限制了系统的顺序吞吐量。基于一致性map仍然需要并行写操作。

按照刚刚列出的步骤,多数据中心设计现在看来原则上是可行的。它还能在实践中发挥作用吗?我们需要哪些类型的资源,以及我们需要多少资源?

计算

使用故障隔离数据中心执行Paxos算法的延迟意味着每个操作大约需要25毫秒才能完成。该延迟的假设基于至少相距几百公里的数据中心。因此,就顺序过程而言,我们每25毫秒只能执行一次操作或每秒40次操作。如果我们需要每秒执行10次连续进程(点击日志),我们需要为每个数据中心至少250个进程(由ad_id分片)用于Paxos操作。在实践中,我们希望添加更多进程以提高并行性,以便能处理任何宕机或流量高峰所造成的积压。

基于我们之前对ClickMap和QueryMap的计算,并使用每秒40次连续操作来估算,我们的多数据中心设计需要多少台新机器?

因为我们的分片LogJoiner设计为每条日志记录引入了一个副本,所以我们每秒创建ClickMap和QueryMap的事务数量翻了一番:每秒20,000次点击和每秒1,000,000次查询。

我们可以通过把每秒总查询数除以每秒最大操作数来计算所需的最小进程数或任务数 :

1
    (1.02 × 106 queries/sec) / (40 operations/sec) = 25,500 tasks

每个任务的内存量(2 TB QueryMap的两个副本):

1
    (4 × 1012 bytes) / (25,500 tasks) = 157 MB/task

每台机器的任务数:

1
    (6.4 × 1010 bytes) / (1.57 × 108 bytes) = 408 tasks/machine

我们知道我们可以在一台机器上安装许多任务,但我们需要确保我们不会达到IO的瓶颈。 ClickMap和QueryMap的总网络吞吐量(使用每个条目2 KB的高估计):

1
    (1.02 × 106 queries/sec) × (2 × 103 bytes) = 2.04 GB/sec = 16 Gbps

每项任务的吞吐量:

1
    16 Gbps / 25,500 tasks = 80 KB/sec = 640 Kbps/task

每台机器的吞吐量:

1
    408 tasks × 640 Kbps/task = 256 Mbps

对于每个任务15MB内存和640Kbps吞吐量的组合是易于管理的。我们在每个数据中心需要大约4 TB的RAM来托管分片的ClickMap和QueryMap。 如果我们每台机器有64 GB的RAM,我们可以只使用64台机器处理数据,这将会只使用每台机器25%的网络带宽。

评估

现在我们已经设计了一个多数据中心的系统,让我们来看看数据流是否有意义。

图12-4显示了整个系统设计。 您可以查看每个搜索查询和广告点击是如何与服务器进行通信的,以及如何收集日志并将其推送到每个组件中。

我们可以根据我们的需求检查这个系统:

每秒10,000次广告点击
LogJoiner可以水平扩展以处理所有日志点击,并将结果存储在ClickMap中。

每秒500,000次搜索查询
QueryStore和QueryMap被设计为能够以此速率存储一整天的数据。

99.9%的仪表盘查询在<1秒内完成
CTR仪表板从QueryMap和ClickMap获取数据,这些数据由ad_id键入,使此事务变得快速而简单。

99.9%的时间,显示的CTR数据不到5分钟
每个组件都设计为水平扩展,这意味着如果管道处理太慢,添加更多计算机将减少端到端管道延迟。

我们相信该系统架构可以扩展以满足我们对吞吐量,性能和可靠性的要求。

图 12-4 多数据中心设计

总结

NALSD描述了Google用于生产系统的系统设计的迭代过程。 通过将软件分解为逻辑组件并将这些组件放入具有可靠基础架构的生产生态系统中,我们得出了对数据一致性、系统可用性和资源效率都能达到合适目标的系统。NALSD的实践使我们能够在不重复每次迭代的情况下改进设计。虽然本章中提出的各种设计迭代都满足了我们原始的问题陈述,但每次迭代都揭示了新的要求,我们可以通过扩展我们以前的工作来满足这些要求。

在整个过程中,我们根据我们对系统增长的预期来分离软件组件。 该策略允许我们独立地扩展系统的不同部分,并消除对单个硬件或单个软件实例的依赖性,从而产生更可靠的系统。

在整个设计过程中,我们通过对NALSD的四个关键问题进行提问来持续改进每次的迭代:

可能吗?
我们可以在没有“魔法”的情况下建造它

我们可以做得更好吗?
它是否像我们可以合理地制造一样简单?

这可行吗?
它是否符合我们的实际限制(预算,时间等)?

它有弹性伸缩的吗?
它会偶尔存在不可避免的中断吗?

NALSD是一项有学问的技能。与任何技能一样,您需要定期练习以保持熟练程度。 谷歌的经验表明,从抽象需求推理到具体的资源是建立健康和长寿命系统的关键。