无毒原料药
无毒原料药
原文:https://medium.com/hackernoon/non-maleficent-apis-869fe439ca2a

第一,不伤害
作为希波克拉底誓言的一部分,医生承诺“不伤害”病人。希波克拉底在其他地方阐述了医生应该“做好事”和“不做坏事”。作为开发人员,我们打破东西。很多东西。事实上,有一个完整的行业,它的存在仅仅是为了跟踪我们打破的东西。但是,我们有没有可能承诺持续交付 web 服务(“做好事”),而不发布不停的更改(“做坏事”)?
要避免伤害的最明显的消费者是消费您的生产环境的付费用户。最终新的或更好的特性的意图从来都不是破坏客户已经购买的现有功能的正当理由。在设计良好的环境中运行的设计良好的服务允许开发人员添加新的甚至完全不同的功能,同时提供无缝的向后兼容性。
但是受益于这种“无害”心态的不仅仅是生产环境。对面向内部的服务使用相同的设计原则允许 API 开发人员快速迭代,而不会阻碍其他团队的开发人员。
这里是面向服务的架构中服务的 5 个设计原则,旨在促进快速开发,而不破坏现有的消费者。
1.多个独立的主要版本
多重
托管和使用一个服务的五个版本应该和只托管和使用一个版本一样容易。持续集成和交付的详细描述超出了本文的范围;然而,一旦您有了部署第一个服务版本的自动化系统,设置接下来的五个或六个版本应该是微不足道的。相反,如果您依赖人工交互来构建、审查和部署您的服务,维护多个并发版本将是一场噩梦。
因此,在设计 CI 和 CD 管道时,要考虑您的构建和部署过程将如何处理一个服务的多个版本。
自主的
变更应该能够在保证对所有其他版本没有影响的情况下进行创作和部署。做出这种保证的最简单的方法是将应用程序的每个主要版本作为单独的可执行文件。这样,部署新的主要版本(或次要版本的更新)不可能影响任何其他部署的应用程序的可执行位。
另一种(较差的)方法是在单个可执行文件中托管服务的所有版本。在这种情况下,添加或更新一个主要版本需要部署所有其他版本,因为它们都被编码在一个已部署的工件中。虽然可以注意防止在其他版本上的退化,但这并不是固有的。
随着时间的推移,您可以部署使用较新版本的遗留版本的修订版,同时保持相同的 API 签名以保持向后兼容性。通过这样做,遗留版本最终会变成新版本所包含的实际业务和持久层的薄薄的外表。然而与此同时,在部署(以及在需要时,回滚)对每个主要版本的更改时,保持了独立性。
主要版本
按照语义版本化,主要版本表示对现有功能的重大更改。web 服务的示例包括删除路由、添加新的必需参数、更改参数的必需格式或任何类似的更改。
基于浏览器的消费者(如 spa)对突破性变化更有弹性;每次有新版本时,用户都会重新安装应用程序。这使得部署突破性的后端更改与相应的前端更改保持同步变得相对容易。这里的主要警告是多个应用程序的部署必须同时发生。这降低了多个团队对他们控制的代码执行发布的自由度,并且经常导致消费者的短期服务中断。
但随着移动或桌面应用程序等原生安装应用程序的出现,情况变得复杂起来。在这种情况下,web 服务开发人员通常对最终用户安装的版本没有任何控制权。因此,发布重大变更会导致付费用户失去现有功能。有一些方法可以解决这个问题,比如要求旧的客户端升级到新的版本。然而,用户通常认为这种变化是应用程序中的一个严重错误,他们必须更新才能修复。
关于版本的话题,有一些关于如何将版本指定为消费者的观点。在 HTTP 请求中,数据可以在 URI、头部或主体中传递。但是并不是所有的请求都支持主体(比如 GET ),因此我们只剩下两个选择:通过 URI 版本控制或者通过消息头版本控制。正确的方法是什么?只要您能够独立部署这些版本,这并不重要。
2.可访问、机器可读的规范
易接近的
API 的规范应该对 API 的所有消费者免费开放。换句话说,如果您可以访问 API,那么您应该可以访问规范。依赖于不可用于安全目的的规范(通过模糊实现的安全性)容易受到逆向工程的攻击,最终只会给合法的开发者制造障碍。
除了可访问之外,规范还应该是可发现的。如果没有人知道 API 在哪里,那么在定义 API 上投入的任何努力都是徒劳的。在之前一篇关于文档的文章中,我详细描述了浪费时间编写不可被发现的文档的问题。重点是,制定一个规范,使其与 API 一起可用,然后在可以找到它的地方进行过度交流。
机器可读的
在开始本节之前,我想定义几个术语。我使用术语“规范”或(spec)来指代 API 定义的严格的、机器可读的描述,而我使用术语“文档”或(docs)来指代如何使用 API 的人类可读的描述。两者都是需要的,但事实上,它们是实现两个不同目的的两个不同的东西。
如果你从机器可读的内容 (m) 开始,那么存在一个确定性函数,它产生人类可读的内容 (h) :
h = f(米)
一个成熟的文档生态系统的标志之一是有过多的工具,包括用于从机器可读文档中呈现文档的实用程序(例如 OpenAPI、API Blueprint、RAML 等)。
然而,相反的转换是一个令人惊讶的学术难题。不存在从主要人类可读内容(h)中产生机器可读内容 (m) 的确定性函数
m!= f(h)
自然语言处理已经走过了漫长的道路,机器翻译是处理人类语言的非常有用的工具;然而,这项技术还没有发展到能够从书面文本中创建一个严格而详尽的规范的程度。不管您从其他来源生成的规范有多好,它总是缺少一些细节。
所有这些都被理解了,如果必须在机器可读性和人类可读性之间进行权衡,那么总是选择机器可读性最终会两者兼顾。
因此,如果人类阅读文档来理解一个 API,以便针对它进行编码,那么为什么要大谈机器可读性呢?简而言之,答案是自动化。代码生成、测试实用程序、重大变更检测、服务模拟和验证框架只是机器可读性带来的众多好处中的一部分。
说到突破性变化检测…
3.自动中断变化检测
有什么比在每次提交时手动检查每个 API 路径来确保重大更改没有进入代码库更容易的呢?可悲的是,在已经引入了破坏之后处理它们通常比严格阻止这样的变化被集成要容易得多。没有任何脚本检查,灭火通常是最实用的解决方案。但是有了一个机器可读的规范,你就可以两全其美了。
一个足够成熟的持续集成管道应该能够从一开始就防止破坏性的变更进入您的代码库。通过用机器可读的规范完全定义 API 的功能,并将该规范与源代码控制中的代码放在一起,很容易看出 API 是如何随着时间的推移而发展的。
有许多工具可以区分两个版本的规范,并确定变更是破坏性的还是非破坏性的。如果您为您的 API 使用语义版本控制,那么这个决定允许分别更新主要版本或次要版本。(在发布不包括规格变更的变更的情况下,这将修订补丁号。)
在集成期间,让您的 CI 系统运行一个脚本,该脚本将源代码控制中规范的新版本与 web 服务提供的规范版本进行区分。(还记得#2 中关于使规范可访问的内容吗?)寻找突破性的变更这种方式排除了手动检查的需要,除非构建在这一步中断。
通过自动检测重大变更,只要必须部署重大变更,唯一需要的手动干预应该是设置应用程序的新版本。
4.完全分离的关注点
到目前为止,我们已经讨论了预测和减轻重大变更的影响,但是现在让我们讨论一下如何首先预防它们。
web API 应该是它所编码的逻辑和使用它的系统之间的一个严密的接缝。在这种情况下,可以对一个系统的实现细节进行更改,而不会破坏另一个系统。但是当一个 API 泄露了它自己的实现细节或者对它的消费者做了假设,突破性的改变变得很难避免。
总是使用无处不在的语言
Eric Evens 在他的书《领域驱动设计》中使用术语“无处不在的语言”来谈论描述领域模型的严格词汇。这个词汇表超越了软件,应该被开发人员、利益相关者、用户和任何与项目交互的人使用。用一种对无处不在的语言极其天真的简化来说,这是一种让各方通过用共同的术语说话来更有效地工作的方式。
关于 API 设计,语言应该由领域模型本身驱动,而不是领域模型的实现。这意味着,虽然定义从数据库或第三方服务映射模型的端点可能很诱人,但如果每个人都用不同的术语(例如,无处不在的语言)谈论数据,那么 API 应该使用这些术语。
领域专家应该反对那些难以表达或不足以表达领域理解的术语或结构;开发人员应该注意会使设计出错的模糊性或不一致性。—埃里克·埃文斯
按照这些思路,我认为用持久层来描述领域模型将不可避免地以不确定性告终。随着对数据持久化方式的改变和优化,在 API 中反映这些修改最终将需要突破性的改变,即使域模型本身没有改变。
永不泄漏存储实现
如前所述,用于描述数据的词汇和数据本身的形状应该基于领域模型,而不是领域模型的后端实现。如果一个数据库表和 API 端点共享相同的名称,这没问题,只要这是因为两者都符合无处不在的语言。
然而,在一些合理的情况下,出于性能原因,一个域对象将分布在多个表中。同样,多个对象可以共享同一个表。即使这些持久性实现不同于公共词汇表,API 仍然应该这样表示它们。
这确保了 API 的实现和它的消费者的实现之间有一条清晰的接缝;任何一方都可以在不要求另一方做出相应改变的情况下改变实施。
永远不要假设客户端实现
泄露实现细节的另一种方式是假设客户将如何使用 API 并相应地做出设计决策。这将消费者的详细信息泄露到 web 服务中。
例如,将多个不相关的领域概念聚合到一个调用中可能很诱人,因为这是网站需要的。但这些需求主要是由 UX 对单一平台的担忧所驱动的。通过迎合单一平台的需求,通常会发生两件事。
首先,其他系统的性能通常会受到影响。这要么是因为通过线路发送了太多不需要的数据,要么是因为没有发送足够的数据,因此需要发出多个请求。其次,当首选平台由于 UX 的原因需要 API 更改时,这些更改对其他系统的影响将是不可预测的,并且经常会导致错误。
在所有网络上,尤其是在移动网络上,发出大量请求的开销会导致严重的性能问题。因此,需要聚合请求来提高性能,但是基于单个平台的需求进行聚合是一个漏洞。
Sam Newman 在他的书《构建微服务》中,提供了前端模式的后端作为这个问题的解决方案。在这种模式中,为前端平台创建了一个瘦“BFF”API,其唯一目的是复制其他下游 API,以便根据单个前端的需求定制请求。在这种情况下,领域(及其相关词汇)是前端平台的 UX 关注点,而不是下游服务实现的更一般的领域模型。因此,围绕前端实现设计 BFF 路由是合理的。
5.电路、超时和记录…哦,我的天!
最后,我想简单谈一下保持 web 服务在部署之间工作的一些稳定性模式。
断路器
美国绝大多数的建筑法规都与消防安全有关,其中大部分都与电气系统有关。这是因为电是危险的。建筑物中的断路器作为故障点存在,旨在快速故障并首先故障。这确保了不是整个建筑被烧毁,而是只有一个开关被扳动。
迈克尔·尼加德在他的书《释放它!描述“断路器”模式。在分布式系统中,这种模式提供了与物理断路器相同的功能。这是代码的一个特性,如果它确定某个“电路”当前有故障,它将阻止对该电路(通常是另一个服务)的访问。它允许系统检测并在底层服务出现故障时适度降级。
在“无害”的上下文中,这允许 web 服务尊重上游和下游系统。在下游服务的情况下,在系统停机时踢它可能会加剧导致它行为不当的任何问题。在上游系统的情况下,返回带有 retry-after 头的有意义的 503 响应,而不是抛出堆栈跟踪,这也允许上游服务适度降级。
总的来说,断路器提供了一种防止故障在分布式系统中传播的方法。突破性的改变是不好的,但是在你的整个生产环境中级联它们导致的失败是非常糟糕的。
超时设定
Nygard 还建议将每次通话都暂停一次。这是一个简单的方法来确保我们不会让那些注定要失败的请求阻塞当前工作的系统。这种快速失效的心态与断路器模式非常契合。例如,可以用来使电路跳闸的一种试探法是重复超时。在这种情况下,对太多请求等待太长时间表明我们应该停止打电话一两分钟。除了不要盲目地接受你的框架的默认超时持续时间或行为之外,没有什么可说的了。
记录
当你弄坏了用户花钱买的东西时,他们通常很擅长抱怨。如果你关闭了一个关键系统,他们会让你知道。但是能够在错误报告开始泛滥之前回复变更是保持客户信任的一个极好的方法。
您的日志记录系统应该能够告诉您在部署之后是否出现了错误高峰。如果错误超过某个阈值,一个足够成熟的 CD 管道将能够自动回滚部署。但是对于我们这些农民来说,如果需要的话,我们可以手动查看日志并触发回滚。这意味着(像您的文档一样)您的日志系统应该是可访问和可发现的。您还应该努力保持较低的误报率(比如零),这样您就不必怀疑所有这些错误是否都是“真实的”
您的日志记录系统也是一种很好的方式,可以确定哪些 API 的哪些版本实际上在野外使用。尤其是对于遗留 API,一些端点可能不再被调用。这一知识在旧版本的淘汰过程中很有用,以便随着时间的推移慢慢地删除未使用的功能。能够从真实数据中提取报告是一种更可靠的方法,而不是试图追踪每个针对您的服务编写代码的人。
你有多希波克拉底?
这里有一个简短的测试来确定您的 web 服务是否“无害”
- 您目前是否支持 web 服务的多个版本?
- 能否独立部署和回滚服务版本?
- 你总是支持旧版本的移动客户端吗?
- 您的 web 服务的每个版本都公开了特定于版本的可读文档吗?
- 您的 web 服务的每个版本都以机器可读的格式公开了特定于版本的规范文件吗?
- 集成突破性变更时,您的构建会中断吗?
- 你的 API 公开数据 仅仅是 吗?
- 能否独立部署后端和前端系统?
- 您是否在前端和后端系统中使用断路器和超时来防止级联故障?
- 您能通过使用您的日志记录系统找到未使用的 API 功能吗?
如果你对所有问题的回答都是肯定的,请把你的简历发给我!如果你对大多数问题的回答是肯定的,那么你在一两件事情上做得很好。如果你对大多数问题的回答是“不”,那么你很可能要花很多时间来处理破碎的消费者。任何花在变得更加“希波克拉底”的时间都可能是非常值得的投资。