什么是软件工程?(非常详细)从零基础到精通,收藏这篇就够了!

编辑:Tom Manshreck

===================

没有什么是建立在石头上的;一切都建立在沙子上,但我们必须像建造在石头上一样去建造。Jorge Luis Borges

我们看到编程和软件工程之间有三个关键区别:时间、规模和权衡。在软件工程项目中,工程师需要更关注时间的流逝和最终的变更需求。在软件工程组织中,我们需要更关注规模和效率,既包括我们生产的软件,也包括生产软件的组织。最后,作为软件工程师,我们被要求做出更复杂的决策,其结果影响更大,通常基于对时间和增长的不精确估计。

在谷歌内部,我们有时会说,“软件工程是随时间积分的编程”。编程当然是软件工程的重要组成部分:毕竟,编程是你最初生成新软件的方式。如果你接受这种区分,也就清楚我们可能需要划分编程任务(开发)和软件工程任务(开发、修改、维护)。时间的加入为编程增加了一个重要的新维度。立方体不是正方形,距离不是速度。软件工程不是编程。

看待时间对程序影响的一种方式是思考这个问题:"你的代码的预期寿命是多长?"1这个问题的合理答案大约相差10万倍。认为代码需要持续几分钟和想象代码将存在几十年同样合理。通常,寿命较短的代码不受时间影响。对于实用期只有一小时的程序,你不太可能需要适应底层库、操作系统(OS)、硬件或语言版本的新版本。这些短期系统实际上"只是"一个编程问题,就像一个在一个维度上压缩得足够小的立方体是一个正方形一样。随着我们将时间延长以允许更长的寿命,变化变得更加重要。在十年或更长的时间跨度内,大多数程序依赖,无论是隐式的还是显式的,都可能发生变化。这种认识是我们区分软件工程和编程的根源。

这种区别是我们所说的软件可持续性的核心。如果在软件的预期寿命内,你有能力对任何有价值的变化做出反应,无论是出于技术原因还是业务原因,那么你的项目就是可持续的。重要的是,我们只关注能力 - 你可能会选择不执行给定的升级,要么是因为缺乏价值,要么是因为其他优先事项。2当你从根本上无法对底层技术或产品方向的变化做出反应时,你就在押注这种变化永远不会变得至关重要,这是一个高风险的赌注。对于短期项目,这可能是一个安全的赌注。但在几十年的时间里,可能就不是了。3

看待软件工程的另一种方式是考虑规模。有多少人参与其中?他们在长期的开发和维护中扮演什么角色?编程任务通常是个人创作的行为,但软件工程任务是团队努力的结果。早期尝试定义软件工程时产生了一个很好的定义:"多人开发多版本程序。"4这表明软件工程和编程的区别在于时间和人员。团队协作带来了新的问题,但也提供了比任何单个程序员更多的生产有价值系统的潜力。

团队组织、项目构成以及软件项目的政策和实践都主导着软件工程复杂性的这一方面。这些问题是规模固有的:随着组织的成长和项目的扩大,它在生产软件方面是否变得更有效率?随着我们的成长,我们的开发工作流程是否变得更有效率,还是我们的版本控制政策和测试策略花费我们更多的成本?围绕沟通和人员扩展的规模问题自软件工程早期以来就一直在讨论,一直可以追溯到《人月神话》。5这些规模问题通常是政策问题,是软件可持续性问题的基础:重复做我们需要做的事情将花费多少成本?

我们还可以说,软件工程与编程在需要做出的决策的复杂性及其影响方面有所不同。在软件工程中,我们经常被迫评估几种前进路径之间的权衡,有时风险很高,而且价值指标往往并不完善。软件工程师或软件工程领导者的工作是致力于组织、产品和开发工作流程的可持续性和扩展成本管理。考虑到这些输入,评估你的权衡并做出理性决策。我们有时可能会推迟维护更改,甚至采用扩展性不佳的政策,但要知道我们需要重新审视这些决定。这些选择应该明确并清楚地说明延迟的成本。

在软件工程中,很少有一刀切的解决方案,这本书也是如此。考虑到"这个软件将存活多长时间"的合理答案可能有100,000倍的差异,"你的组织有多少工程师"可能有10,000倍的范围,以及"你的项目可用的计算资源有多少"的未知因素,谷歌的经验可能与你的不匹配。在本书中,我们旨在介绍我们在构建和维护预期将持续数十年、拥有数万名工程师和全球范围计算资源的软件方面的经验。我们发现在这种规模下必要的大多数做法对于较小规模的项目也同样适用:可以将此视为一个我们认为在你扩大规模时可能有用的工程生态系统报告。在少数情况下,超大规模会带来自身的成本,我们更希望不必支付额外的开销。我们会将这些作为警告指出。希望如果你的组织发展到足以担心这些成本,你能找到更好的答案。

在我们讨论团队合作、文化、政策和工具的具体内容之前,让我们先详细阐述一下这些关于时间、规模和权衡的主要主题。

时间与变化

当新手学习编程时,所产生的代码的生命周期通常以小时或天为单位计算。编程作业和练习往往是一次性的,几乎没有重构,当然也没有长期维护。这些程序通常在初次生成后就不再重新构建或执行。这在教学环境中并不奇怪。也许在中等或高等教育中,我们可能会遇到团队项目课程或实践性论文。如果有的话,这样的项目可能是学生代码存活时间超过一个月左右的唯一时候。这些开发人员可能需要重构一些代码,也许是为了响应不断变化的需求,但不太可能要求他们处理环境的更广泛变化。

我们还在常见的行业环境中发现短期代码的开发人员。移动应用程序的生命周期通常相当短6,而且无论好坏,完全重写都相对常见。早期创业公司的工程师可能会正确地选择关注眼前目标而不是长期投资:公司可能活不到足以收获缓慢回报的基础设施投资的好处。一个连续创业的开发人员很可能有10年的开发经验,但几乎没有或根本没有维护任何预期存在超过一两年的软件的经验。

