Press "Enter" to skip to content

创业公司CTO手册(四)—— 技术架构

作为技术领导者,您的一个重要职责是做出有关架构和工具的明智决策。良好的架构将您选择的工具和模式的优势与您组织现在和可预见的未来的需求相结合。这需要了解每种选择中固有的优点、缺点和权衡。我在本书的本节中的目标是向您介绍各个领域的选项,并帮助您认识到不同策略带来的一般权衡。

在与团队讨论工具和工具选择时,有一点需要记住:工程师可能会对工具选择有很多情绪化的反应。工具被评价为好坏,并且人们对个人喜好、反感和偏见。作为领导者和决策者,我强烈警告您不要在讨论工具时采用此类方式的语言。这不仅可能疏远团队成员(如果您诋毁他们个人喜爱的工具),而且是不生产和分散注意力的,会使寻找问题的好解决方案成为困难。有些个别工具确实设计得很糟糕,并被更好的替代品遮盖。

更常见的情况是,通过更细致入微的评估,我们会发现某个特定工具并不是本质上糟糕,而是在特定的公司或项目中合适或不合适。不要因为使用一个对解决某个问题不合适的工具而因为曾经有一个坏的经验而拒绝在另一个时候使用它,也不要让您的团队因此而拒绝使用这个工具阻碍您的团队在另一个时候使用它,当时它可能更适合。

架构

有许多优秀的资源深入探讨了各种架构模式;我最喜欢的之一是Martin Fowler的Patterns of Enterprise Application Architecture。在本章中,我将提供一些关键短语的摘要,以便您在深入研究这些主题时有一些背景知识。

面向领域的设计

面向领域的设计(DDD)是一种软件开发方法,其重点是理解和建模问题领域,以便设计更好的软件解决方案。

DDD的核心概念包括:

领域模型:您的技术系统中以对象形式表示的业务概念的表示;

共通语言:一种在公司范围内使用的共同、一致的词汇和语言,以最大程度地减少混淆;

有界上下文:适用领域模型并使用共同语言的边界。

高级模式

当有人使用术语“技术架构”时,他们通常指的是代码如何执行以及信息在系统中的流动方式。架构的大多数描述涉及服务、单块或消息传输的术语。与此相反,编码模式涉及到经常出现的术语,如面向对象编程、函数式编程或依赖注入。编码模式有时称为代码架构,并在Coding Patterns,第188页进行讨论。

技术架构中最具影响力的决策是代码是作为单块代码运行,还是作为一组服务(通常称为微服务)运行。我将从描述每种模式的外观开始,然后对它们之间的权衡提供一些建议。

单体架构

单体架构模式是指所有代码都作为一个单独的进程执行,其中信息完全在内存中在系统的组件之间移动,以简单的函数调用为模型。如果您曾经坐下来在一个下午构建一个简单的应用程序,那么很有可能它属于单体架构的范畴。单体架构有各种形状和规模,从非常小到庞大的、由数百万行代码构成的项目。

构建一个成功的单体架构的关键在于仔细设计应用程序内的数据流,使用面向领域的设计。您可以很容易地衡量这一点;您要确保当开发人员想要更改应用程序的功能时,他们很容易知道应该在单体架构的哪个部分进行修改。他们只需要在一个明显且定义明确或受限的区域内更改代码即可实现目标。每个需要更改以满足功能需求的代码基础的其他部分都增加了额外的复杂性或错误的机会,通常会减慢开发速度。

单体架构的关键特征:

  • 代码以单个单位进行部署。
  • 代码以单个源代码仓库进行管理。
  • 部署的代码以单个单位进行上下缩放。
  • 信息在系统的各个部分之间通过内存移动,通常是通过函数调用。
  • 系统不强制执行面向领域的设计和清晰的信息流设计,而是由工程师自行设计。
面向服务的架构(SOA)/微服务

术语“面向服务的架构”(SOA)起源于1990年代,用于指代一些相当特定的技术选择。如今,这个术语更广泛地用于描述信息在系统的不同部分之间通过网络传递的系统。 SOA的主要权衡是与单体架构相比,思考它可以非常复杂,并且需要团队进行大量设置和深思熟虑的设计,以确保好处超过增加的复杂性。

微服务是服务导向架构的一种子集,每个服务都是非常小,正如其名称所暗示的那样。有些系统实现了成千上万个微服务,每个微服务只有几行代码。也就是说,您不需要拥有数千个微服务才能体验到面向服务的架构的好处。即使将系统分解为四五个较小的服务,在适当的情况下,也可以极大地改善代码的健康状况。

您可能听说过微服务是唯一的好架构模式,这是不正确的。这种观点源于许多单体架构设计不当或未经过大规模技术债务投资而无法释放生产力。关于所有微服务架构都很容易处理的观念也是不正确的。有许多微服务实现由于各种原因无法实现预期的好处。

SOA或微服务系统的关键特征:

  • 不同的服务可以独立部署和扩展。
  • 代码由单个源代码仓库或多个代码仓库管理。
  • 信息在系统的各个部分之间通过网络传递,通常通过HTTP、RPC(远程过程调用)或队列系统。
  • 数据合同必须经过有意识的设计和深思熟虑,因为合同是以API的形式实现并通过网络进行通信。
在面向服务的架构和单体架构之间进行选择

通常情况下,单体架构比SOA更容易设置,并且需要管理的技术逻辑要少得多。因此,对于绝大多数问题来说,单体架构是第一天的正确答案。如果团队非常自律和慎重地设计单体架构,它可以随着团队的规模而扩展。然而,这并不适用于所有人。对于许多团队/项目来说,单体架构的缺乏强制合同、无法作为单独组件进行缩放以及缺乏强制关注点分离将成为生产力的障碍。

如果您发现自己面临着一个混乱的单体架构,这并不意味着您的工程师做得不好。软件工程的本质就是需求的变化和系统的进化。维护单体架构有时可能意味着需要投入大量资源来更新系统设计以实现相应的发展,当一个团队未能做出这样的投资时,单体复杂性就变成了生产力的障碍。

在某些情况下,转向面向服务的架构显然是正确的选择:

  • 您的服务需要独立扩展的元素。例如,一个功能消耗大量的CPU资源,而您不希望它对其他功能产生影响,或者您希望在经济效益可观时单独扩展该部分,而不是为了扩展所有功能而付费;
  • 您正在处理的功能需要公开其自己的独立API,并且除主系统外具有其独立的数据域。特别是如果此API用于为外部客户提供服务,则将此功能作为单独的服务提供是一个明显的好选择。
  • 由于某种原因,您需要在应用程序中使用另一种编程语言。一个很好的例子可能是因为Python中有一个强大且高质量的框架来解决某种类型的问题,但是您的应用程序的其他部分是用Java编写的。在内存中桥接这两种语言是可能的,但是比较笨重。更简单的选择是通过API来桥接它们,将它们自然地作为独立服务托管。

部署您的整体式应用程序可能过于昂贵、缓慢或有风险。在这种情况下,您可以通过将新代码部署为独立服务来提高生产力并缩短部署时间。只需确保新服务独立于整体式应用程序运行,并且不会创建新的部署依赖关系。

面向服务体系结构的源代码控制:单一仓库与多个仓库

管理整体式应用程序的源代码相对简单,因为它存储在一个单一的仓库中,并使用单一的构建系统。一旦您开始将代码分解为不同的包、项目和服务,您就面临一个选择:您是在单一代码仓库中管理多个服务,还是在多个仓库中创建多个代码仓库?这种权衡被称为单一仓库与多个仓库之争。

如果您选择在单一仓库中管理多个服务,您可能会希望寻找一种工作区管理解决方案(例如,JavaScript生态系统中的yarn workspaces),以便分别构建这些项目。下面是单一仓库和多个仓库方法之间的一些基本区别:

**单一仓库的优缺点 TODO:将图表放在这里

很容易确保每个服务或包依赖项的版本都是最新的。

许多CI系统不能原生地支持单个仓库中的多个包,因此您必须手动构建一个支持此功能的工具。

将所有代码放入单个仓库可以提高可发现性,使开发人员更容易找到他们正在寻找的模块或引用。IDE对这种搜索具有强大的支持。

相比之下,多个仓库

需要使用集中软件包管理器和版本控制。这并不一定是坏事,但在同时处理项目和其依赖项时,可能会导致显著的开销。

可以与CI/CD流水线系统(Bitbucket流水线、GitHub actions等)完美集成。

我的一般建议是保持简单。对于中小型项目,单一仓库会更简单设置和维护。转向多个仓库意味着愿意投入一些工具成本,以确保多个仓库对开发人员而言易于使用,这是一项显著的成本。对于一个小型创业公司来说,这种成本可能不值得。另一方面,如果您正在快速增长或者已经有50多名开发人员,并且单一仓库变得难以管理,并且您拥有一个专门的内部平台或DevOps团队可以轻松处理多个仓库的操作重任,那么转向多个仓库模式可能是正确的选择。

分布式单体应用程序

分布式单体应用程序是指部署为多个服务的系统,这些服务没有足够的独立性或隔离性,因此无法独立部署。明确地说,这是两全其美的最差情况。与使开发人员可以进入任何服务并以独立方式对其进行操作、而不考虑任何其他服务的情况不同,这种设置要求开发人员考虑该服务如何影响其他服务。不仅如此,他们还可能需要在多个服务中进行更改,并在发布期间按照特定顺序协调部署,以确保兼容性。这种开发和部署复杂性抵消了微服务系统的主要好处。

如果注意到团队陷入这些模式或抱怨在服务之间协调发布,那么这应该是一个警示信号,提示您仔细观察并考虑还清一些技术债务,以使服务能够独立部署。这些技术债务通常存在于合同、API设计和系统中数据处理的方法中。

编写可读性好的代码

在专业环境中,任何一行代码的主要受众不是计算机,而是未来某个时刻需要阅读该代码以进行进一步开发的开发人员。这是编程的黄金法则:工程师应该以与对方的代码同样的可读性水平来编写代码。

语言和生态系统选择

根据编程的黄金法则,您选择的编程语言应该使您的团队能够编写高度可读和易于维护的代码。一般来说,一个优秀的工程师可以在任何语言中做到这一点;但是,有些语言相对于其他语言更容易做到这一点。选择语言或生态系统时还要考虑以下几个方面:

  • 了解该语言的人才库有多大,尤其是熟悉该环境并对像您这样的创业公司感兴趣的人才。
  • 是否存在可以用作起点的现有实现?
  • 是否有特定的性能或扩展要求?某些语言在特定类型的任务上比其他语言快得多。Haskell在字符串处理方面以效率低下而闻名,而C在大多数任务上都以速度快而闻名,尽管其他一些语言在某些问题上接近或超过C的速度,同时提供更简单和更友好的编程环境。
  • 是否存在特定的框架可能是特定语言的一个好的起点?例如,React Native是一种功能强大的跨平台移动语言,需要使用JavaScript或TypeScript。

在企业环境中,我建议使用具有静态类型系统的语言,例如Golang、TypeScript、Rust等,以便编译器可以更多地为确保代码正确性而进行重型工作,并使其他开发人员能够看到这些约束,并避免在运行时遇到此类问题。您应该努力实现一个本地开发环境,在执行代码之前,工具应该能够在编译时检查错误。修复编译时检查通常比修复运行时问题更快、更便宜,并且由于是自动化的,因此比运行时检查更能可靠地找到问题。

