ChatGPT 4O生成的图片
这是我在博客系列中分享在Akrose Labs构建平台架构经历的延续部分。前几篇可以在这里找到:
在解决了资源及其分配的问题之后,我们可以把注意力转向平台的另一个关键方面——合同。在这篇文章里,我想介绍我们之前确立的接下来的5个原则:
- 服务之间应仅通过契约进行通信,而不暴露内部细节。
- 服务之间必须有明确的契约来描述它们的交互。
- 我们力求提供内外部契约具有一致的质量标准。
- 每个服务都应该对系统做出有意义的贡献,并增加其价值。
- 不应强制契约链通过那些没有增加价值的服务。
合同往往是许多平台的短板,尤其是那些在初创企业内部自然发展的平台。如果处理不当,它们可能会长时间限制你平台的发展,有时甚至会长期限制,特别是涉及第三方的情况下。这就是为什么把它们做好真的非常重要。
我不管它是怎么做到的(只要能做到就行)服务之间应该仅通过合约通信,并利用信息隐藏原则。
在部分 2中,我们说明了为什么每个资源都应该由一个单独服务来管理的原因。这有助于避免多个服务同时编辑同一个资源,从而避免陷入越来越难以维护的困境,而不是陷入数据库级别的契约关系中。
然而,就像一个服务不应侵犯另一个服务的责任范围一样,资源的所有者也不应过度暴露其内部运作细节。某个服务具体是如何处理、存储和管理其资源的细节,对资源使用者来说并不重要。使用者不需要关心这些细节。它只关心“完成任务”(也就是对某个资源进行特定操作)。如果不遵守这一点,这可能导致消费者和提供者服务纠缠在一起,最终形成一个分布式的单体系统,这时候你可能会开始遇到修改和维护这种耦合的难题。
一个简单的例子来说明。在我的经历中,我遇到过一些依赖特定语言的二进制序列化模型而非标准化模型(如 JSON 或 Protobuf)的合同约定。实际上是在构建服务合同约定的语言中直接嵌入了运行时对象的序列化。根据我的经验,这些合同约定通常是出于“节省时间”(尤其是在初创环境中匆忙完成时),或者在实现更标准的表示法时遇到问题所建立的。然而,虽然在做出这一决定时节省了时间和精力,但这通常意味着将来需要付出更多的麻烦和努力。特别是当你决定重写原始服务(或添加一个新的服务)为另一种编程语言时。在一个动态发展的软件公司中,这种情况绝不是少见的。在我的职业生涯中,我见过服务从一种语言(或框架)重写为另一种语言(或框架)的情况非常多。有时甚至可以看到同一个服务被多次重写。所以,你选择的语言和框架不应该影响合同约定。
出于这个原因,我们已经建立了一个要求,即在我们声明的合同中使用信息隐蔽。这一概念通常更多地用于应用内部模块,但同样可以适用于服务间交互。因为你实际上在执行相同的操作,只是在不同的抽象级别上。因此,虽然提供者服务拥有并维护合同,但其复杂性和抽象度应由使用合同的服务决定。合同不应依赖于所选语言或框架,也不应依赖于处理和存储数据的基础技术。这可能意味着你可能需要在输入和输出格式上更加抽象,进行额外的映射或类似处理。但在一个需要对变化保持一致快速响应的环境中,偶尔稍微减缓一下速度是有益的。
给我看看合同服务必须有明确的合同来描述它们的互动。
我们要求任何新的或现有的服务合同都应明确声明,并使用相应的标记在专门的存储库中进行存档。你选择如何存储合同更多是个人偏好和组织适应性的选择。我们选择将它们存储在一个集中的位置,以便所有开发团队都能轻松访问,并且任何人都可以轻松获取并利用这些资源。这也帮助架构团队更集中地监督合同的质量。然而,你可能觉得这不适合你的业务情况,更倾向于将合同与你的服务一起存放。但主要观点仍然不变——你希望合同被明确地声明在一个已知的地方。
这并不总是容易的——特别是在一个动态的初创环境中——当你试图在有限的时间内尽可能多地推出功能时。作为技术领导者,你需要你和你的团队具有很强的纪律性。然而——根据我的经验——这总是值得的从长远来看。绘制现有的契约实际上需要更多的时间和精力——尤其是在动态类型的语言中——因为随着你服务的契约表面扩大,它们之间的交互变得更加复杂,许多边缘情况就会出现。而这些边缘情况往往是在推出变更时引起最多麻烦的地方,因为这些区域往往测试覆盖率不足,对大多数开发人员来说也更加难以理解。所以当出现问题时,往往不清楚具体哪里出了问题。
确保您的合同相关且最新最好的方法是实际使用合同,而不仅仅是声明它们的存在。选择您的技术并使用相应的表示法。例如,对于RESTful API合同,您可以使用Swagger/OpenAPI表示法,对于gRPC使用Protocol Buffers等。有许多工具可以帮助您生成所有冗余代码和样板文件。有时可能需要一些调整才能达到您想要的效果。随着新版本的发布,事物会逐渐变化。例如,我记得为了某些OpenAPI生成器,我不得不做很多自定义转换,仅仅是因为特定的语言和框架组合落后于版本——因此它将无法生成您需要类型的源代码。不过这还是值得投资的。如果合同只是挂在Wiki页面上,它很快就会变得过时,而实际的API接口将以一种并不总是容易跟上的速度发展。
所有合同都平等我们力求确保内部合同的质量标准与外部合同一样高。
在我的职业生涯中,我见过一些案例,投入合同的努力程度并不一致。这种情况在比较外部合同和内部合同时更为突出。外部合同通常定义明确且维护良好,而内部合同则未必如此。造成这种情况的主要原因是初创公司中的系统自然增长。在大多数情况下,这通常是由于为了快速推出产品而采取捷径所导致的技术债务。这种情况最常发生在小型开发团队中,因为同一组人需要维护多个服务。
虽然其中一些情况可能合理,但大多数其实不然。在我看来,最初稍微多花一点力气,仔细考虑功能请求,并以与现有方法一致的方式定义新合同,通常比“临时凑合”更好。尤其是当你可能需要处理庞大且复杂的合同,就像我们一样,这些合同有时会包含成百上千个数据点。而且,你的系统架构越复杂,你从这种努力中得到的回报就越大。因此,也请好好对待内部合同。
只是为了微服务而存在的微服务吗?任何服务都必须对系统有所助益并实现增值。
这不仅仅与合同本身有关,而是对系统中的合同效率有重大影响。
如我在之前的博客文章中提到的,您应该避免过度拆分服务。否则,您可能会陷入一堆实际上在系统中没有执行任何有意义任务的服务,这些服务不仅浪费了计算资源,还通过增加不必要的网络跃点延长了延迟。在这种情况下,仔细考虑是否真的需要新服务是有益的。是否为系统带来了额外的价值和好处。下面举几个例子来说明:
- 我遇到过的一个比较常见的场景是一个网关/枢纽/中继服务可能位于系统不同部分之间。当不同的团队各自负责系统中紧密相连的部分时,这种情况最为常见。此时,该服务起到了各团队职责边界的作用。在某些情况下,这样做是合理的,例如当该“边界”服务作为底层服务生态系统的一种抽象,或者它起到聚合作用(类似于 GraphQL 接口),或者当这种转换逻辑无法在底层服务中实现时。但在某些情况下,这部分逻辑可能会被你的应用程序或基础架构工具所吸收,这就使得该服务存在的必要性值得怀疑。
- 另一个场景是,当某个功能被分割成一个服务时,这个服务显得过于单薄。如果这个操作可以作为另一个服务的库或附加工能,那么这个服务是否真的有必要存在并消耗资源?显然,具体情况会有所不同,每个案例都有其独特性,但总的来说,仔细审视你的服务并问自己:‘这个服务真的有必要存在吗?’是很有帮助的。
上述示例也可能经常说明,不同开发团队之间的职责划分可能需要重新调整。因此,审查您系统中各个开发团队的职责范围,并引入一些调整以使团队的职责范围更加清晰。
不要硬搬合同链不应该被迫经过那些没有增值的服务。
这是对之前原则的一种延伸,这个原则更多地关注于系统内的数据和调用流程。实际上,它是之前原则与内部和外部契约的原则的结合。换句话说,我们将关注特定的开发团队内部或外部的契约。
团队负责各自系统的特定“切片”,这可能导致一种情况,即你会有一个由同一团队拥有的服务生态系统,这些服务与更大平台的接触点较少。由于那些不直接与外部系统通信的服务现在变为团队内部的服务,同样的问题也会在内部合约中浮现,即这些服务合约的维护努力会减少。这里的理由相同——节省努力。但结果也一样——合约质量下降。这可能导致“管道化”的交互方式,即来自团队外部服务的请求会被强行通过“外部”服务(即拥有更高质量接口的服务),仅仅因为“内部”服务的合约太乱。
这种情况往往会导致相应服务的效能和可维护性下降。请求被迫进行多余的跳转(这些跳转并没有带来任何好处),同时你也创建了服务之间奇怪的隐含联系。当需要改变这些服务时,这些联系将很难被打破。
因此,在任何层级和边界上维护同等质量的标准同样重要。我们采取的立场是:无论你当前如何接入服务——你都需要有高质量合同,因为如果一个内部服务的合同需要被你的同级服务使用,它必须能够以高标准支持这些服务。
结尾下面是我的结论。
在这篇博客文章中,我们已经探讨了为合同制定的原则。系列的最后一部分将涉及构建平台架构时关注的服务质量、稳定性和可观测性。
共同学习,写下你的评论
评论加载中...
作者其他优质文章