在另一端,一些成功的项目实际上有无限的生命周期:我们无法合理预测谷歌搜索、Linux内核或Apache HTTP服务器项目的终点。对于大多数谷歌项目,我们必须假设它们将无限期存在——我们无法预测何时不需要升级我们的依赖项、语言版本等。随着它们的生命周期增长,这些长寿项目最终会与编程作业或创业开发有不同的感觉。

考虑图1-1,它展示了两个处于这个"预期生命周期"谱的两端的软件项目。对于一个从事预期生命周期以小时计的任务的程序员来说,什么类型的维护是合理的?也就是说,如果在你编写一个只执行一次的Python脚本时出现了新版本的操作系统,你应该放下手头的工作去升级吗?当然不应该:升级并不重要。但在另一端,如果谷歌搜索停留在20世纪90年代的操作系统版本上,那显然是个问题。

图1-1. 生命周期和升级的重要性

低点和高点在预期寿命范围内表明存在某种转变。在一次性程序和持续数十年的项目之间的某个地方,发生了一个转变:项目必须开始对不断变化的外部因素做出反应。7 对于任何从一开始就没有计划升级的项目来说,这种转变可能会非常痛苦,原因有三个,每个原因都会加剧其他原因:

  • 你正在执行一项尚未为该项目完成的任务;更多隐藏的假设已经被内置。

  • 试图进行升级的工程师不太可能有这种任务的经验。

  • 升级的规模通常比平常更大,一次性完成几年的升级,而不是更渐进的升级。

因此,在实际经历过这样的升级后(或中途放弃),高估后续升级的成本并决定"再也不要"是相当合理的。得出这个结论的公司最终会承诺直接抛弃并重写他们的代码,或者决定永远不再升级。与其采取自然的方法来避免痛苦的任务,有时更负责任的答案是投资使其不那么痛苦。这完全取决于你的升级成本、它提供的价值以及相关项目的预期寿命。

不仅要完成第一次大规模升级,而且要达到能够可靠地保持最新状态的程度,这是项目长期可持续性的本质。可持续性需要规划和管理必要变更的影响。对于谷歌的许多项目,我们相信我们已经通过反复试验实现了这种可持续性。

那么,具体来说,短期编程与产生具有更长预期寿命的代码有何不同?随着时间的推移,我们需要更加意识到"碰巧可行"和"可维护"之间的区别。没有完美的解决方案来识别这些问题。这很不幸,因为保持软件的长期可维护性是一场持续的战斗。

海勒姆定律

如果你正在维护一个被其他工程师使用的项目,关于"它可行"与"它可维护"最重要的教训就是我们所称的海勒姆定律:

对于一个API,只要有足够多的用户,无论你在合同中承诺什么:你的系统的所有可观察行为都会被某人所依赖。

根据我们的经验,这个公理是任何关于随时间变化的软件的讨论中的主导因素。它在概念上类似于熵:关于随时间变化和维护的讨论必须意识到海勒姆定律8,就像关于效率或热力学的讨论必须注意熵一样。仅仅因为熵永远不会减少并不意味着我们不应该努力提高效率。仅仅因为在维护软件时会适用海勒姆定律,并不意味着我们不能为此做计划或试图更好地理解它。我们可以缓解它,但我们知道它永远无法根除。

海勒姆定律代表了一种实际知识——即使有最好的意图、最好的工程师和可靠的代码审查实践,我们也不能假设完全遵守已发布的合同或最佳实践。作为API所有者,通过明确接口承诺,你将获得一些灵活性和自由,但实际上,给定更改的复杂性和难度也取决于用户发现你的API的某些可观察行为有多有用。如果用户不能依赖这些东西,你的API就很容易更改。给定足够的时间和足够多的用户,即使是最无害的更改也会破坏某些东西;9 你对该更改价值的分析必须包含调查、识别和解决这些破坏的难度。

示例:哈希排序

考虑哈希迭代顺序的例子。如果我们将五个元素插入基于哈希的集合,我们以什么顺序取出它们?

>>> for i in {“apple”, “banana”, “carrot”, “durian”, “eggplant”}: print(i) … durian carrot apple eggplant banana

大多数程序员都知道哈希表的顺序是不明显的。很少有人知道他们正在使用的特定哈希表是否打算永远提供那种特定的顺序。这可能看起来不起眼,但在过去的十年或二十年里,计算机行业使用此类类型的经验已经发展:

  • 哈希泛洪10攻击为非确定性哈希迭代提供了更多动机。

  • 对改进的哈希算法或哈希容器的研究带来的潜在效率提升需要改变哈希迭代顺序。

  • 根据海勒姆定律,如果程序员有能力这样做,他们就会编写依赖于遍历哈希表顺序的程序。

作为结果,如果你问任何专家"我可以假设我的哈希容器有特定的输出顺序吗?"那位专家可能会说"不行。"总的来说这是正确的,但可能过于简单化了。一个更微妙的回答是,"如果你的代码是短期的,没有硬件、语言运行时或数据结构选择的变化,这样的假设是可以的。如果你不知道你的代码会存活多久,或者你不能保证你依赖的东西永远不会改变,那么这样的假设是不正确的。"此外,即使你自己的实现不依赖于哈希容器的顺序,它也可能被其他隐式创建这种依赖关系的代码使用。例如,如果你的库将值序列化到远程过程调用(RPC)响应中,RPC调用者可能最终会依赖这些值的顺序。

这是"它能工作"和"它是正确的"之间区别的一个非常基本的例子。对于一个短期程序来说,依赖容器的迭代顺序不会造成任何技术问题。另一方面,对于一个软件工程项目来说,这种对定义顺序的依赖是一种风险 - 只要有足够的时间,就会有一些东西使得改变迭代顺序变得有价值。这种价值可以以多种方式体现,无论是效率、安全性,还是仅仅是为未来的变化做好准备。当这种价值变得明显时,你需要权衡这种价值与破坏开发人员或客户的痛苦之间的利弊。