代码风格和格式化

在任何广泛使用的语言中,都会有一种公认的代码格式标准(例如,在Python中是PEP8),或者有一种可配置的工具可以强制执行特定的代码风格和格式化(例如,JavaScript中的ESLint或Prettier,或C#中的ReSharper)。这些工具大部分都非常出色,可以确保不管是谁编写的代码,在风格上都是完全一致的。为了确保您的代码库具有可读性,没有任何借口不使用其中之一,并确保您的代码库中100%的代码都按照相同的规则进行格式化。您使用哪些规则完全取决于您和您的团队的偏好,但确保它们是一致的并产生可读性好的结果就可以了。

我建议您在开发人员使用的集成开发环境(IDE)中设置一组配置选项或说明,以在保存文件时自动格式化代码。然后,在持续集成系统中,确保所有新代码都被正确格式化。

需要注意的是,在持续集成系统中强制执行风格,而没有自动格式化是非常令工程师沮丧的,所以请确保在第一天就培训所有人正确设置他们的IDE,以避免一致的惊喜和CI中的lint失败导致的浪费的周期时间。

静态代码分析

现代静态代码分析可以识别和警报各种常见的代码问题,从安全漏洞到明显的错误和风格上的不一致。这些工具相对便宜,并可以与广泛使用的持续集成系统和开发人员IDE很好地集成。从使用这些工具对一系列项目和编程语言的经验来看,信号与噪声比非常好,并且输出是生产力和软件质量的净增益。在软件项目的早期阶段,您应该集成静态代码分析。我鼓励您查看与您选择的编程语言特定的工具,例如JavaScript的ESLint,以及SonarCloud,Codebeat,Scrutinizer-CI,Code Climate或Cloudacity等通用分析平台。

绿地建筑与棕地建筑

绿地软件开发是指在几乎没有现有遗留代码的新环境中进行开发工作,可以自由选择工具、模式和架构。这有明显的优点,允许仔细选择适合工作的正确架构和工具,并且不会因为现有技术债务而分心。微小的缺点是,在如此多的选择和如此少的约束条件下,糟糕决策的风险更高。此外,新项目通常有一个相当大的启动成本,很容易低估这些事情,比如设置测试、构建系统、静态代码分析等。

棕地软件开发指的是与现有遗留系统一起工作的相反情况。权衡通常正好相反:无论好坏,都被那些在您之前做出的高层决策所束缚。

棕地开发中最大的风险是“不在这里发明”症候群。在这里不发明的倾向意味着个体避免对自己没有创建的事物负责或给予足够的关注。在棕地软件开发中,这可能导致系统理解的系统性低投入,从而导致增强或修改现有系统时的不满和低效。我强烈鼓励管理者在要求团队修改现有系统之前,为团队阅读和理解现有系统提供明确的空间。在理解上的投入所花费的时间将通过减少意外和加快未来速度来回收。

编程模式

关于如何编写代码的风格的问题对许多开发人员来说很困扰。我在本章中的目的是简要介绍编程模式中最常见的短语的含义,并向您介绍有关每一种实践的更广泛资源。

如果您面临的是一个在这个主题上引起情绪化讨论的问题,请记住,许多成功的公司都使用了每一种模式。一切都是一种权衡。一个糟糕的程序员可以用任何工具制造一团糟,而一位优秀的程序员甚至可以找到一种方法来编写可读的解决方案,即使使用了次优的工具。

面向对象编程(OOP)

面向对象编程(OOP)是一种设计代码的方法论,目的是模拟现实世界的名词和动词。一个典型的例子是将两个人之间的交互建模为两个人对象,而任何与人相关的行为,如说话,将是这些对象的函数。许多语言本质上都是面向对象的,例如Python、Ruby和C#。有些语言(如JavaScript或C++)是对象可选的(支持程度上支持面向对象和函数式风格),而其他语言则完全不同。

纯度

纯度高的代码没有外部依赖或副作用。换句话说,对于相同的输入,纯代码总是会产生相同的输出。纯代码的优点是易于测试,不需要外部设置或模拟。纯代码也更容易阅读和理解,因为它不需要阅读任何其他代码来理解其功能。一个简单的纯代码示例是一个将两个数字相加的函数;给定任意两个输入数字,求和函数总是产生相同的输出。

有些代码天生不纯,例如与外界交互的代码,例如文件系统、网络或数据库。对于大多数其他情况,可以以纯方式建模业务逻辑。在可能的情况下,我鼓励您和您的团队编写纯代码。

函数式编程

为了坚持词类行模型来描述编程模式,函数式编程将动词(函数)作为系统的一部分。大多数函数式编程从非常小的功能开始,然后将其组合起来创建更复杂的系统。当它做得好时,函数式代码的好处是它往往更纯粹,因此更易于阅读、推理和单独测试。函数式代码的优点在于存在可以形式推理的学术实例,这意味着可以生成一种数学证明,证明代码的正确运行。

如果编写不当,函数式编程可以创建非常冗长和难以阅读的代码。例如,当组合多个函数时,重要的是考虑组合的函数数量以及组合链中每个函数的行为有多明显。

最糟糕的情况是想象一个由十个连续函数组成的函数链,在这个链中的每个函数都有您无法理解的名称(例如,a(b(c(d(e(f(g(h(i(j(input)))))))))))。唯一更糟糕的事情是,如果这些字母表函数的定义位于代码库中的十个不同文件中的不同位置,或者更糟糕,来自不同的导入库。

极限编程和测试驱动开发(TDD)

极限编程是一种开发方法论,类似于敏捷或SCRUM。它可以用来引用Kent Beck的书《极限编程解释》中描述的正式方法,或者更非正式地指涉该方法中倡导的一些编码实践。短语的非正式用法主要描述了方法中的测试实践,特别是测试驱动开发的概念。

测试驱动开发(TDD)是一种先编写功能软件之前编写测试的过程,与首先编写功能代码,再编写测试的方式相反。行为驱动开发(BDD)和验收测试驱动开发(ATDD)是类似的实践。

依赖注入

依赖注入是一种模式,其中特定对象、模块或代码块的服务依赖项是通过传递而不是实例化的方式提供的。例如,数据对象可以通过在配置文件中查找连接字符串并创建数据库客户端来实例化自己与数据库的连接。或者,一个调用块的父级可以创建数据库服务,然后将单个数据库服务传递给每个数据对象实例。

依赖注入的主要优点是它降低了服务与其依赖项之间的耦合度,从而在它们之间添加了一个有文档的接口。此接口允许使用该接口的其他实现,例如测试上下文中的模拟服务。

进行干净的依赖注入存在一些微妙之处。我建议您采用通常使用的并经过深思熟虑的框架或模式,以满足您的编程语言的需求。

领域驱动设计

领域驱动设计这个术语来自于埃里克·埃文斯在2003年出版的书籍《领域驱动设计》。其核心思想是创建一个模型,无论是面向对象设计中的对象还是数据库中的模式,都将业务域中的名词建模。这看起来可能很简单和直观;然而,对于复杂的业务领域来说,很容易出现代码在不一致的情况下对领域建模,或者以一种阻碍团队理解的方式对领域进行建模。特别是在面临更大和更复杂的问题时,我总是坚持让团队坐下来并达成一致,以一种一致的方式对问题进行建模,使用一致的术语来标识整个系统中的相同概念。

API合约

应用程序接口(API)有点像法律合同。

它在实施之前经过设计、调整和协商,并且双方都期望对方遵守合同以达到期望的结果。当您设计和实施API时,您承诺使用API的消费者将按照一定的方式工作。就像一个法律合同一样,您可能对API的功能有一个具体的想法,但是如果其他一方对细微差别的解释有所不同,您可能无法实现您的目的。API的细节真的非常重要,作为技术负责人,您的角色是确保团队以一致、高效的方式设计和构建API。

尽管如此,构建一个高质量的API是一个令人惊讶地复杂的任务。它需要考虑许多方面:设计接口,实现处理逻辑/数据的代码,测试功能,构建文档,处理版本/更改管理,随着API的更改而及时更新文档,以及使开发人员与API进行交互变得更加容易。做到这些事情良好的表现可能意味着在开发人员喜欢的API和阻碍实现以及延缓重要项目上线的API之间的区别。作为负责人,您可以通过治理和架构两个主要手段来确保处理这些事情。

API设计治理

构建API的每个要素都需要做出无数的决策。良好的API与糟糕的API之间的区别在于这些决策的一致性、可预测性和正确性。作为技术负责人,您的工作就是确保整个组织都有一个结构,以帮助开发人员构建彼此一致的API,以及使用适用于当前问题的常见模式并正确。

要实现这些目标,需要一种形式的治理体系。这可能是一组明确文档化的指导方针和标准,也可能是一组负责定期审查和批准所有API的人员。团队越大,您需要在流程和治理方面投入的时间和精力就越多,以保持高水平。

API架构

在实践中,您可能会遇到两种主要类型的API:基于HTTP和非基于HTTP。与任何工具一样,HTTP具有其权衡,不适合每个工作。因此,如果您的业务要求超低延迟、超高吞吐量/低开销或实时流应用程序,则很可能需要超越HTTP的解决方案。下面我将讨论一些基于HTTP的API类型,然后简要介绍一些您可能遇到的非基于HTTP的API。

基于HTTP的API

如果您正在构建Web或移动应用程序,或者甚至大多数系统后端,很有可能主要使用基于HTTP的API。

  • XML和SOAP API

在21世纪初,最常见的API模式是基于XML的简单对象访问协议(SOAP)。SOAP和其他基于XML的API样式在2020年代的初创公司中已经不流行了,但它们在传统行业中的大公司的遗留系统中仍然很普遍。您不应该构建新的SOAP或基于XML的API。

  • REST

REST(表述性状态转移)是一个通用短语,用于描述使用JSON通过HTTP作为API的方式。REST有时会与一种称为HATEOAS的模式相结合,该模式为REST API的内容/有效载荷提供了一套更正式的标准,但REST不包括有关如何建模JSON数据的正式或品牌化指导。REST API通常将单个名词建模为一个终端点,并使用HTTP动词(GET、PUT、POST、DELETE等)来确定对名词的操作。例如,GET/users将列出用户,POST/users将创建一个新用户,DELETE/users/123将删除ID为123的用户。

REST可能是您会遇到的最常见的API形式。REST有广泛而强大的工具生态系统,几乎每位工程师都很熟悉它。

  • GraphQL

GraphQL与REST类似,它使用JSON通过HTTP传输数据;但是,它并不依赖于HTTP动词。在GraphQL中,几乎所有操作都是POST,并使用结构化的查询和变更模式。

我认为GraphQL就像具有类型和自描述的REST。因此,GraphQL API通常配有自动生成的文档和模式浏览器。通过其模式系统,GraphQL还允许将多个服务的多个模式组合成一个更大、更强大和更复杂的数据图形,称为联合模式。Apollo公司提供了管理和扩展图形的复杂解决方案。

构建一个基于图形模型来对公司数据进行建模的好处,以及由于被迫设计模式而带来的良好习惯,有很多可说的。但是,没有哪个系统是没有代价的。因为GraphQL放弃了标准的HTTP动词,它与Web堆栈的某些元素不兼容。 GET请求缓存和开发人员工具仍然在努力很好地处理GraphQL请求。如果这些缺点对您的业务不构成重大问题,我强烈建议您查看一下apollographql.com,并考虑在API的内部用例中特别使用GraphQL。

非基于HTTP的API

通常,对于传统的同步请求/响应(也称为远程过程调用或RPC)样式的API,您将希望使用HTTP API,因为它的普及性。但是,对于一些不便于与HTTP直接映射的异步操作,存在几种常用的替代实现。

  • 存储系统

存储系统维护一个邮件收件箱(或一组邮件收件箱),用于接收消息,并提供一个接口供消费者读取消息,并有一定的保证。

一个常见的消息队列系统可以保证消息顺序(先入先出或后入先出),以及至少一次或至多一次传递。大多数云平台都有托管的队列实现,例如AWS简单队列服务(SQS)或Google云任务队列。

消息队列系统通常有明确的调用概念,这意味着当发布者创建消息时,它们会明确指定请求应如何处理或执行。相比之下,大多数发布订阅系统支持的是隐式执行。这意味着发布者不一定事先知道哪个系统将处理该消息,只知道发布/订阅系统将传递该消息。

  • 发布/订阅(pub/sub)模式

发布/订阅模式,简称pub/sub,允许设计系统,其中消息由多个来源创建,并通过各种模式交付给多个订阅者。发布/订阅关系可以建模为一对一(直接)、一对多(扇出)、多对一(扇入)和多对多。各种发布/订阅实现可以提供传递消息给所有订阅者、至少一个订阅者、至少一次等保证。与队列类似,有现成的解决方案,例如RabbitMQ,以及可以轻松扩展的云托管选项,例如Amazon简单通知服务(SNS)或Google云消息中心。

发布/订阅模式及其提供的保证非常强大。然而,折衷办法是实施需要一些细致的注意和细节来实现广告所承诺的保证。例如,实现一个订阅者需要密切关注消息确认语义,并仔细管理主题订阅,以确保正确的消息发送到正确的位置。

如果您在使用队列、发布/订阅或HTTP API之间犹豫不决,请根据原则选择简单的同步HTTP API。您之所以在这些实现之间犹豫,可能是因为异步系统提供的保证对您的实现并不重要,因此额外的复杂性对于您的初创项目可能不值得。

  • 作业系统

作业或定时作业是一种后端API类型,很少由发布者或客户端触发,而是由某种形式的计时器触发。常见的例子包括每晚的数据清理任务或每周发送电子邮件摘要/通知。有关作业的一些最佳实践:

  • 使用由其他人维护的作业系统,不要自己构建。
  • 在选择作业系统(或自己构建作业系统,如果必须这样做)时,请确保它具备以下功能:
    • 对每个作业执行进行记录;
    • 允许配置失败的作业进行重试;
    • 在作业失败时进行通知。很常见的情况是工程师设置了一个计划作业,第一天可以工作,到了第十五天就失败了,而且在第三十天之前没有人注意到;
    • 提供查看作业和作业状态的界面;
    • 允许将作业配置存储为代码或配置在源代码控制中;
    • 允许根据需要在开发和生产环境中运行作业,以及在每个环境中进行简单的测试。

文档

对于API而言,拥有全面、清晰和最新的文档与如何构建和维护API一样重要。一些优秀的API文档的关键特征包括:

  • 始终与实施保持最新状态;
  • 文档了所有可能的输入及其类型;
  • 文档了所有可能的错误;
  • 方便其他工程师阅读和导航。

使用包含API文档生成的系统是一个好主意。否则,几乎不可能始终如一地实现所有这些目标。如果您正在构建REST API,我强烈建议您使用OpenAPI设计API(一个描述API的YAML或JSON文档)。在大多数语言中,都有可以使用OpenAPI规范生成控制器/路由以匹配规范并/或生成测试框架的软件开发工具包(SDK)。此外,还有一些在线工具,例如stoplight.io和readme.com,可以支持OpenAPI文档并生成美观易于导航的文档。

如果您正在使用GraphQL,GraphQL Playground或Apollo Studio explorer可以提供对丰富的类型文档的合理替代。但我建议您仍然建立单独的API文档页面,使用像readme.com这样的工具或手动创建,作为入门指南。内置的GraphQL文档缺乏有关身份验证运作方式的描述,并且在提供API中数据之间的关系的空间方面做得不好。

您需要在其他地方填补这些空白。

使用OpenAPI或GraphQL的另一个好处是,生成的API规范不仅可以用于文档生成器和测试框架,而且还可以用于开发人员的IDE,例如Insomnia或Postman。这些IDE允许开发人员在不编写代码的情况下快速与API进行交互以验证功能。正式的规范还可与代码生成工具一起使用,以确保代码中的类型一致性。

幂等性

当执行相同请求多次与执行一次具有相同效果时,API请求被称为幂等。幂等性是构建健壮系统和避免数据损坏的重要概念。与所有事物一样,幂等性为系统提供了有用的保证,但这也会增加后端系统的复杂性。

在REST API中,普遍认为除了POST以外的每个HTTP动词都应该是幂等的。例如,按照定义,GET请求应始终针对同一输入返回相同结果(除非底层数据发生更改)。一般来说,PUT请求正在修改现有对象,应自然而然地是幂等的。然而,在大多数系统中,对POST请求的多次调用表示要创建多个对象的意图。

幂等键

对于REST中的HTTP POST请求和GraphQL变更API,标准/规范并不提供幂等性。如果您希望客户端能够重试此类请求并具有幂等行为,则应实现“幂等键”模式。幂等键是一个任意字符串,由客户端提供(可以作为HTTP头部或可能在GraphQL中作为输入变量),后端使用其来去重传入的请求。这要求后端存储幂等键,并且还存储带有该键的请求的响应,以供稍后提供给客户端。

请注意,实现幂等键是非平凡的,因为它将需要额外的数据库写操作、围绕捕获请求响应的逻辑,并处理同时到达的重复请求的并发/锁定问题。如果幂等性在您的应用程序中很重要,例如处理财务交易,我鼓励您采用一个后端API实现,它在成熟的基础上提供了一个强大的幂等性系统,而不是从头开始构建。

数据和分析

大多数初创公司至少有三种不同类型的用于业务的数据:

  • 事务性数据
  • 分析型业务智能数据
  • 行为数据

这些类型的数据的大小不同,读/写模式不同,并且需要不同的工具来进行可视化和深入洞察。

关于所谓的大数据一词。作为一家初创公司,您很有可能在技术上没有所谓的大数据,需要以无限规模(或 Web 规模)的架构为基础进行架构设计。具备合理的硬件配置和良好的数据模型设计的典型现成数据库完全能够处理数千万行和数百GB的数据,并具有可接受的性能。大多数大数据解决方案,例如数据流水线或数据仓库设备,涉及重要的附加设置复杂性、延迟和成本,并且可能对您的初创公司而言过于复杂。为了简单起见,只有在您能够提出一个令人信服的观点证明常规数据库(例如 PostgreSQL)无法胜任工作时,才应考虑大数据解决方案。换句话说,不要过早地优化您的数据库架构。

事务性数据

交易数据是可以为您的应用程序提供动力的数据,通常是您的主要NoSQL或SQL数据库。交易数据需要非常低的延迟和高可用性,并且与其他形式的数据相比总体大小较小。我的建议是选择一个现成的SQL或NoSQL解决方案,最好是像MongoDB Atlas或Google Cloud SQL这样为您托管的解决方案。以下是您在生产数据库中可以考虑的一些有用功能:

  • 一键式按时间点恢复
  • 定期备份并一键恢复
  • 用于负载均衡的只读副本
  • 多区域复制和托管以提高可用性
  • 基于事件的审计日志记录
  • 自动磁盘扩展/收缩
  • 连接/IP级安全性
  • 资源(CPU、RAM、磁盘、网络)监控和报警
  • 一键式CPU/RAM升降级
  • 慢查询监控

分析业务智能数据

业务智能(BI)是用于深入了解用户行为的数据,通常是从您的交易数据中获取的。在早期阶段,您通常可以直接在交易数据库上运行业务智能查询。随着数据量和查询复杂性的增加,这变得更加困难,因为它给需要高可用性和低延迟的系统增加了额外的负载。因此,解决方案要么是查询交易数据库的只读副本,要么是通过数据流水线将数据复制/转换到另一个数据存储系统。

构建数据流水线和数据仓库是一本关于自身的书,并且技术水平一直在发展。我只有一些高层次的建议:

考虑使用Snowflake、Databricks或Google BigQuery等企业级数据解决方案作为您的主要业务智能数据仓库。这些工具将改变游戏规则。尤其是无服务器仓库(BigQuery、Aurora)非常易于设置,无论数据大小如何,延迟基本保持一致,对于早期/中期初创企业来说成本效益很高。

在现代,初创企业不需要构建和托管复杂的数据流水线架构。ELT(提取、加载、转换)和ETL(提取、转换、加载)工具现在可以完全在企业数据库数据湖/仓库中运行,而dbt等工具提供了可重复性、可测试性和代码化流水线功能,使数据流水线的运行更加可控。

考虑使用Looker、Domo或Preset等托管或云原生解决方案可视化数据。

确保您的工程和产品团队与负责数据和业务智能的团队紧密合作。在产品过程的早期阶段引入数据的视角将在未来节省很多麻烦,并根据量体裁衣的原则仅需新建数据架构一次。

行为数据

行为数据,也称为行为分析事件,描述了用户如何使用您的应用程序。行为数据通常具有相对较高的数据量,具有相对有限的架构,并且最好与强大的可视化软件结合使用。

总体而言,您希望从应用程序中获取行为数据并将其发送到多个来源。这带来了一个路由问题:您有一个单一的数据源(您的应用程序),但您希望将事件发送到多个位置。几乎普遍采用的解决方案是Twilio的Segment平台,但也有一些新兴的可替代解决方案,例如RudderStack等名为客户数据平台(CDP)。CDP可以接收来自您的应用程序的数据,然后将数据发送到您的数据仓库和尽可能多的其他SaaS平台。

行为数据和您的应用程序生成的交易数据之间的一个重要区别是精确性。大多数行为数据是有损的,因为用户使用广告拦截器,请求被丢弃,或者防火墙阻止了它们。事件之所以无法从客户设备传输到CDP,原因有很多。这并不意味着行为数据无用,但是意识到其有损性应该为您对数据的预期和查询时的用例设定限制。如果您需要准确的数字,请预期从您的BI平台和交易数据中派生这些数字。

架构设计的一般提示和最佳实践

让我们在这一部分中提出一些总体建议,用于设计您的架构。

将业务逻辑放在后端

在构建应用程序时,您经常面临的选择是逻辑应该放在哪里:在客户端(例如Web浏览器、移动设备、物理硬件)上还是某种形式的后端服务器上。对于某些类型的逻辑(例如与身份验证、价值计算、反作弊/篡改机制相关的逻辑),这是一项明确要求。对于其他大多数逻辑,出于以下原因,将它们放在后端仍然是个好主意:

后端通常比客户端更易于测试,因此您可以更自信地确认后端上的业务逻辑的正确性。

后端上的逻辑越多,客户端就越轻巧,同时还可以为多个平台生成客户端,这些客户端可以利用单个逻辑源来减少代码重复。

后端上的逻辑无法被客户端篡改或修改。

将服务外化

从您的后端到其他后端或从您的后端到前端的API应被视为可以被第三方使用的通用目的API。这迫使您遵循一些良好的设计习惯,包括确保接口本身可以理解(领域驱动设计),并使用合理的身份验证机制和适当的高层所有权抽象来设计数据。另外,万一您将来希望将服务外化,这样做的路径将会更短。

尽量少使用语言

每种编程语言都伴随着关联的构建系统、依赖管理系统、程序最佳实践和接口。您的团队应该付出很大努力,确保您的主要语言和生态系统可以很好地集成并在本地开发人员、测试环境和生产环境中正常工作。

对于您在堆栈中添加的每种额外语言,您将需要复制所有这些工作,并且在运行时之间无法共享代码。在允许将额外语言添加到堆栈之前,您应该能够建立一个强大且无可挑剔的论据,证明新语言的好处超过了运营和维护的负担。否则,您最好不要添加它。

工具

软件工程的工具生态系统和模式不断发展和变化。您很有可能会被自己或团队成员引导去改变您的工程方式,比如采用新的库、框架、语言或模式。每个这样的变化只会导致一个拼凑而不经过深思熟虑的架构。相反,忽视所有变化将使您的代码库变得陈旧,随着时间的推移,它将变得效率更低,新加入的人才将很难在其中工作。正确的做法是正式化改变技术栈的过程,并提供一些规范,以激励您的团队对工具变化保持好奇和深思熟虑的态度。

实施内部技术雷达

Thoughtworks是一家总部位于旧金山的领先软件咨询公司,他们发布了一个名为Technology Radar(ctohb.com/radar)的工具,评估他们每年看到的数百个项目。他们将新工具、技术、模式和语言(他们称之为blips)根据在实际世界中的有效性将其分为四个类别之一。

这些类别是保持(hold)、评估(assess)、试用(trial)和采纳(adopt)。

如果您以前从未阅读过Thoughtworks雷达,请强烈推荐阅读,作为了解正在进行的事情的一般入门知识,同时也是您自己团队流程的灵感来源。

我倾向于遵循Thoughtworks的做法,并实施一个内部技术雷达来平衡保持工程师积极性和代码库相关性之间的挑战。与Thoughtworks评估新技术的普遍吸引力相比,我的方法使用相同的四个级别评估blips,以及它们对我们组织的适应性和有效性。具体而言:

  1. 有人提议使用新的工具、技术、平台或语言(blip)。该提案首先被归类为评估。提议人必须在技术文档中提出论据,说明新的blip对业务已经选择的项目或创新冲刺中的实验(参见Cooldown/Innovation Sprints,第163页)能提供实质性的好处。然后,如果获得批准,它将移动到试用阶段。
  2. 开发人员在一个项目中使用新的blip,这个项目要么是由业务选择的,要么是在他们的创新冲刺窗口中选择的。项目结束时,作者将撰写一份后续文档,描述他们对blip的体验,包括优点和缺点,以及blip与公司工具生态系统的兼容性如何。
  3. 基于试用的结果,团队作为一个整体将要么采用这个blip,使该blip可以在不需要进一步仪式的情况下被整个团队使用,要么将其保持,这将需要一个新的试验和评估来再次使用它。如果一项业务项目的试验失败,建议团队仔细考虑是否从该实施中删除该blip,以避免将来的维护问题。

在大多数情况下,我发现当一个blip试验失败时,它往往在早期就失败了,主导项目的工程师在最终交付的实施中不会包含该blip。

朴素技术

朴素技术是由Dan McKinley提出的一个概念,详细介绍在ctohb.com/boring。核心思想是您团队的任务是提供支持业务的功能,而这通常不依赖于使用新的花哨工具。实际上,使用一些不朴素的东西通常会带来许多隐藏成本,只有当您的团队充分意识到这些成本并相信好处更大时,才应该采用新工具。根据朴素技术的描述,总成本 = 维护成本/速度好处。要考虑的一些隐藏成本包括:

  • 不完整、不准确或不成熟的文档
  • 工具/技术周围的生态系统不完善,包括SDK和与其他工具的集成
  • 遇到缺陷或缺失功能/特性的可能性更高
  • 需要团队成员额外的培训来采用新工具
  • 维护该工具或软件包的负担,包括修补安全漏洞等。

工具成本

对于现代的初创企业来说,SaaS的开销很大。您的公司很可能不例外,因此当您发现在Series B融资之前,在基础设施和工具上花费了一个人头以上的时间时,不要感到惊讶。

预算

已有一些公开发布的基准,用于各个公司阶段的SaaS和工具开销,作为公司收入或总支出的百分比。

没有一个精确的基准,但似乎典型的SaaS成本挂账(COGS)在收入的10%到30%之间。

了解您的支出,并密切关注成本的增长。当您发现自己在AWS上意外地运行了几台机器,并将1万美元增加到您的年度云托管费用时,不要感到惊讶。大多数云平台都内置了预算功能,因此无理由不使用。如果您使用基础设施即代码的方式,可以轻松地设置模块,对于每个新的云系统部署,同时还将在同一时间自动应用一个云预算,该预算将监控并警示该特定系统的成本。

随着时间的推移,SaaS成本通常会增长,无论是因为基础设施增长,还是因为您发现了一个新的SaaS供应商,可以为您的团队节省时间。我建议不要以成本为理由来避免采用典型的SaaS工具(每月数百美元的成本范围)。相反,我建议将定期增长纳入您的SaaS成本预测中。

跟踪

您应该跟踪组织在工程工具上的支出,包括IDE、SaaS和基础设施(云平台)。您可以在电子表格中手动完成这项任务,或使用SaaS管理平台(SMP)进行跟踪。这些解决方案由诸多供应商提供,例如BetterCloud、Zluri和Vendr,它们与您的信用卡或银行链接,并自动对现金支出进行分类。

DevOps

维基百科将DevOps描述为将软件开发和IT运维结合起来的一组实践。它旨在缩短系统开发生命周期,并提供高质量的持续交付。

对我来说,这个定义中的关键是DevOps旨在缩短软件开发生命周期,换句话说,DevOps是实现更广泛团队生产力的一种手段。如果您尚未专门考虑DevOps,您可能在某种程度上对DevOps的优先级或投资不足。这不仅仅是我的观点,它已被广泛接受,认为高质量的DevOps是推动整体工程速度的关键因素。

四个关键指标(DORA)

Thoughtworks Technology Radar 2022年最受好评的blip是四个关键指标。这些指标由Google Cloud内的DORA(DevOps Research and Assessment)团队描述,并且来自为期七多年的研究项目,验证了结果及其对技术、流程、文化和定量结果的影响。这四个指标如下:

上线时间: 从提交到在生产中运行的代码所需时间

部署频率: 代码发布到生产环境/最终用户的频率

恢复平均时间(MTTR): 在发生故障/缺陷后恢复服务所需的时间

变更失败百分比: 需要热修复、回滚、打补丁等的生产发布的百分比

这些指标共同度量了您的团队部署软件的自信程度。在所有四个指标上得分高需要在自动化、DevOps、测试和文化方面投资。正如Thoughtworks迅速指出的那样,从这些指标中获得价值并不一定需要高度详细的工具、指标或仪表板。DORA发布了一份快速检查调查(ctohb.com/dora),您的团队可以进行该调查,以粗略了解自己的进展。还有许多工具可以轻松入门,可以产生足以支持您进展的数据质量,例如LinearB或Code Climate。

DevOps的以下子部分介绍了有助于改善这些指标的概念、学科和关注领域。

可复现性

部署代码是一项高度微妙的活动,需要极高的精确性。配置文件中的一个错位字符就可能导致服务无法启动。更糟糕的是,对于大多数工程师来说,调试DevOps问题是一项缓慢而痛苦的过程,确定并修复那个单个字符的错误可能意味着可能花费几个小时的时间来调试和修复。我们都是人类;这些错误是不可避免的。由于在DevOps中这样的错误非常昂贵,因此必须采取措施尽量减少人为错误的机会。减少人为错误在DevOps中的频率和影响的关键组成部分是可复现性的概念。

可复制性意味着我们有能力再做一次某件事,而且成本低廉,并且能够保证与第一次做的完全相同。在 DevOps 中,可复制性需要自动化和工具支持。可以说,在改善可复制性和加速开发时间方面,容器化是 DevOps 工具中最重要的工具。紧随其后的是基础设施即代码(IaC)的理念。鉴于这些技术非常基础,我将花一些时间在这里介绍它们。

容器化

解释容器在 DevOps 上的角色最常见的方法是考虑到其名称来源:航运集装箱。在集装箱标准化之前,如果你想要横越大洋运输货物,你需要以各种形式包装你的货物,如将其放在托盘上,存放在箱子或桶中,或者简单地用布包裹起来。这种以不同方式打包的货物装载和卸载是低效且容易出错的,主要是因为没有一种起重机或手推车可以有效地移动所有货物。

将这种杂乱无章的方式与使用标准化集装箱部署相比较,在这种部署方式中,船只和港口操作人员可以使用一种统一的形状,使用标准设备和承运商,以及一种灵活的统一包装形式来处理他们所有的货物。从历史上看,标准化集装箱的使用开启了一个范式转变,将全球运输成本降低了若干数量级。将软件打包到一个标准化的容器中,并能以相同的方式在任何系统上运行,为能力和效率方面提供了类似的提升。

你将最常接触到容器的方式是通过一个名为 Docker 的软件系统。Docker 提供了一种声明性的编程语言,让你在一个名为 Dockerfile 的文件中描述你希望如何设置系统,例如需要安装哪些程序,哪些文件放在什么位置,需要存在哪些依赖关系等。然后,你可以将该文件构建成容器镜像,该镜像提供了根据你的 Dockerfile 指定的整个文件系统的表示。然后,该镜像可以移动到并在任何其他具有 Docker 兼容容器运行时的机器上运行,保证每次都在一个隔离的环境中启动相同的文件和数据。

容器管理最佳实践

以”构建一次,随处运行”为目标设计容器

只需构建一次容器(比如在 CI 中),以便它可以在各种环境中运行,包括生产环境、开发环境等。通过使用单一的镜像,可以确保相同的代码和相同的设置从开发环境顺利过渡到生产环境。

为了实现在任何地方运行你的容器,将环境之间的差异提取到运行时容器环境变量中。

这些变量可以是密钥和配置,如连接字符串或主机名。或者,你可以在镜像中实现一个入口脚本,该脚本从中心的秘密存储库(如 Amazon 或 Google 的秘密管理器、HashiCorp Vault 等)下载必要的配置和密钥,然后调用你的应用程序。

运行时秘密/配置下载策略的另一个好处是它可以在本地开发中重复使用,避免了开发人员手动获取密钥或要求其他开发人员发送密钥文件的需要。

在 CI 中构建镜像

为了保证可复制性,我建议你使用自动化方式构建镜像,最好是作为持续集成的一部分。这样可以确保镜像的构建方式是可重复的。

使用托管注册表

一旦你开始构建容器镜像并进行移动,你会立即希望在管理已构建镜像方面保持有序。我建议给每个镜像打上一个唯一的标签,该标签可以从源代码中获得,可能还包含一个时间戳(例如,构建镜像的 Git 提交哈希),并将镜像托管在一个镜像注册表中。Dockerhub 提供了一个私有注册服务,所有主要云平台也提供了托管的镜像注册服务。

很多托管注册服务还会提供与图像注册相关联的漏洞扫描和其他安全功能。

尽可能减小镜像的大小

更小的 Docker 镜像上传到 CI 的速度更快,在应用程序服务器上下载更快,并且启动更快。从运营的角度来看,上传一个 50MB 的镜像与上传一个 5GB 的镜像之间的区别可能是启动一个新应用服务器的时间从五秒变成五分钟。这意味着在部署时间、恢复/回滚时间等方面增加了五分钟。这可能看起来不算多,但特别是在修补漏洞的情况下,或者当你管理数百台应用服务器时,这些延迟会累积并对业务产生真正的影响。

Dockerfile 最佳实践

Dockerfile 中的每一行或命令生成的内容都被称为一个 layer —— 实际上是整个镜像硬盘的快照。后续的每个 layer 存储着上一层到当前层的差异。容器镜像是这些 layer 的集合和组合。

因此,你可以通过保持单个 layer 的大小较小来减小容器的总体大小,并通过确保每个命令在移动到下一个命令之前清理任何不必要的数据来减少 layer 的大小。

保持镜像大小较小的另一种技术是使用多阶段构建。多阶段构建的描述有些复杂,但你可以查看 Docker 自己的文章了解更多信息。

容器编排

现在你拥有了具有可复制和较小规模的镜像,并在托管注册中进行管理,接下来你需要在生产环境中运行和管理它们。管理包括以下方面:

  • 在服务器上下载和运行容器
  • 在容器和其他服务之间建立安全网络连接
  • 配置服务发现/DNS
  • 管理容器的配置和密钥
  • 根据负载自动缩放服务
  • 对于容器管理,有两种常见的方法:托管式和自管理式。
托管式容器管理

除非你的需求很独特或规模非常大,否则通过选择一个托管解决方案来管理生产容器,你将获得最高的投资回报率。这些解决方案的一个普遍而公正的批评是,它们往往比自我管理的选项更昂贵,并提供更少的功能和更多的限制。与此相对,你将获得更少的开销和更少的复杂性,这对于大多数创业公司来说是一个非常划算的选择。大多数小团队缺乏有效自我托管的专业知识,因此,自我托管要么要求现有团队成员投入大量的时间,要么迫使你在早期雇佣一名昂贵的 DevOps 专家。为了避免这些问题,额外投资每月1000美元可能会带来非常好的投资回报。

一些常见的托管式容器平台包括 Heroku、Google App Engine、Elastic Beanstalk、Google Cloud Run。Vercel 是另一个受欢迎的托管后端解决方案,尽管它不是根据此处描述的方式运行容器。

自管理/Kubernetes

自我管理容器的流行解决方案是 Kubernetes,通常简称为 K8s。Kubernetes 是一个非常强大和灵活的系统,因而也很复杂。它的学习曲线很陡峭,但如果你需要自我管理容器,它的好处和投资回报是值得的。

如果你考虑采用这种方式,我强烈建议你不要在项目进行中学习 Kubernetes。特别是对于团队负责人来说,这对于投入和有效地临时完成工作来说是太多了。相反,我建议你购买一本关于 Kubernetes 的书,并花一周或两周的时间阅读它,并设置自己的沙盒环境,以便在开始专业项目之前快速上手。另外,寻找一个对 Kubernetes 有良好了解的顾问或导师,他们可以加速你学习工具的过程。

ClickOps vs. IaC

ClickOps 是指使用用户界面配置云基础设施的过程,而不是使用提供的 API。随着基础设施的增长,系统中的细微差别和详细零碎很快就会超过使用 ClickOps 复制它的能力。ClickOps 对于原型或概念验证来说是可以的,但是当真正开始构建生产环境和镜像开发环境的时候,使用 ClickOps 很快就会导致相当大的挫败感和成本,以及功能的局限性。与 ClickOps 的另一个选择是基础设施即代码(IaC)。

有几种工具和框架可以用来定义 IaC,其中主要的一个是 HashiCorp 的 Terraform。Terraform 使用 HashiCorp Config Language(HCL),一种声明性配置语法,让工程师们定义他们想要的资源以及如何配置这些资源。然后,Terraform 代码应该和其他代码一样进行管理,使用源代码控制和同行审查的实践。一旦获得批准,Terraform 可以为你生成基础设施变更计划,并与你选择的云服务提供商一起应用这些计划。我无法强调 Terraform 有多么易用、强大和可维护,并且通过从 ClickOps 迁移到 IaC 可能获得的投资回报。

持续集成

持续集成是将新代码自动纳入项目的过程。这可能包括对新代码运行静态分析、运行测试、构建代码以及生成所需的构建产物(例如容器镜像)。大多数创业公司使用托管的持续集成平台,如 GitLab runners、GitHub actions、Bitbucket pipelines、Jenkins 或 CircleCI 来执行持续集成活动。

一些持续集成的最佳实践:

  • 确保团队了解持续集成系统,并能够轻松地添加新的要求、更新需求,以及排除故障。
  • 确保构建是一致且可确定的。不可靠或不稳定的构建将极大地影响生产力和时间。
  • 尽量减少构建时间。对于大多数团队而言,一个好的目标是使持续集成需要的时间少于十五分钟。
  • 了解你的持续集成工具在保持构建速度方面的能力,包括构建缓存、构建产物以及并行运行作业。
  • 构建可能变得复杂。尽量在你的CI代码中保持DRY原则,即在构建流水线之间尽可能重用代码。
  • 在构建访问密钥时保持一致性。使用云密钥管理器,或在必要时构建环境密钥,并尽量避免混合使用。应该有一种明显且一致的方式来处理配置和密钥。

持续部署

在项目的初期阶段,部署新代码相对比较简单。此时,代码量不多,架构复杂度也不高。然而,随着需要管理依赖关系、依赖服务、CDN、防火墙、构建产物、构建配置、密钥等的需求增加,部署过程变得复杂起来。随着这些需求的累积,很容易忽视自动化,简单地依赖于专门的、高度信任的个人作为发布管理人员。有无数的团队按照这种模式进行,我向你保证,其中的大部分发布管理人员都提前在日历上圈出了发布日期,并且对这些日期不可避免地产生的压力、长时间工作和沮丧感都感到恐惧。

幸运的是,给那些每月(甚至更长时间)的发布日带来错误和压力的这个中心化过程有着根本性的解决办法。即每天发布,甚至每小时发布!这必然符合技术文化的十大支柱之一(见第138页),即频率降低难度,更频繁的发布将强制你的发布管理人员和团队自动化部署中的困难部分。通过足够的迭代,发布可以完全自动化,并通过足够的测试给你在新变更上有信心,从而达到每个代码变更集触发新的发布的程度,这被称为持续部署。

除了通过自动化消除发布流程的复杂性之外,更频繁地发布意味着每个发布的代码量更小。较小的改动更容易由其他开发人员审查。较小的更改,仅仅因为较小,为缺陷提供了更少的机会。

自动化发布过程还意味着更好地处理变更的回滚或从生产问题中恢复。这也可以视为恢复平均时间 (MTTR/meantime to recovery)。

总结起来,自动化发布意味着代码发布更快(减少前期时间),可以更频繁地部署(增加部署频率),并改善 MTTR。这是 DORA 指标(请参见第208页)中的三个关键指标的提升之一!

在我直接或间接与的公司中,至少有十几个团队投入了部分或全部地进行持续部署。这并不总是一条直线的旅程,不是一夜之间能实现的,往往会有充分理由的反对意见。然而,在任何情况下,当团队回顾花在这些方面的时间——无论是三周、三个月还是两年后——不管怎样,在文化、整体产出和团队的速度方面,差异都是不折不扣的转变。

功能分支环境

功能分支环境是一个托管环境和基础设施的集合,它在内部对你的公司可用,运行来自特定分支的代码。功能分支环境对于验证代码在类似生产的环境中是否可用以及使公司的团队成员能够使用或测试更改而无需构建和运行代码非常有用。不要低估人类的懒惰;每多一步测试和验证你的代码所需的步骤,意味着他们会更少进行测试。检出代码、安装依赖项和启动服务器比访问自动生成的功能分支 URL 要消耗更多的工作量,因此功能分支将被更多地使用。

功能分支环境还解决了仅有一个单一的暂存环境的团队所遇到的冲突问题。我提倡给每个分支分配自己的暂存环境。

功能分支环境的一些建议:

  • 自动化是关键;手动设置功能分支环境几乎总是不切实际的。
  • 在可能的情况下,使用诸如 Vercel 或 Firebase 等系统,将功能分支环境作为一项一流的功能来尽量减少设置和维护成本。
  • 仔细考虑如何处理功能分支环境中的数据。你需要回答以下问题:
    • 每个后端功能分支环境是否都有一个新的数据库?我的建议是是的。
    • 功能分支环境是否使用生产数据?我的建议是不使用。相反,使用一个种子脚本为开发人员生成与生产数据类似数量的数据。对于调试,最好有一个复制、清理和恢复生产数据的过程,以便开发人员在必要时进行调试。
    • 功能分支环境中的数据库模式更改/迁移如何处理?
    • 在面向服务的架构中,如何在功能分支环境中进行服务发现?通常情况下,你可以有一组常用的服务,并部署仅测试的服务的功能分支版本。我建议每个服务都有一个始终运行的集成环境,称为集成。让所有功能分支引用其他服务的集成环境。每个集成环境更新都应该是一个生产更新,但你也应该允许开发人员将功能分支代码临时部署到集成环境中进行跨服务的集成测试。

阅读完这份关于你所关注的问题的列表后,你可能会感到担忧,觉得设置和维护功能分支的负担很重。确实,正确设置功能分支并不便宜,但它提供的价值在于提高测试能力,并减少验证不同类型软件更改所需的后勤开销。

管理 DNS

了解域名系统(DNS)的工作原理,了解如何管理DNS以及其对公司的安全影响,是初创企业CTO通常要承担的关键工作。如果您还不熟悉DNS的基本原理和不同的记录类型,我建议您花几分钟浏览维基百科,以便对这个主题有一个基本的了解。

您还应该了解DNS在您的组织中用于电子邮件的方式。特别是要熟悉发件人策略框架(SPF)记录和DKIM / DMARC。

您还应该使用基础架构即代码(IaC)设置DNS记录。我看到过很多公司,DNS完全由一位高级职员管理,只允许他们更新区域记录的两种身份验证,并且当该人不在时,没有后备机制来管理网站。

更好的解决方案是使用Terraform设置DNS(它与所有主要的DNS提供商都有集成),然后使用源代码控制来管理DNS记录,从而赋予各个开发人员以负责任的方式添加新的记录,而不需要依赖于任何人。

将代码发布与功能发布解耦(功能开关)

从高层次来看,功能开关是一种可以在不改变实际代码的情况下改变系统行为的开关。我强烈推崇使用功能开关,特别是因为它们使得团队可以为发布代码和发布功能设立单独的流程和时间表。Pete Hodgson在ctohb.com/hodgson上对功能开关进行了详细的解释。

《四个关键指标》(The Four Key Metrics)提倡尽可能频繁地发布代码。这样做会为您的工程流程的健康带来很多积极的益处。然而,一个自然的担忧是,当代码完成后,您的业务可能还没有准备好立即启用特定的功能。开发和发布计划之间可能存在不同步的原因有很多,比如需要与市场激活的时间协调,创建客户支持文档,等待监管批准,停顿进行内部沟通等等。功能开关使得工程团队能够专注于尽快可靠地发布代码,并将功能启用的协调问题委托给其他团队负责的非常规流程。

系统监控: APM 和 RUM

应用性能监控(APM)和实时用户监控(RUM)是两种帮助团队了解生产环境中应用程序的性能并识别/预防用户面临故障的工具。

APM工具通常位于或与应用程序在生产环境中运行,并从后端的角度提供资源使用情况、请求延迟和吞吐量的分析和洞察。RUM是一种外部工具,它假装是一个用户,并从前端的角度或实际用户的角度提供延迟的分析。

如果你必须选择一个(这些工具通常非常昂贵,所以你可能被迫选择其中一个),选择那个更有可能对你的应用程序有问题的死角进行覆盖的工具。如果你有很多用户,并且每个次要错误或边缘情况都会导致实时投诉的泛滥,那么APM监测后端负载可能比RUM产生冗余警报对用户更有价值。然而,对于大多数处于种子或增长阶段的创业公司来说,你的应用程序使用不一致,尤其是在处理边缘情况时,这种情况下,RUM可能比大部分空闲的后端上的APM更有价值。

在这个领域有一些常见的工具,比如New Relic,Datadog和Akamai mPulse。

测试

想象一下,与其测试软件代码,你的工作是成为一个市政桥梁的检查员。桥梁已经建好了,现在你的工作是检查它,决定是否让它对公众开放。你会检查桥上的每一个螺栓和铆钉吗?这样做可能会让你对桥的安全性有很高的信心,但同时也会花费很长时间,并导致市长计划下周开放桥梁的计划搁浅。

另一个合理的策略是确定哪些要素对满足指定的安全因子至关重要,并测试/检查这些要素。也许只有每隔一个铆钉,再加上全部的电缆和结构混凝土。

同样,在软件工程测试中,目标不是为了追求测试覆盖率本身,而是为了提供对软件按照意图进行功能验证的信心。

有效的软件测试并不总是要求百分之百的代码覆盖率。一个良好的软件测试套件的标准是给团队带来信心,即当构建绿色并且所有测试通过时,软件已经准备好发布给最终用户。这可能意味着百分之百的代码覆盖率,也可能是百分之三十的代码覆盖率。确切的数字取决于您来确定和监控,并且由于发现测试无法提供相同的信心(相反,如果发现您投入过多,即花费了大量资源在测试套件上,但错误仍然经常出现)时,这种努力的程度随着时间的推移可能会发生变化。

测试/质量保证团队

根据团队的规模,您可以没有专门的测试团队,一个测试团队负责一种或多种类型的测试,或多个测试团队负责各种软件测试。不管谁来进行测试,认识到软件测试是一个复杂的过程,细微之处很重要。为了有效地进行软件测试,不管是编写代码的测试人员还是收到代码并开始测试的测试人员,都必须对代码/软件的预期行为有深入的了解。您的角色是建立您的团队,使他们能够相互理解。为此,确保团队共享目标/KPI,您的流程在开发人员和测试人员之间有稳固和持续的沟通,并监控团队之间是否存在健康、高效的关系。

测试质量

在深入了解不同软件测试范式的细节之前,值得思考一下软件测试的目的是什么,因此什么样的测试才是好的,相反什么样的测试才是不好的。

定义不好的测试很简单。不好的测试对您的团队产生的成本高于收益。一些不好的测试的常见特征包括:

  • 为了维护和修复测试而对合法代码更改付出的时间比通过发现的错误节省的开销更多。
  • 测试的误报率高或不一致,导致CI变慢,给开发人员带来沮丧,并导致重复运行无意义故障。
  • 测试设计不合理,实际上验证了错误代码。
  • 套件测试验证代码是否正确,并且误报率低,但是它非常复杂和难以理解,只有编写测试的人才能添加新测试,其他看到它的工程师都会感到头痛。
  • 测试不能让人对评估的代码准备好发布给最终用户感到有信心。

有了这个概念,相对而言,很容易通过好的测试的特性来看出来。好的测试应该:

  • 在本地和共享的CI环境下运行起来既容易又快速
  • 能够可靠地运行且产生相同的结果,易于扩展
  • 对于基础逻辑更改而言,易于进行重构或更新
  • 与底层代码模式适当地结合,以适当的高度(可以这么说)进行测试,以捕获重要的破坏
  • 任何工程师都能够理解和工作

评估测试方法的一个最好的方法也是最明显的方法:用简单的情感分析询问您的团队对测试的感受。结果往往是非常二元的,即测试是团队依赖的安全源,而且自然地会进行增强,因为它们显然是增加净价值的;或者团队被动地,甚至主动地讨厌测试,因为它们对生产力的影响超出了明显的价值。

测试内容

您的团队应该以提高对系统正常工作的信心为目标,同时尽量减少在有机系统增长中长期维护这些测试的额外负担。最小化这种痛苦的一般模式是对公共接口进行测试。公共接口应该经过深思熟虑,并且随着时间的推移变化很小。

它们也是软件的使用者实际经历的快乐路径。

因此,公共接口上的测试应该在时间上变化较小,并且提供信心,表明重要的代码部分正常工作。

软件的公共接口可能因项目而异。对于许多项目来说,它将是一个实际的基于HTTP的API;对于一些项目来说,它将是库或内部服务中的一组函数/类;对于其他项目来说,它可能是用户界面。

测试类型比较

软件测试可以分为以下几类/范式:

  • 单元测试
  • 集成测试
  • 端到端测试
  • 手动测试
  • 半自动化测试

区分这些测试类型的属性如下:

代码规划: 在实际编写代码时需要多少事先考虑来确保它在这种测试范式下是可测试的。

测试范围: 每个测试可以同时评估多少内容。

更改粒度检测: 测试可能检测到和导致失败的代码更改的大小或类型。

运行成本: 运行测试的速度或成本,无论是时间还是金钱。

添加工作量: 添加额外覆盖的工作量。

设置工作量: 建立有效测试套件所需的工作量。

以下图表总结了这五种范式与这些度量标准的对应关系。请注意,没有一种完美的测试范式;每种都有其权衡,我鼓励您仔细考虑哪种权衡对您的公司和代码库来说是合理的。正确的答案通常是在应用程序中为每种测试范式添加最多价值的方式的综合。

TODO: 插入书中的表格

单元测试

当有人谈论软件测试时,单元测试通常是首先想到的。它通常是学校教学和教科书中所讲授的内容,且在实际应用中通常会投入最多的工作。鉴于所有这些,您可能认为单元测试是最好的测试类型,尽管我认为并不总是这样。让我们首先明确定义单元测试,以便将其与其他测试范式区分开来。

单元测试完全在机器的内存中运行,在与正在评估的代码共享内存空间中,而不需要任何网络连接或对外部服务的依赖。大多数单元测试运行非常快,一次只测试很小的代码量,并且相对容易上手。单元测试通常还与您的代码合同紧密相连,并且通常需要使用模拟的新代码来使被测试的代码能够在正常可用的外部依赖性下执行。

单元测试的主要缺点是它们与被测试的代码紧密相连。单元测试很容易与实际的函数调用和内部数据对象紧密交织在一起。这种深层依赖意味着任何对代码的重构,即使是简单和良性的更改,也需要进行相当的单元测试更新。为了使单元测试运行,通常还需要创建模拟和测试数据固件的大量代码,这增加了单元测试框架的意外成本。

集成测试

集成测试放宽了单元测试的内存和零依赖性约束。因此,集成测试往往运行更慢,并与被测试的代码在更高的级别上进行集成。当它们在与待测试代码不同的进程中运行时,通常是通过运行通过外部接口(如API)执行代码。这意味着内部重构不会改变API行为的情况下,集成测试往往不会察觉到,这使得这些测试总体上更不容易受到破坏,但也更不太可能检测到较小的副作用。

此外,集成测试减少了或不需要创建模拟对象,因此可以减少需要实现的代码量。

端到端测试

端到端(e2e)测试以与最终用户相同的方式对代码进行测试。对于后端代码,端到端测试和集成测试可能以相同的方式工作,通过外部合同执行代码。对于面向用户的前端代码,端到端测试通常涉及模拟客户端界面,通常是一个Web浏览器或移动电话。我不建议任何技术领导人自己编写客户端机制代码,这是一个非常棘手的问题,可供下载的工具如Selenium、Cypress和Puppeteer可以代替您完成。对于移动端,也有诸如HeadSpin和Detox之类的工具。

端到端测试的主要权衡在于可靠性。至少在撰写本文时,可靠的Web端到端测试仍然有点困难;浏览器渲染的性质意味着意外的情况很容易发生。构建可靠的Web端到端测试套件需要特别的小心、注重细节和维护。

然而,回报是非常可观的,可以通过e2e测试套件创建非常高的测试覆盖率和对用户界面流程的功能性高度信心。有几家公司,包括testim.io和rainforestqa.com,正在探索使用人工智能和机器学习来解决这个问题。这些解决方案部署了模糊的视觉测试,而不是依赖于CSS选择器的存在或缺失,例如,以改善测试的可靠性。希望到您阅读这篇文章的时候,现有技术水平已经进一步提高了一些,端到端测试的价值主张将比本文撰写时更加强大。

可视化回归测试

可视化回归测试是一种相对较新的范式,旨在通过对渲染可视化进行增量操作来检测用户界面应用程序中的缺陷。有各种框架在不同的粒度级别上做到这一点,从捕获整个页面的屏幕截图到渲染单个组件,并生成增量以检测缺陷。明显的缺点是,任何对经过测试的组件的任何有意变更都需要对测试进行更改。

幸运的是,这些测试框架通常能够简单而轻松地复制正确的可视化效果,这也带来了另一个问题:使用易用的工具覆盖测试目标很容易产生错误负面结果,意外接受了一个事实上是错误的视觉差异。

手动测试

顾名思义,手动测试由人类而不是机器代码运行。对于人类测试代码,我们可以进一步将其细分为专业化和非专业化测试人员。

专业化手动测试

专业手动测试是一个内部手动测试团队。除了内部团队的传统好处,比如保持一致的激励和长期合作关系,内部团队的价值在于团队可以在你的产品上建立专业知识并深入了解你的用户。这使得团队能够发现训练不足或不熟悉的测试人员会忽略的缺陷。高质量的测试团队不仅可以作为捕捉软件缺陷的资源,还可以在产品层面上提供有价值的反馈,发现设计上的不一致,并就某个功能的工作原理提出引人思考的问题。

一个优秀的内部手动测试团队可以极大地提高整体软件和产品质量。

专业手动测试团队应该为产品功能创建详细的测试计划,并以易于检索和重复的方式存储这些计划,最好使用类似TestRail的工具,该工具允许创建完整的手动测试计划测试套件,团队可以根据需要手动重新运行这些测试。这样的工具还有一个好处,就是与其他开发人员和产品工具的集成,例如将TestRail运行链接到Jira里程碑,以展示为给定功能的发布运行了多少个手动测试和回归测试。这不仅作为一个发布检查项有价值,而且可以帮助回顾任何发布的缺陷,让你重新审查在发布任何给定功能之前运行了哪些手动测试,并添加额外的手动测试来捕捉到达的任何缺陷。

非专业化手动测试

非专业化手动测试通常被称为众包测试。有几个专有的平台可用于获取测试人员,例如Rainforest QA、Pay4Bugs、99Tests和Testlio。这些平台的定价模型通常基于提交的验证错误数量而有所不同。根据你的产品性质和你要优化的缺陷类型,众包测试可以是一种非常具有成本效益和低投入的提高产品质量的方式。

半自动化测试

半自动化测试是一种相对较新的软件测试类别。这些测试由非技术人员创建,也许是你的专业手动测试团队,然后在完全自动化或受控环境中运行。

这些测试的主要问题是可靠性。由于它们是由非技术人员创建的,它们的精度可能不如完全自动化的测试那么高,因此更容易产生错误的正面结果和负面结果。尽管如此,这个领域正在快速发展,每年都有新的公司和工具推出,例如Rainforest QA和Testim,这些公司和工具致力于提高可靠性并降低总体成本。

源代码控制

绝大多数公司所使用的行业标准源代码管理工具是Git。除非你的组织有非常有说服力的理由使用其他工具,否则大多数现有和未来的团队成员都已经掌握了Git的基本知识。如果不使用Git,你可能会给团队带来不必要的学习曲线,迫使他们学习你选择的替代工具。

目前有三个主要的云端Git托管平台:GitLab、GitHub和Bitbucket。这三个平台占据了市场的主导地位,所以在偏离这些标准之前需要仔细考虑。

Git有一个有趣形状的学习曲线。大多数人达到一个水平,他们可以以一种基本的理解来处理大部分正常路径的测试场景。然而,有时候出现问题,一个开发者可能会丢失一个提交或者在合并时弄乱了情况。当这种情况发生时,他们没有克服Git学习曲线的剩余部分,会导致沮丧和减速。作为团队负责人,我鼓励你付出努力去克服这个曲线的后半段。在命令行上专门使用Git来熟悉实际发生的情况。了解reflog、交互式变基、二分搜索和各种内置合并策略。掌握这些知识后,你就能避免整个类别的低效问题,并培训你的团队成为Git专家。

同行评审

一般而言,专家们建议对所有代码更改实施强大的同行评审流程(在撰写本文时,即2023年初,有越来越多的声音质疑这一建议,至少在其中增加了细微之处,我将在下一节中讨论)。多数同行评审是通过代码托管解决方案进行的,具体称之为拉取请求、代码审查或合并请求。以下是一些建议,以保持代码评审的高效性和效率:

  • 保持评审的规模适中!为代码评审设定最大限制,例如10个文件和200行。其他任何内容都应该拆分成多个堆叠/渐进式评审。堆叠评审是基于先前评审或依赖先前评审的代码评审。完成后,评审将按顺序合并,形成完整的更改。
  • 与团队共同确定代码评审的目标,并将其融入到你的文化中。代码评审并不是为了风格或琐碎的语义;这是你的自动代码格式化/代码检查器和静态分析工具的任务。代码评审的目的是确保清晰性,识别架构问题,标记缺陷和偏离模式,注意边界情况,并确保遵守业务规则。
  • 要求作者让评审者的工作变得容易。作者应包括对变更的描述,相关要求和工单的链接,以及使用类似loom.com的工具的代码和代码按预期工作的视频演示。
  • 鼓励在要求他人进行评审之前,代码评审的作者自行进行自审。作者合理地在注释中为读者提供指导意见,可以节省大量时间。
  • 分配专门的评审时间/窗口,以尽量减少干扰。

发布、展示、提问

常识认为,每个代码更改在交付给客户之前应由两个人进行评审。当然,任何事物都有权衡之处。手动代码评审不是免费的,也不能保证软件质量。考虑到手动代码评审需要付出一定的代价,所以值得思考何时付出这种代价才能带来最高的回报,并将代码评审作为最高ROI的工具。这个普遍的想法在2021年罗安·威尔斯纳赫(Rouan Wilsenach)的一篇题为“发货/展示/询问”(Ship/Show/Ask)的博客文章中得到了推广。

我们来考虑下代码评审的成本。一个代码评审需要两个人,我们称之为作者和评审人,他们需要进行多次上下文切换。一个常见的异步代码模式可能如下:

上下文切换 #1: 作者停止在项目1上编码,设置代码评审并标记评审人。作者开始在项目2上工作。

上下文切换 #2: 评审人收到通知,在项目3上停止工作,并开始对项目1进行评审。评审人给作者提供反馈后,在项目3上继续工作。

上下文切换 #3: 作者收到关于项目1的反馈通知,在项目2上停止工作并回应评审人的评论。然后作者继续在项目2上工作。

上下文切换 #4: 评审人停止在项目3上工作,并在最佳情况下对项目1的更改感到满意,批准代码评审。评审人在项目3上继续工作。最坏的情况是,作者和评审人必须多次重复上下文切换 #3和 #4。

上下文切换 #5: 作者收到批准的通知,在项目2上停止工作,合并项目1,然后继续在项目2上工作。

有办法最小化这些上下文切换,但这也涉及到权衡。一个常见的替代方案是将所有代码评审作为同步的成对编程活动进行;然而,这种策略将上下文切换转化为同步的会议时间,这仍然会影响生产力。无论你如何切割,人工代码评审都是昂贵的。

我的建议是将工作类型按风险水平和代码评审的预期收益进行分类。一个样本分类系统如下:

微不足道的更改,不需要批准

  • 复制/翻译更新
  • 微小的UI更改,最好提供更改的可视化证据
  • 仅用于测试的更改
  • 显式未使用或被功能开关禁用的新代码
  • 无法访问的代码(例如隐藏页面)

小的更改,最少程度的评审或事后评审

  • 代码更改伴随着测试,并涉及对现有模式和功能的扩充
  • 使用有限或没有实际使用的代码(例如,未部署的产品)
  • 变更重构,可以通过可靠的测试证明是正确的

重要的更改,认真的前期评审

  • 任何涉及新工具、框架、模式或架构的内容
  • 重大的新功能
  • 任何涉及敏感数据、个人身份信息或可能对安全形势产生影响的内容

尽管我相信这个系统提高了整个团队的效率,但我承认这对于每个人来说都不是一个选择。许多合规制度(如PCI或SOC 2)要求进行百分之百的人工代码评审。在这种情况下,你只能遵守,并可能开辟一些不受合规框架约束的产品或功能领域,以尝试更细致、高效的流程。

分支模型

处理源代码控制分支有很多方法,尽管整个行业正在围绕主干开发的概念建立势头。因为在目前看来,这似乎是最有效和最常用的模式,所以我们将在这里讨论它。如果你真的在考虑其他模式,你会发现在线上有大量的资源讨论替代方法的方法论和最佳实践。

有许多博客文章都有有用的图形,介绍git分支模型,比如Reviewpad的这篇文章:ctohb.com/branching。如果以下描述对你来说不太清楚,请查询任何这些文章及其相关的图形。

在传统的分支模型中,有两个长期存在的分支,一个是主分支,一个是开发分支,有基于开发分支进行的工作,然后通常会fork到另一个发布分支用于特定的发布,最后再合并回主分支。接下来针对主分支进行修补,同时在开发分支上进行进一步的开发。每次更改都需要至少四个分支才能到达生产环境,并且需要同时维护多个分支。出于这些原因及其他原因,GitFlow已经不再被广泛使用,也不再被视为最佳实践。

基于主干开发的方式,以及稍微复杂一些的GitHub Flow,是管理源代码的模型,旨在最小化分支的数量和持续时间。GitHub Flow和基于主干开发的具体实施可能会有所不同,但它们的共同之处是只有一个分支,其名称是不确定的并且不太重要。我们称之为生产分支。生产分支始终可以部署。事实上,我建议你设置自动化,以便每次对生产的提交实际上都可以部署到生产环境中。然后可以在基于于生产分支上的特性分支中进行工作,在特性分支上进行评审,并在准备就绪时合并。仅此而已,一个长期存在的分支和许多短期存在(最理想的情况下很小)的特性分支。

为了使这种模式能够良好运作,你需要一些先决条件:

  • 运行强大测试套件的持续集成,以确保特性分支可以安全地合并。
  • 一种文化和实施使用特性切换的方法,以便可以快速合并分支,然后在以后的某个时候根据业务需要部署/启用特性。
  • 对生产环境进行强大的监视,以检测更改。
  • 快速部署代码更改到生产环境,零停机时间。同样,可以根据事件对单个更改进行快速撤销。
  • 有一个在创建短小的、短期的特性分支方面有纪律的文化。如果特性分支变得庞大、持续时间长且不易控制,GitHub Flow模型将失去其效率和简洁性。正如在3.3.5特性分支环境中讨论的那样,小的提交、小的分支和小的拉取请求是推动生产力的关键因素。

长期存在的分支与短期存在的分支

保持团队的顺畅分支和合并系统的关键是保持分支的生命周期短暂。几乎所有与代码合并相关的问题都源于代码分支存在时间过长或分支中包含过大的差异(ctohb.com/diffs)。一般来说,短期存在的分支应该只有几天,或者绝对不能超过两周。

请记住,一个功能并不一定要在一个分支中实现。例如,你可以有一个仅包含测试的初始分支进行评审和合并,然后再进行实现的分支。或者,你可以构建一个与主应用程序没有连接的实现,并在后续的分支中进行评审和合并,然后在后续的分支中构建连接和测试。

通过一些思考和实践,大多数实现都可以分解成可以独立合并的部分。这是一个技能,通过你的指导,团队可以随着时间的推移发展起来。

保持分支生命周期短暂的好处:

  • 限制来自其他分支的新代码进入主干的时间,从而限制冲突代码的范围。较小的分支也本质上具有较少的冲突面积。
  • 使特性分支的代码保持相对较小,从而使审阅者更容易阅读,并限制破坏范围。
  • 在审查中鼓励更快的反馈,并允许在实现功能过程中更早进行修正。
  • 鼓励团队拥有可靠的持续集成系统。频繁的合并将突出你的构建/测试环境的缺陷,如果系统不可靠,将带来痛苦,并推动改进这些系统。

生产事故处理

递升器是一种工具,用于处理事故并管理轮值,向轮值工程师进行报警,如果未被接收,就会进行递升给其他人。PagerDuty可能是这些工具中最流行的。

实施递升器

在实施递升器和设置轮值之前,确保团队中的工程师已经选择加入轮值,并且每个人都知道和理解创建异常(例如,在假期期间与其他人交换轮值窗口)的预期。

您还需要确保已经建立了充分的文档,并且每个人都了解接收页面时的标准程序。建立这些程序时要考虑以下几点:

  • 公告接收页面的接收确认位置(可以是递升器工具本身或专门用于处理递升的共享群聊)。
  • 方便访问用于帮助诊断特定类型问题的操作手册。
  • 决定是否以及在哪里设置任何类型的网站维护通知(例如,公司状态页面需要更新)。
  • 决定何时何地以及多频繁地发布关于调查状态、影响估计和恢复估计的更新。
  • 确定一旦事件关闭后要做什么,安排根本原因分析练习并确保特定事件不再发生。

根本原因分析(RCA)练习

每当系统问题对用户产生可衡量的影响时,您的团队应该进行一定程度的根本原因分析(RCA,又称复盘)。RCA的目标是了解您的系统在哪些方面出现故障,导致重大缺陷进入生产并影响最终用户。

需要明确的是,根本原因分析绝不能用于确定责任或指责。这一点需要贯穿于根本原因分析的整个过程,并融入到团队文化中。RCA应针对系统性问题(而非人为错误),即可以导致故障发生的系统中的问题。

如果没有这种安全性和愿意让团队成员坦诚地提供反馈和文档的意愿,您将错失改进系统的重要机会。

RCA文档

您的团队应该为每个根本原因分析制作一定形式的文档。根据您的系统问题频繁程度和性质,您可能希望为RCAs创建一个分类系统,低影响事件使用较轻量级的RCA流程,而高影响事件使用较重型的RCA流程。我们应该承认,对高影响事件进行彻底的RCA是一项昂贵的工作,需要很多时间和思考,并且在微不足道的缺陷上可能过重。

也就是说,对于大多数公司来说,更好的做法是在这方面过度投入,确保更高的可靠性。您应该始终从彻底的RCA开始,并根据对您的团队将面临的问题类型的景观和影响有了良好的了解后,过渡到分层的RCA系统。

对于需要进行全面思考的问题,这里提供一个模板,可以帮助您开始并向团队提出正确的问题:ctohb.com/rca。这是一种良好的实践,实际上对于大多数合规框架都需要为每个事件创建类似的新文档,并将其组织在内部公司文档存储库中供后续参考。

RCA会议和时间表

在解决事件后尽快进行之后,指定一个适当的人作为RCA的负责人。负责人应克隆模板,并开始填写与事件相关的相关数据,并开始探索该事件的“五个为什么”(ctohb.com/5whys)。

他们应该完成RCA的初稿,并在安排团队的共同时间以一起探索和改进分析以及未来预防措施之前将其传达给相关同事。

会议参与者应提前阅读RCA草稿,并准备好探讨事件的细节,并对未来的预防措施进行构思。

选择RCA负责人

RCA负责人不一定是响应事件的人。理想的RCA负责人应该对所涉及的系统非常熟悉,并能提出有关工具和流程出现故障的有见地的问题,并提出改进的想法。

请注意,我们并不会指责任何犯了人为错误的人。如果符合先前标准,这个人可能是RCA的负责人,但是他们的错误本身并不意味着他们是领导RCA的合适人选。他们肯定会为此次过程做出贡献并有机会通过此过程学习。但是,请记住,在这个过程中并不会因为他们的错误而惩罚他们。编写RCA并不是惩罚,而是一项重要的职责和系统维护的一部分。

安排RCA矫正工作

一个良好的RCA过程通常会确定许多工作项,以改进系统并减少未来事件的可能性。自然而然的下一个问题是:我们现在就做这些工作吗?对于参与其中的工程师来说,答案很可能是肯定的;对于关心达到截止日期和进度规划的经理来说,答案将不太明确。

这个问题没有一个正确的答案,但以下是一些建议:

好事不要浪费。解决问题的动力在事件发生和RCA会议周围时最为旺盛,高度积极的工程师通常是最高效的。此外,我们很容易低估系统可靠性问题对团队的整体成本,从而低估可靠性改进的重要性。一个生产事件发生应该提醒您和团队,这些投资对于限制干扰、使团队专注于高效特性工作和提供一致的高速度至关重要。

许多补救措施的努力级别可能会有很大的差异。一些典型的任务可能包括增加记录或更改CI提供程序的设置,以确保无法合并具有失败构建的PR。这些类型的琐碎任务在以后的维护和整理工作中的投入成本超过了处理它们所需要的时间,所以直接去做它们。它们被选为解决问题的正确方法的可能性非常低,而且如果出现负面后果,它们可以很容易地被撤销。

对于付出很大努力的补救措施,我鼓励您对其进行分类,并通过常规的规划过程进行处理。通常,随着时间和规划的好处,高努力的补救措施可以简化。换句话说,第一天解决问题的正确方法可能不是最理想的解决方案,只有通过进行常规的技术审查,才能找到更好、更少成本的解决方案。

IT

这里我提到的IT是指公司内部用于日常业务的工具和技术。这与您的公司为客户产品构建的技术相对应。

IT通常包括公司硬件(台式机、笔记本电脑和手机)、VPN、电子邮件、防病毒和监控软件等工具。作为现代世界中的初创公司,无论您是团队办公还是远程团队,如果您做出了明智的决策,您不需要花费太多时间或资金来处理IT事务。

在大多数小型技术公司中,以下是帮助您最大限度减少IT成本的一些建议:

使用基于云的系统进行公司邮件、数据和文件存储。大多数初创公司都在使用Google Workspace,但如果您的团队成员(以及未来潜在的招聘者)对其他选择更熟悉,请选择其他选择。此阶段设置自己的内部邮件服务器、文档存储、数据访问、网络等没有任何好处。

在初期阶段,除非合规系统要求,不要要求员工使用公司硬件。在小规模采购(尤其是在产品市场适应阶段之前),配置和管理公司硬件是一项繁重而且成本不菲的工作,而且只提供少量或罕见的实际受益。

不太愿意承认,但是妥善保护产品和IT系统是一项庞大的任务,对年轻的初创公司来说,在早期全面做好这一点是不现实的。我建议您务实一点,并侧重于从员工的人为错误中最有可能导致违规或数据盗窃的最可能源头保护系统。更有可能的是,您的工程团队忘记在API前放置身份验证,或者有人将笔记本电脑在咖啡店中解锁,而不是攻击者通过使用漏洞中间人拦截或入侵您的云基础架构来窃取数据。

即使遵循最佳实践来最大程度减少IT工作量,您仍然无法避免某些不可避免的IT任务,主要涉及激活和停用员工帐户以及员工密码恢复。我建议您为并培训其他同事(例如人力资源部门)记录这些任务的方法,以便它们不会经常干扰您或工程团队。

安全性和合规性

在本节中,我将简要概述初创公司的安全性和合规性问题。您可以并且应该在此书之外花时间寻找详细的资源以了解这些话题。

身份验证安全术语

特别是在安全方面,用语要准确和精确。以下是一些常常被误用的术语的定义:

认证(Authentication):验证用户或客户是否是他们所说的那个人。您的登录系统执行用户认证。

授权(Authorization):验证用户或客户是否有权限执行他们试图执行的操作。基于角色的访问控制(RBAC)或权限系统执行授权。

2FA或MFA:双因素身份验证和多因素身份验证是使用多种类型凭证对服务进行身份验证的过程。通常使用密码(第一因素)和某种所有权证明,例如一次性密码(证明您拥有邮箱)、短信(证明您拥有电话号码)或一次性密码(TOPT)(证明您拥有设备/通行证)。由于SIM劫持攻击的普及,其中攻击者可以拦截或重定向短信,使用短信作为第二因素通常不被鼓励。

初创公司中的安全性

初创公司通常因资源受限而受到定义。因此,安全姿态和合规性往往是待办清单上首先被忽视的事项,因为与其他紧迫问题相比,它们不太可能对业务构成生存威胁。如果您没有用户或收入,黑客能偷到什么呢?

考虑到安全性可能会影响生产力或成本的任务,特别是如果您的任务是保护现有系统。但如果您从一开始就开始,您有机会在最初时做出明智的决策,以最小的额外成本建立强大的安全姿态。

在您的初创公司中引入安全措施而不增加太多成本的几种方法:

  • 在您的入职和培训材料中,将安全性作为团队思维的优先事项。
  • 将所有工程师纳入入职和定期的基本安全培训,例如OWASP十大安全风险或各种通过游戏化学习的安全培训,每月只需几分钟即可使安全保持在关注重点。
  • 依靠经过验证和维护良好的工具来进行身份验证或授权相关的任何操作。
  • 不要浪费时间自己构建登录页面;在2023年,没有真正的理由这样做。像Auth0、SuperTokens和AWS Cognito这样的工具提供安全的用户注册、登录、社交登录、忘记密码管理、电子邮件身份验证、双因素身份验证和会话管理。其中一些工具还提供了强大的授权系统。处理身份验证是一个庞大的项目,非常复杂且错误代价高昂。没有理由让您的初创公司去解决这个问题。
  • 在IT安全方面不要懒惰。无论您使用的是Dropbox、Box、Google Drive、SharePoint等,花几分钟设置策略,以减少人为错误,例如将默认共享权限设置为仅限内部。设置定期数据共享报告,并指定一个员工定期对特别敏感的文档或电子表格的权限设置进行审核。
  • 使用企业级密码管理解决方案,例如1Password,并确保所有员工为重要工具使用强密码。同样,尽可能经常使用单一登录(SSO),并确保您的SSO提供商配置了高级安全性(至少要求多重身份验证)。
  • 不要在您的代码库中提交机密。利用安全的密钥管理器,例如Google Cloud Secret Manager或AWS Secret Manager,并在代码中提交密钥的名称/位置,并在生产中将该名称解析为值,可以在引导时使用类似Berglas或Whisper的工具或直接在运行时与密钥管理器API进行解析。

合规性

无论是由于所处行业、公司规模还是客户性质,大多数初创公司都需要符合至少一个正式的合规性框架。如果您的用户在欧洲,那么您需要遵守《通用数据保护条例》(GDPR)。如果您处理用户数据,了解《加利福尼亚隐私权法》(CCPA)是明智的。如果与企业客户合作,他们可能会要求您的SOC 2或ISO 27001认证。在医疗保健领域,存在《医疗保险便携和责任法案》(HIPAA),而在支付领域,您可能听说过PCI DSS。

对于初创公司来说,始终符合任何或所有这些框架可能是难以接受的开销。以下是保持合规性并预测成本的一些建议:

  • 不要在最后一分钟尝试获得合规性证书。从头到尾准备和进行用于PCI DSS或SOC 2之类的审计是一个漫长的过程,对大多数初创公司而言,范围从六个到十二个月不等。早点开始并保持合规性比迟开始并进行重复性工作要便宜。
  • 尽可能多地使用自动化来执行或提供合规性证据。有许多专门提供自动化这些合规性框架的SaaS公司;例如,像Vanta、Tugboat Logic、Secureframe、Laika和Drata这样的公司提供的工具将大大减少您获得证书的时间和总成本。
  • 如果您幸运地有一个正式的合规性人员或部门,请与他们保持密切联系。您与合规性部门分享计划的主动程度越高,并在早期就纳入他们的反馈意见,遵守合规性的成本就越低,也就越不会令人沮丧。

本文是 THE STARTUP CTO’S HANDBOOK 中译版,由极客智坊翻译服务自动翻译完成,另有中文PDF版本提供下载:创业公司CTO手册。:

微信扫描体验极客翻译
发表回复