一些语言特意在库版本之间甚至在同一程序的执行之间随机化哈希顺序,以防止依赖。但即使这样仍然允许一些海勒姆定律的意外:有代码使用哈希迭代顺序作为低效的随机数生成器。现在移除这种随机性会破坏这些用户。就像每个热力学系统中熵都在增加一样,海勒姆定律适用于每一个可观察到的行为。

思考以"现在能工作"和"永远能工作"的心态编写的代码之间的差异,我们可以提取一些明确的关系。将代码视为具有(高度)可变生命周期要求的产物,我们可以开始对编程风格进行分类:依赖于其依赖项的脆弱和未公开特性的代码可能被描述为"黑客式的"或"聪明的",而遵循最佳实践并为未来做好计划的代码更可能被描述为"干净的"和"可维护的"。两者都有其用途,但你选择哪一个取决于所讨论的代码的预期寿命。我们常说,“如果’聪明’是一种赞美,那就是编程,但如果’聪明’是一种指责,那就是软件工程。”

为什么不直接瞄准"什么都不变"?

在所有这些关于时间和需要对变化做出反应的讨论中,隐含着一个假设,即变化可能是必要的。真的是这样吗?

与本书中的其他内容一样,这取决于具体情况。我们可以很容易地承诺"对于大多数项目来说,在足够长的时间段内,它们下面的一切都可能需要改变。"如果你有一个用纯C编写的项目,没有外部依赖(或只有承诺长期稳定的外部依赖,比如POSIX),你可能确实能够避免任何形式的重构或困难的升级。C在提供稳定性方面做得很好 - 在许多方面,这是它的主要目的。

大多数项目都更容易受到底层技术变化的影响。大多数编程语言和运行时的变化比C多得多。即使是用纯C实现的库也可能会为了支持新特性而改变,这可能会影响下游用户。从处理器到网络库再到应用程序代码,各种技术都会披露安全问题。你的项目所依赖的每一项技术都有一些(希望很小)风险,可能包含关键错误和安全漏洞,这些问题可能只有在你开始依赖它之后才会暴露出来。如果你无法部署 Heartbleed[1] 的补丁或缓解像 Meltdown和Spectre[2] 这样的推测执行问题,因为你假设(或承诺)什么都不会改变,那这是一个重大的赌博。

效率改进进一步使情况复杂化。我们希望为数据中心配备具有成本效益的计算设备,特别是提高 CPU 效率。然而,早期谷歌的算法和数据结构在现代设备上效率较低:链表或二叉搜索树仍然可以正常工作,但 CPU 周期与内存延迟之间不断扩大的差距影响了"高效"代码的样子。随着时间的推移,如果没有相应的软件设计变更,升级到新硬件的价值可能会降低。向后兼容性确保旧系统仍能正常运行,但这并不能保证旧的优化仍然有用。不愿意或无法利用这些机会可能会招致巨大成本。这种效率问题尤其微妙:最初的设计可能完全合乎逻辑并遵循合理的最佳实践。只是在一系列向后兼容的变更之后,一个新的、更高效的选择才变得重要。没有犯任何错误,但时间的流逝仍然使变革变得有价值。

刚才提到的这些问题就是为什么长期项目如果没有投资于可持续性就会面临巨大风险。我们必须能够应对这些问题并利用这些机会,无论它们是直接影响我们还是仅在我们所依赖的技术的传递闭包中表现出来。变革本身并不是好事。我们不应该为了变革而变革。但我们确实需要具备变革的能力。如果我们为这种最终的必要性留有余地,我们还应该考虑是否投资于使这种能力变得廉价。正如每个系统管理员都知道的那样,理论上知道可以从磁带恢复是一回事,而在实践中确切知道如何做以及在必要时需要多少成本则是另一回事。实践和专业知识是提高效率和可靠性的重要驱动力。

规模和效率

正如《站点可靠性工程》(SRE)一书中所指出的,11 谷歌的整个生产系统是人类创造的最复杂的机器之一。构建这样一台机器并保持其平稳运行所涉及的复杂性需要我们组织内部和全球各地的专家进行无数小时的思考、讨论和重新设计。因此,我们已经写了一本关于在这种规模下保持该机器运行的复杂性的书。

本书的大部分内容都集中在生产这样一台机器的组织的规模复杂性上,以及我们用来使该机器长期运行的流程上。再次考虑代码库可持续性的概念:"当你能够安全地更改所有应该更改的内容,并且可以在代码库的整个生命周期内这样做时,你的组织的代码库就是可持续的。"在关于能力的讨论中也隐含着成本的问题:如果更改某些内容的成本过高,很可能会被推迟。如果成本随时间呈超线性增长,显然这种操作是不可扩展的。12 最终,时间会占据主导地位,会出现一些意想不到的情况,你必须进行更改。当你的项目规模扩大一倍,你需要再次执行该任务时,它是否会需要两倍的人力?下次你甚至还有解决这个问题所需的人力资源吗?

人力成本并不是唯一需要扩展的有限资源。正如软件本身需要在传统资源(如计算、内存、存储和带宽)方面良好扩展一样,该软件的开发也需要扩展,无论是在人力时间投入方面还是在为开发工作流提供动力的计算资源方面。如果测试集群的计算成本呈超线性增长,每个季度每人消耗的计算资源越来越多,那么你就走上了一条不可持续的道路,需要尽快做出改变。

最后,软件组织最宝贵的资产——代码库本身——也需要扩展。如果你的构建系统或版本控制系统随时间呈超线性扩展,可能是由于增长和不断增加的变更日志历史记录导致的,可能会到达一个你根本无法继续的点。许多问题,例如"完整构建需要多长时间?",“拉取存储库的新副本需要多长时间?”,或者"升级到新的语言版本需要多少成本?"并没有得到积极监控,而且变化速度很慢。它们很容易变成 比喻中的煮青蛙[3] ;问题慢慢恶化而从未表现为单一的危机时刻,这种情况太容易发生了。只有在整个组织范围内意识到并致力于扩展,你才有可能掌控这些问题。

你的组织依赖于生产和维护代码的所有内容都应该在总体成本和资源消耗方面具有可扩展性。特别是,你的组织必须重复做的所有事情都应该在人力投入方面具有可扩展性。许多常见的政策在这个意义上似乎并不具有可扩展性。

不可扩展的政策

以下是逐字翻译成中文的结果,保留了原文的标点符号和格式:

随着一些练习,识别具有不良扩展性的政策变得更容易。最常见的是,可以通过考虑施加在单个工程师身上的工作,并想象组织规模扩大10倍或100倍来识别这些政策。当我们的规模扩大10倍时,我们的样本工程师需要跟上的工作量是否会增加10倍?我们的工程师必须执行的工作量是否会随着组织规模的增长而增长?工作量是否会随着代码库的大小而增加?如果其中任何一个是真的,我们是否有任何机制来自动化或优化这项工作?如果没有,我们就存在扩展性问题。

考虑一下传统的弃用方法。我们在 弃用[4] 中更多地讨论了弃用,但弃用的常见方法是扩展性问题的一个很好的例子。一个新的Widget已经开发出来了。决定是每个人都应该使用新的Widget,停止使用旧的。为了推动这一点,项目负责人说"我们将在8月15日删除旧的Widget;确保你已经转换到新的Widget。"

这种方法可能在小型软件环境中有效,但随着依赖关系图的深度和广度的增加而迅速失效。团队依赖于越来越多的Widget,单个构建中断可能会影响公司越来越大的比例。以可扩展的方式解决这些问题意味着改变我们进行弃用的方式:不是将迁移工作推给客户,团队可以自己内化它,并获得由此带来的所有规模经济。

2012年,我们试图通过缓解变动的规则来制止这种情况:基础设施团队必须自己完成将内部用户迁移到新版本的工作,或者以向后兼容的方式进行更新。这项政策,我们称之为"变动规则",扩展性更好:依赖项目不再花费越来越多的精力只是为了跟上。我们还了解到,让一群专门的专家执行变更比要求每个用户付出更多的维护努力扩展性更好:专家花一些时间深入学习整个问题,然后将这种专业知识应用到每个子问题上。强迫用户响应变动意味着每个受影响的团队都会做得更差,他们匆忙上手,解决他们眼前的问题,然后丢弃那些现在无用的知识。专业知识的扩展性更好。

开发分支的传统使用是另一个具有内在扩展问题的政策例子。一个组织可能发现将大型功能合并到主干中已经破坏了产品的稳定性,并得出结论,"我们需要更严格地控制合并的时间。我们应该减少合并频率。"这很快导致每个团队或每个功能都有单独的开发分支。每当任何分支被决定"完成"时,它就会被测试并合并到主干中,这会触发其他仍在开发分支上工作的工程师的一些潜在昂贵的工作,比如重新同步和测试。这种分支管理对于juggling 5到10个这样的分支的小型组织来说可能是有效的。随着组织规模(和分支数量)的增加,很快就会发现我们正在为完成同样的任务付出越来越多的开销。随着规模的扩大,我们需要一种不同的方法,我们在 版本控制和分支管理[5] 中讨论了这一点。

扩展性良好的政策

什么样的政策能随着组织的成长而产生更好的成本效益?或者,更好的是,我们可以制定什么样的政策,随着组织的成长提供超线性的价值?

我们最喜欢的内部政策之一是基础设施团队的一个很好的支持者,保护他们安全进行基础设施变更的能力。“如果产品由于基础设施变更而遇到中断或其他问题,但这个问题没有在我们的持续集成(CI)系统的测试中暴露出来,那么这不是基础设施变更的错。“更通俗地说,这被表述为"如果你喜欢它,你应该在它上面放一个CI测试”,我们称之为"碧昂斯规则”。13从扩展性的角度来看,碧昂斯规则意味着复杂的、一次性的定制测试,如果不是由我们常用的CI系统触发的,就不算数。没有这个,基础设施团队的工程师可能需要追踪每个有任何受影响代码的团队,并询问他们如何运行他们的测试。当我们只有一百名工程师时,我们可以这样做。我们现在肯定负担不起这样做了。"

我们发现,随着组织规模的扩大,专业知识和共享交流论坛能提供巨大价值。当工程师在共享论坛中讨论和回答问题时,知识往往会传播开来。新的专家也会成长起来。如果你有一百名编写Java的工程师,一位友好且乐于解答问题的Java专家很快就能培养出一百名编写更好Java代码的工程师。知识具有传染性,专家是传播者,为工程师们扫清常见障碍的价值是不言而喻的。我们在 知识共享[6] 中对此进行了更详细的讨论。

示例:编译器升级

考虑一下升级编译器这个令人生畏的任务。理论上,考虑到语言为保持向后兼容所付出的努力,编译器升级应该是一项成本较低的操作,但在实践中这个操作的成本究竟如何呢?如果你从未执行过此类升级,你将如何评估你的代码库是否与这一变更兼容?

根据我们的经验,即使普遍预期会保持向后兼容,语言和编译器升级仍然是一项微妙而困难的任务。编译器升级几乎总是会导致行为的细微变化:修复错误编译、调整优化,或者可能改变先前未定义的任何结果。面对所有这些潜在结果,你将如何评估整个代码库的正确性?

Google历史上最著名的编译器升级要追溯到2006年。当时,我们已经运营了几年,拥有数千名工程师。我们大约五年没有更新过编译器。我们的大多数工程师都没有经历过编译器变更。我们的大部分代码只接触过单一版本的编译器。对于一个(主要由)志愿者组成的团队来说,这是一项艰难而痛苦的任务,最终变成了寻找捷径和简化方法,以绕过我们不知道如何采用的上游编译器和语言变更。14 最终,2006年的编译器升级极其痛苦。许多大大小小的海勒姆定律问题悄然潜入代码库,加深了我们对特定编译器版本的依赖。打破这些隐式依赖是痛苦的。相关工程师承担着风险:我们当时还没有碧昂斯规则,也没有普及的CI系统,因此很难预先知道变更的影响,也无法确保他们不会因回归而受到指责。

这个故事并不罕见。许多公司的工程师都能讲述类似的痛苦升级经历。不同寻常的是,我们事后意识到这项任务是痛苦的,并开始专注于技术和组织变革,以克服扩展问题并将规模转化为优势:自动化(使单个人能做更多事情)、整合/一致性(使底层变更的问题范围受限),以及专业知识(使少数人能做更多事情)。

你越频繁地更改基础设施,就越容易进行更改。我们发现,大多数情况下,当代码作为编译器升级的一部分进行更新时,它会变得不那么脆弱,更容易在未来进行升级。在一个大多数代码都经历过多次升级的生态系统中,代码不再依赖于底层实现的细微差别;相反,它依赖于语言或操作系统保证的实际抽象。无论你升级的是什么,预计代码库的第一次升级会比后续升级显著更昂贵,即使控制了其他因素。

通过这些经验和其他经历,我们发现了许多影响代码库灵活性的因素:

专业知识

我们知道如何做这件事;对于某些语言,我们现在已经在多个平台上进行了数百次编译器升级。

稳定性

由于我们更regularly采用新版本,版本之间的变化较少;对于某些语言,我们现在每一两周就部署一次编译器升级。

一致性

由于我们定期进行升级,尚未经历升级的代码较少。

熟悉度

因为我们经常这样做,我们可以发现执行升级过程中的冗余,并尝试自动化。这与SRE对琐事的看法有很大重叠。15

政策

我们有像碧昂斯规则这样的流程和政策。这些流程的净效果是使升级保持可行,因为基础设施团队不需要担心每一个未知的使用情况,只需关注在我们的CI系统中可见的那些。

潜在的教训不是关于编译器升级的频率或难度,而是一旦我们意识到编译器升级任务是必要的,我们就找到了方法来确保即使代码库不断增长,也能用固定数量的工程师来执行这些任务。16 如果我们反而决定这项任务太昂贵应该在未来避免,我们可能仍在使用十年前的编译器版本。由于错过了优化机会,我们可能会为计算资源多付出25%的费用。考虑到2006年左右的编译器肯定无法帮助缓解推测执行漏洞,我们的核心基础设施可能会面临重大安全风险。停滞不前是一种选择,但通常不是明智之举。

左移

我们看到的一个普遍真理是,在开发者工作流程的早期发现问题通常可以降低成本。考虑一下从左到右推进的功能开发工作流程时间线,从构思和设计开始,经过实施、审查、测试、提交、金丝雀测试,最终部署到生产环境。将问题检测"左移"到这个时间线的早期比等待更长时间更便宜,如图1-2所示。

这个术语似乎源于"安全不能推迟到开发过程的最后"的论点,伴随着"在安全方面左移"的呼吁。在这种情况下,论点相对简单:如果只有在产品投入生产后才发现安全问题,你就会面临一个非常昂贵的问题。如果在部署到生产环境之前发现,可能仍需要大量工作来识别和解决问题,但成本更低。如果你能在原始开发者将缺陷提交到版本控制之前发现它,成本就更低了:他们已经了解了该功能;根据新的安全约束进行修改比提交并强迫其他人分类和修复它更便宜。

图1-2. 开发者工作流程时间线

本书中多次出现同样的基本模式。通过静态分析和代码审查在提交之前发现的错误比进入生产环境的错误要便宜得多。提供在开发过程早期突出质量、可靠性和安全性的工具和实践是我们许多基础设施团队的共同目标。不需要任何单一的流程或工具是完美的,所以我们可以采用纵深防御的方法,希望在图表的左侧捕获尽可能多的缺陷。

权衡和成本

如果我们了解如何编程,了解我们正在维护的软件的生命周期,并了解如何在更多工程师生产和维护新功能时进行扩展维护,那么剩下的就是做出好的决策。这似乎很明显:在软件工程中,就像在生活中一样,好的选择会带来好的结果。然而,这一观察的影响很容易被忽视。在谷歌内部,人们强烈反感"因为我说了算"。对于任何主题都有一个决策者,并且在决策似乎错误时有明确的上报途径是很重要的,但目标是达成共识,而不是一致。看到一些"我不同意你的指标/评估,但我理解你如何得出这个结论"的情况是正常和预期的。所有这些都包含一个想法,即一切都需要有理由;“就是这样”、"因为我说了算"或"因为其他人都是这样做的"是潜藏着糟糕决策的地方。只要有效率,我们就应该能够在决定两个工程选项的一般成本时解释我们的工作。

我们所说的成本是什么意思?我们不仅仅在谈论金钱。"成本"大致可以理解为努力,可能涉及以下任何或所有因素:

  • 财务成本(例如,金钱)

  • 资源成本(例如,CPU时间)

  • 人员成本(例如,工程努力)

  • 交易成本(例如,采取行动的成本是多少?)

  • 机会成本(例如,不采取行动的成本是多少?)

  • 社会成本(例如,这个选择对整个社会有什么影响?)

历史上,忽视社会成本问题一直特别容易。然而,谷歌和其他大型科技公司现在可以可信地部署拥有数十亿用户的产品。在许多情况下,这些产品显然是净收益,但当我们在如此大的规模下运营时,即使在可用性、可访问性、公平性或滥用潜力方面存在微小差异,也会被放大,往往会损害那些已经边缘化的群体。软件渗透到社会和文化的许多方面;因此,我们在做出产品和技术决策时,明智的做法是意识到我们所实现的好处和坏处。我们在 工程公平性[7] 中对此进行了更多讨论。

除了上述成本(或我们对它们的估计)之外,还存在偏见:现状偏见、损失厌恶等。当我们评估成本时,我们需要牢记所有上述成本:一个组织的健康不仅仅是银行里是否有钱,还包括其成员是否感到受重视和富有成效。在软件工程这样高度创造性和利润丰厚的领域,财务成本通常不是限制因素——人员成本通常才是。保持工程师快乐、专注和投入所带来的效率提升很容易主导其他因素,仅仅是因为专注度和生产力变化很大,10-20%的差异很容易想象。

示例:标记笔

在许多组织中,白板标记笔被视为珍贵物品。它们受到严格控制,总是供不应求。在任何给定的白板上,不可避免地有一半的标记笔是干的和无法使用的。你有多少次参加会议时因为缺少可用的标记笔而被打断?你有多少次因为标记笔用完而打断了思路?有多少次所有的标记笔都不见了,大概是因为其他团队用完了标记笔不得不偷走你的?所有这些都是为了一种成本不到一美元的产品。

谷歌倾向于在大多数工作区域设有未上锁的储物柜,里面装满办公用品,包括白板标记笔。只需片刻就可以轻松拿到几十支各种颜色的标记笔。在某个时候,我们做出了明确的权衡:优化无障碍头脑风暴远比防止有人带走一堆标记笔更重要。

我们的目标是对我们所做的每件事都有同样程度的睁眼和明确权衡成本/收益权衡,从办公用品和员工福利到开发人员的日常体验,再到如何提供和运行全球规模的服务。我们经常说,"谷歌是一种数据驱动的文化。"事实上,这是一种简化:即使没有数据,可能仍然有证据、先例和论证。做出好的工程决策就是权衡所有可用的输入并对权衡做出明智的决定。有时,这些决定是基于直觉或公认的最佳实践,但只有在我们穷尽了试图衡量或估计真正潜在成本的方法之后才会这样做。

最终,工程团队的决策应该归结为很少几件事:

  • 我们这样做是因为我们必须这样做(法律要求、客户要求)。

  • 我们这样做是因为这是我们当时根据现有证据所能看到的最佳选择(由某个适当的决策者确定)。

决策不应该是"我们这样做是因为我说了算。"17

决策的输入

当我们权衡数据时,我们发现两种常见情况:

  • 所有涉及的数量都是可测量的,或者至少可以估计。这通常意味着我们正在评估CPU和网络之间的权衡,或者美元和RAM之间的权衡,或者考虑是否花费两周的工程师时间来节省我们数据中心的N个CPU。

  • 一些数量是微妙的,或者我们不知道如何测量它们。有时这表现为"我们不知道这需要多少工程师时间。"有时它甚至更加模糊:你如何衡量设计糟糕的API的工程成本?或产品选择对社会的影响?

对于第一类决策,几乎没有理由不足。任何软件工程组织都可以而且应该跟踪计算资源、工程师小时数和其他你经常接触的数量的当前成本。即使你不想向你的组织公开确切的美元金额,你仍然可以制作一个转换表:这么多CPU的成本与这么多RAM或这么多网络带宽相同。

以下是逐字翻译成中文的结果,保留了原文的标点符号和格式:

有了商定的转换表,每个工程师都可以进行自己的分析。"如果我花两周时间将这个链表改成一个性能更高的结构,我将多使用5 GiB的生产RAM,但节省2000个CPU。我应该这么做吗?"这个问题不仅取决于RAM和CPU的相对成本,还取决于人员成本(两周支持一名软件工程师)和机会成本(该工程师两周内还能产出什么?)。

对于第二种类型的决策,没有简单的答案。我们依靠经验、领导力和先例来协商这些问题。我们正在投资研究,以帮助我们量化难以量化的东西(参见 衡量工程生产力[8] )。然而,我们最好的广泛建议是要意识到并非所有事情都是可衡量或可预测的,并尝试以同样的优先级和更多的关注来处理此类决策。它们通常同样重要,但更难管理。

示例:分布式构建

考虑你的构建。根据完全不科学的Twitter民意调查,即使在今天大型复杂的构建中,大约60%到70%的开发人员仍在本地构建。这直接导致了 这个"编译中"漫画[9] 所描绘的非笑话情况 - 你的组织中有多少生产时间浪费在等待构建上?将其与为小组运行 distcc之类的东西的成本进行比较。或者,为大型团队运行一个小型构建农场需要多少成本?这些成本需要多少周/月才能实现净收益?

早在2000年代中期,Google完全依赖本地构建系统:你检出代码并在本地编译。在某些情况下,我们有大型本地机器(你可以在桌面上构建Maps!),但随着代码库的增长,编译时间变得越来越长。毫不奇怪,由于时间损失,我们在人员成本方面产生了越来越多的开销,以及更大更强大的本地机器的资源成本增加,等等。这些资源成本特别麻烦:当然,我们希望人们的构建尽可能快,但大多数时候,高性能的桌面开发机器会闲置。这感觉不像是投资这些资源的正确方式。

最终,Google开发了自己的分布式构建系统。开发这个系统当然产生了成本:工程师花时间开发,花更多工程师时间改变每个人的习惯和工作流程并学习新系统,当然还需要额外的计算资源。但总体节省显然是值得的:构建变得更快,工程师时间得到回收,硬件投资可以集中在管理的共享基础设施上(实际上是我们生产车队的一个子集),而不是越来越强大的台式机上。构建系统和构建哲学[10] 更详细地介绍了我们对分布式构建的方法和相关权衡。

所以,我们构建了一个新系统,部署到生产环境中,加快了每个人的构建。这就是故事的圆满结局吗?不完全是:提供分布式构建系统极大地提高了工程师的生产力,但随着时间的推移,分布式构建本身变得臃肿。在之前的情况下受到个别工程师约束的东西(因为他们有既得利益保持本地构建尽可能快)在分布式构建系统中不受约束。构建图中臃肿或不必要的依赖变得太常见。当每个人直接感受到非最优构建的痛苦并被激励保持警惕时,激励更好地对齐。通过消除这些激励并将臃肿的依赖隐藏在并行分布式构建中,我们创造了一种消费可能失控的情况,几乎没有人被激励关注构建臃肿。这让人想起 杰文悖论[11] :资源消耗可能会随着其使用效率的提高而增加。

总的来说,添加分布式构建系统带来的节省成本远远超过了与其构建和维护相关的负面成本。但是,正如我们在消费增加中看到的那样,我们并未预见到所有这些成本。在勇往直前之后,我们发现自己处于需要重新构想系统目标和约束以及我们的使用情况,确定最佳实践(小型依赖,机器管理依赖),并为新生态系统提供工具和维护资金的情况。即使是相对简单的权衡,形式为"我们将花费$$$用于计算资源以收回工程师时间",也有不可预见的下游影响。

示例:在时间和规模之间做出决定

很多时候,我们的主要主题时间和规模是重叠并协同工作的。像Beyoncé规则这样的政策可以很好地扩展,并帮助我们长期维护事物。对操作系统界面的更改可能需要许多小的重构来适应,但这些更改中的大多数都会很好地扩展,因为它们具有相似的形式:操作系统的更改不会对每个调用者和每个项目表现出不同。

偶尔时间和规模会发生冲突,最明显的莫过于这个基本问题:我们应该添加一个依赖项还是分叉/重新实现它以更好地满足我们的本地需求?

这个问题可能出现在软件堆栈的多个层面,因为定制的解决方案通常可以在狭窄的问题空间中胜过需要处理所有可能性的通用解决方案。通过分叉或重新实现实用代码并为您的狭窄领域定制它,您可以更轻松地添加新功能,或更确定地进行优化,无论我们谈论的是微服务、内存缓存、压缩例程还是我们软件生态系统中的任何其他内容。也许更重要的是,您从这种分叉中获得的控制权将您与底层依赖项的变化隔离开来:这些变化不是由另一个团队或第三方提供商决定的。您可以控制如何以及何时对时间的流逝和变化的必要性做出反应。

另一方面,如果每个开发人员都分叉他们软件项目中使用的所有内容,而不是重用现有的内容,那么可扩展性和可持续性都会受到影响。对底层库中的安全问题做出反应不再是更新单个依赖项及其用户的问题:现在是识别该依赖项的每个易受攻击的分叉及其用户的问题。

与大多数软件工程决策一样,这种情况没有一刀切的答案。如果您的项目寿命很短,分叉的风险就会降低。如果有问题的分叉在范围上可证明是有限的,那也会有帮助 - 避免对可能跨时间或项目时间边界运行的接口(数据结构、序列化格式、网络协议)进行分叉。一致性具有很大的价值,但通用性也有其自身的成本,如果您谨慎行事,通常可以通过做自己的事情而获胜。

重新审视决策,犯错误

致力于数据驱动文化的一个未被歌颂的好处是承认错误的能力和必要性。在某个时候会做出决定,基于可用的数据 - 希望是基于良好的数据和只有少数假设,但隐含地基于当前可用的数据。随着新数据的出现、背景的变化或假设被打破,可能会变得明显,一个决定是错误的,或者它在当时是有意义的,但现在不再是了。这对于一个长寿的组织来说尤其重要:时间不仅会触发技术依赖和软件系统的变化,还会影响用于驱动决策的数据。

我们坚信数据应该为决策提供信息,但我们认识到数据会随着时间而改变,新的数据可能会出现。这意味着,在系统的生命周期内,决策需要不时地重新审视。对于长期项目来说,在初始决策做出后能够改变方向通常是至关重要的。而且,重要的是,这意味着决策者需要有承认错误的权利。与一些人的直觉相反,承认错误的领导者会得到更多而不是更少的尊重。

要以证据为导向,但也要意识到无法衡量的东西可能仍然有价值。如果你是一个领导者,这就是你被要求做的:运用判断力,断言事物是重要的。我们将在 如何领导团队[12] 和 大规模领导[13] 章节中更多地讨论领导力。

软件工程与编程

当我们提出软件工程和编程之间的区别时,你可能会问是否存在内在的价值判断。编程是否在某种程度上比软件工程差?一个预计持续十年、由数百人组成的团队的项目是否比一个只有两个人构建、只有一个月有用的项目本质上更有价值?

当然不是。我们的观点并不是说软件工程更优越,只是这代表两个不同的问题领域,有着不同的约束、价值观和最佳实践。相反,指出这种差异的价值在于认识到某些工具在一个领域很好用,但在另一个领域却不适用。对于只持续几天的项目,你可能不需要依赖集成测试(参见 大型测试[14] )和持续部署(CD)实践(参见 持续交付[15] )。同样,我们在软件工程项目中对语义版本控制(SemVer)和依赖管理的所有长期考虑(参见 依赖管理[16] )并不真正适用于短期编程项目:使用任何可用的东西来解决手头的任务。

我们认为区分相关但不同的术语"编程"和"软件工程"很重要。这种区别很大程度上源于随时间推移对代码的管理、时间对规模的影响,以及面对这些想法时的决策。编程是立即产生代码的行为。软件工程是一套政策、实践和工具,这些是使代码在需要使用的时间内保持有用并允许团队协作所必需的。

结论

本书讨论了所有这些主题:组织和单个程序员的政策,如何评估和改进最佳实践,以及用于可维护软件的工具和技术。谷歌一直在努力拥有可持续的代码库和文化。我们并不认为我们的方法是唯一正确的方式,但它确实通过示例证明了这是可以做到的。我们希望它能为思考一般问题提供一个有用的框架:你如何在代码需要继续工作的时间内维护它?

TL;DRs

  • "软件工程"与"编程"在维度上有所不同:编程是关于生产代码。软件工程将其扩展到包括在其有用生命周期内维护该代码。

  • 短期代码和长期代码的生命周期之间至少相差100,000倍。假设相同的最佳实践普遍适用于这个范围的两端是愚蠢的。

  • 当我们能够在代码的预期生命周期内应对依赖、技术或产品需求的变化时,软件就是可持续的。我们可能选择不改变事物,但我们需要有这种能力。

  • 海勒姆定律:当API有足够多的用户时,你在合同中承诺什么并不重要:你系统的所有可观察行为都会被某人所依赖。

  • 你组织必须重复执行的每项任务都应该是可扩展的(线性或更好)在人力投入方面。政策是使流程可扩展的绝佳工具。

  • 流程效率低下和其他软件开发任务往往会缓慢扩大。要小心煮青蛙问题。

  • 专业知识与规模经济结合时,效果尤其好。

  • "因为我说了算"是做事的糟糕理由。

  • 数据驱动是一个好的开始,但实际上,大多数决策都是基于数据、假设、先例和论证的混合。当客观数据构成这些输入的大部分时是最好的,但很少能成为全部。

  • 随着时间的推移保持数据驱动意味着当数据发生变化时(或当假设被推翻时)需要改变方向。错误或修订计划是不可避免的。

1我们不是指"执行生命周期",我们是指"维护生命周期"——代码将继续被构建、执行和维护多长时间?这个软件将提供价值多长时间?

2这也许是技术债务的一个合理的粗略定义:应该做但还没做的事情——我们的代码与我们希望它成为的样子之间的差距。

3还要考虑我们是否提前知道一个项目将是长期的问题。

4对于这句话的原始归属存在一些疑问;共识似乎是它最初由Brian Randell或Margaret Hamilton提出,但可能完全是由Dave Parnas编造的。常见的引用是"软件工程技术:NATO科学委员会赞助的会议报告",1969年10月27日至31日,意大利罗马,布鲁塞尔,NATO科学事务司。"

Here is the letter-by-letter translation into Chinese while preserving markdown format:

5弗雷德里克 P. 布鲁克斯 Jr. 神话中的人月:软件工程随笔 (波士顿:艾迪生-韦斯利,1995)。

6Appcelerator," 除了死亡、税收和移动应用程序寿命短之外,没有什么是确定的[17] ,"Axway 开发者博客,2012年12月6日。

7你自己的优先事项和品味将决定这种转变究竟在何时发生。我们发现大多数项目似乎愿意在五年内升级。在5到10年之间似乎是对这种转变的保守估计。

8值得称赞的是,Hyrum 真的很努力地谦虚地称之为"隐式依赖法则",但"Hyrum 法则"是 Google 大多数人已经确定的简称。

9参见" 工作流程[18] ",一幅 xkcd 漫画。

10一种拒绝服务(DoS)攻击,其中不受信任的用户知道哈希表的结构和哈希函数,并以降低表操作算法性能的方式提供数据。

11Beyer, B. 等人。站点可靠性工程:Google 如何运行生产系统[19] 。(波士顿:O’Reilly Media,2016)。

12每当我们在本章的非正式上下文中使用"可扩展"时,我们指的是"关于人际互动的次线性扩展"。

13这是对流行歌曲"单身女士"的引用,其中包括副歌"如果你喜欢它,那你就应该给它戴上戒指"。

14具体来说,C++ 标准库中的接口需要在 std 命名空间中引用,而对 std::string 的优化更改对我们的使用来说是一个重大的劣化,因此需要一些额外的变通方法。

15Beyer 等人。站点可靠性工程:Google 如何运行生产系统,第5章,“消除繁琐工作”。

16根据我们的经验,一个普通的软件工程师(SWE)在单位时间内产生的代码行数相当恒定。对于固定的 SWE 人口,代码库呈线性增长——与随时间推移的 SWE-月数成正比。如果你的任务需要与代码行数成比例的努力,那就令人担忧。

17这并不是说决策需要一致通过,甚至需要广泛共识;最终,必须有人是决策者。这主要是关于决策过程应该如何流动的声明,无论谁实际上负责决策。

参考链接

1. Heartbleed: http://heartbleed.com/
2. Meltdown和Spectre: https://meltdownattack.com/
3. 比喻中的煮青蛙: https://oreil.ly/clqZN
4. 弃用: https://abseil.io/resources/swe-book/html/ch15.html#deprecation
5. 版本控制和分支管理: https://abseil.io/resources/swe-book/html/ch16.html#versioncontrolandbranchmanagement
6. 知识共享: https://abseil.io/resources/swe-book/html/ch03.html#knowledgesharing
7. 工程公平性: https://abseil.io/resources/swe-book/html/ch04.html#engineeringforequity
8. 衡量工程生产力: https://abseil.io/resources/swe-book/html/ch07.html#measuringengineeringproductivity
9. 这个"编译中"漫画: https://xkcd.com/303
10. 构建系统和构建哲学: https://abseil.io/resources/swe-book/html/ch18.html#buildsystemsandbuildphilosophy
11. 杰文悖论: https://oreil.ly/HL0sl
12. 如何领导团队: https://abseil.io/resources/swe-book/html/ch05.html#howtoleadateam
13. 大规模领导: https://abseil.io/resources/swe-book/html/ch06.html#leadingatscale
14. 大型测试: https://abseil.io/resources/swe-book/html/ch14.html#largertesting
15. 持续交付: https://abseil.io/resources/swe-book/html/ch24.html#continuousdelivery-id00035
16. 依赖管理: https://abseil.io/resources/swe-book/html/ch21.html#dependencymanagement
17. 除了死亡、税收和移动应用程序寿命短之外,没有什么是确定的: https://oreil.ly/pnT2_
18. 工作流程: http://xkcd.com/1172
19. 站点可靠性工程:Google 如何运行生产系统: http://shop.oreilly.com/product/0636920041528.do

黑客/网络安全学习包

资料目录

  1. 成长路线图&学习规划

  2. 配套视频教程

  3. SRC&黑客文籍

  4. 护网行动资料

  5. 黑客必读书单

  6. 面试题合集

因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取

CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享

1.成长路线图&学习规划

要学习一门新的技术,作为新手一定要先学习成长路线图方向不对,努力白费

对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图&学习规划。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。


因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取

CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享

2.视频教程

很多朋友都不喜欢晦涩的文字,我也为大家准备了视频教程,其中一共有21个章节,每个章节都是当前板块的精华浓缩


因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取

CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享

3.SRC&黑客文籍

大家最喜欢也是最关心的SRC技术文籍&黑客技术也有收录

SRC技术文籍:

黑客资料由于是敏感资源,这里不能直接展示哦!

4.护网行动资料

其中关于HW护网行动,也准备了对应的资料,这些内容可相当于比赛的金手指!

5.黑客必读书单

**

**

6.面试题合集

当你自学到这里,你就要开始思考找工作的事情了,而工作绕不开的就是真题和面试题。

更多内容为防止和谐,可以扫描获取~

因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取

CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值