程序框架的最佳实践【翻译】

原文作者Chris Nokleberg and Brad Hawkes来自Google,Java框架开发者 原标题:程序框架最佳实践——虽然很强大,但并不适合所有人 共享代码库鼓励代码复用,实现不同团队技术的一致性并改进产品效率和质量。开发者需要选择合适的库,研究如何正确配置,最终把所有的库组装在一起。程序框架可以通过对库的默认安装和配置,来简化开发流程,提供更好的一致性,当然也损失了一些库的便利性。 框架不仅是一堆库的集合,还控制了整个程序的生命周期。确定的框架行为可以为开发开拓空间——例如,不用为每个程序应用都深入审查安全和隐私相关的代码。框架提供了跨团队和跨语言的功能一致性,同时也是更高级的自动化和智能系统的基础。 这篇文章先从框架的核心方面综述开始,然后深入到框架的优势、妥协以及我们推荐实现的最为重要的框架功能。最后,这篇文章展示了一个Google的实际框架应用案例:如何开发一个微服务平台,使得Google可以打破单一代码库的限制,以及框架如何使这一切成为可能。 什么是框架 框架和共享代码库在很多方面都类似。在Google,有两个技术原理用于区分框架和库:依赖反转和可扩展性。虽然看起来比较容易,但本文所讨论的框架很多优势,原则上都是从这两个原理衍生出来的。 IOC(依赖反转) 当从头开始开发一个程序的时候,工程师来决定程序的流程——这通常称为普通控制流。在一个基于框架的程序中,则是由框架来控制流程以及调用用户的代码——这叫做反转控制流。反转控制流有时也被引用为好莱坞原则:“别给我们打电话,我们会给你打电话”。框架控制流很好地定义了不同程序之间的标准。理想情况下,程序只需要实现它们特有的逻辑,而由框架来解决所有其他构建一个微服务所需要的各种细节。 可扩展性 扩展性是区分库和框架的第二个重要特点,它和依赖反转互相配合。因为框架的控制流是由框架负责的,唯一的改变框架运行方式的方法只能通过它暴露出来的扩展点。例如,一个服务端框架可能有一个扩展点是允许一个应用程序在每个请求来的时候运行一些代码。扩展点的模式也意味着框架的其他不可扩展性部分很固定,应用程序无法改变。 框架的好处 框架除了提供共享代码库所能提供的功能之外,还有很多好处,它们对不同的角色在不同方面都有益处。 对开发者 开发者这个角色最终决定是否使用一个可用的框架,也是最显然从框架中获益的角色。大部分的开发者的好处在于提升生产力,简便高效以及可应用最佳实践。开发者可以写很少的代码,利用内置的框架功能。由于框架处理了各种样板代码,因此他们所写的代码可以大幅简化。框架提供了合理的默认配置,消除了无意义而且浪费时间的决策判断,从而提供了充足的最佳实践。 对产品团队 除了为开发者提供生产力以外,框架也为产品团队解放了团队浪费在重复建设基础设施的资源。产品团队因此可以更关注于使他们产品与众不同的功能开发了。 框架可以让产品团队的开发与底层基础设施的变更隔离开,这也能让他们从中受益。虽然并不是普遍适用,但框架提供了额外的抽象,这意味着有些基础设施的迁移可以被当做实现细节,完全由框架维护者全权处理。 Google产品的发布需要很多团队的确认。例如,一个发布协调工程师负责审查产品安全性和有效性,同时有一个信息安全工程师检查程序应用的场景安全漏洞防范设计。当负责审查的团队熟悉框架并可以信任框架的功能保证时,框架就可以帮助简化审查流程。产品发布以后,标准化流程可以让系统更加便于管理。 对公司 从公司层面来说,常用的框架可以减少开发者上手启动一个新应用的时间,从而提升开发者的灵活性。如果一个公司有一个足够大的程序员社群,投入资源在高质量的文档和培训程序员将变得非常有价值。这也有助于吸引社群贡献文档和代码。一个被广泛应用的框架意味着,对框架很小的改进投入可以换来巨大的收益影响。 在过去一段时间,收敛框架架构可以使针对全局性的变更有广泛的应用范围。例如,如果你依赖一个统一的微服务/RPC框架,而且带宽比CPU更贵,那么框架可以基于成本权衡来优化默认的压缩参数。 框架的权衡 虽然框架有上述的多种好处,但也面临着各种权衡。 死板的框架能够掩盖创新 框架常常决定支持哪种技术方案。由于支持所有能想到的技术是不现实的,所以一个比较死板的框架也是有明显的好处的——那就是,它们更鼓励使用某种技术或者更偏好某种设计模式。 死板的框架可以极大简化一个开发者从无到有创建一个系统的工作量。当开发者有很多种方式来完成同一种任务时,他们很容易陷入是否会影响整个系统的细节决策中。对于这些开发者而言,接受一个框架所推荐的技术,可以让他们把重点放在构建他们系统的业务逻辑上。有一个普适并且一致的技术偏好对于整个公司而言更有利,虽然这个答案还不尽完美。 当然,你也必须处理程序和团队的长尾问题,有些产品需求或者团队喜好和现有的框架可能并不完全适配。框架的维护者被放在一个判断什么是最佳实践的位置上,也会需要确认一个不方便的使用案例是否“真实”,因为这个案例可能对每个用户来说都不方便。 另一个需要重点考虑的是,即使某些技术在今天看来的确是一个最佳实践,但技术也在不断快速地进化,对于框架而言也有一定的风险无法跟上技术创新的发展。采用不同的程序设计方案可能开发成本更高,因为开发者既需要学习框架实现细节,也需要框架维护者的协助。 普适性也会导致不必要的抽象 很多框架优势,例如通用的控制面板(后面解释),只有当大部分关键重要的应用都基于这个框架才有真正的意义。这样的一个框架必须足够普适来支持绝大多数用户的场景,这也意味着必须拥有丰富的请求生命周期,以及任何程序都需要的所有的扩展点。这些需求有必要在应用和底层库之间增加若干间接层,这些层也会增加学习成本以及CPU开销。对于应用开发者而言,软件栈中的更多层也会导致调试更复杂。 另一个框架潜在的缺陷是,它们需要工程师额外学习。当新来的Google员工学习如何让一个“hello world”样例运行的时候,他们经常会被他们所需要学习的技术的数量所抓狂。一个功能完备的框架也会让这个情况更糟而不会更好。 Google已经开始尝试让每个框架的核心尽量简单符合预期,让其他功能都成为可选模块,从而来解决这些问题。Google也会尝试提供配套框架的工具,可以帮助了解框架内部结构来简化调试。最终,虽然框架有一些你必须学习的成本,但你需要确保任何一个给定的框架提供了足够的好处来弥补这个成本。不同的编程语言的框架也许有不同的权衡组合,对于开发者而言这也是一个新的决策点和成本/收益权衡场景。 重要的框架特性 如上所述,反转控制和扩展性是框架最为基础的两个特点。除了这些基础能力以外,框架还需要负责若干其他功能。 标准化程序生命周期 再次重申,反转控制意味着框架拥有并且使一个应用程序的整个生命周期标准化,但这样的结构带来什么样的好处呢?我们以避免级联故障为例来说明这个问题。 级联故障是一个典型的引起系统超载的原因,Google内部也很有很多。它可能发生在分布式系统的部分服务失败,增加了其他部分失败的可能。关于级联故障更多的原因,以及如何避免他们,参考SRE(Site Reliability Engineering (O’Reilly Media, 2016))中的定位级联故障这一章。 Google的服务框架有很多内置的避免级联故障的保护功能,两个最重要的原则是: 持续运行。如果一个服务能成功响应请求,那么就应该保持。如果它能处理一部分请求但不能处理其他部分请求,那么就必须继续运行,而且响应它能服务的请求。 快速启动。一个服务必须尽量快地启动。更快地启动意味着可以快速从崩溃中恢复。服务必须避免类似启动时顺序等待RPC访问外部系统返回这样的操作。 Google生产环境给每个服务配置了一个成为健康状态(可以响应请求)的时长。如果超时了,系统则认为一个无法恢复的故障发生了,则终止这个服务进程。 有一个常见的反模式,常发生在缺乏框架的场景中:一个库创建他们管理的RPC连接,并等待连接可用。当服务端代码随着时间推移不断膨胀,你能获得一个实际有几十个这样的有序依赖的库。这样的结果是,处理初始化代码有效展开的话看起来如图1所示: 在通常的情况下,这样的代码工作很好。但因为没有任何潜在问题的提示,这样的代码会有个特殊问题。这个问题只有当一个相关的后端服务慢了或者同时挂了才会显现出来——这时关键服务启动已经延迟了。如果服务启动足够慢,那它将会在有机会处理请求之前被杀死,这会导致一场级联故障。 一种可能的改进是先创建RPC stubs,如图2所示,然后并行等待它们初始化完毕。在这种情况下,你只需等待stub初始化时间最长的那个而不是初始化时间之和。 虽然处理的并不十分完美,但这个有限的重构也说明了你需要某种在库所创建的RPC stubs之间的协调机制——它们必须可以有等待stub以及库以外的某些资源的能力。在Google的案例中,这样的事情是由服务端框架负责的,同时它也有如下功能: 通过定期拉取可用性来并行等待所有stubs可用(<1秒)。一旦配置的超时到了,即使并不是所有后端服务都准备好,服务依然可以继续初始化。 去掉用于人工或者机器可读的调试日志,而是用集成标准化监控和报警来代替。 通过一个通用的机制来支持插拔不同的资源,而不仅是RPC stubs。技术上来说,仅有返回布尔值的函数(为了“我是否准备好”)以及对应的名字,是有打印日志的必要。这些插拔点常被那些处理资源的通用库(例如:文件API)所使用;程序开发者通常只需要用这个库,就能自动地拥有这些能力 提供一个中心化的方式配置特定的关键后端服务,可以改变他们启动或者运行时的行为。 对任何一个单独的库来说,这些功能可能(也正确地)被认为是多了,但实现这些功能,能够让你在一个集中的地方实现,并在所有用这个库的后端服务都能生效。这也是有意义的一件事情。就像共享库是一种在不同应用程序中分享代码的方式,在这个定义下,框架也是一种在不同库之间共享功能的方式。 因为有类似这些的功能,SRE们更愿意支持基于框架的服务。他们也经过鼓励他们对口的程序员选择基于框架的开发方案。框架提供了一个生产规范的基础水平。这个生产规范将一堆没有关联的库关联到一起时是非常难以实现的——但也不是不可能。 标准化的请求生命周期 虽然细节取决于应用程序的类型,但很多框架支持在总的程序生命周期以外的生命周期管理。对于Google的服务端框架而言,最为重要的任务单位就是请求。除了遵循类似依赖反转的模型以外,请求生命周期管理的目标是将请求的不同方面的职责划分成独立的可扩展的代码片段。这允许程序开发者只关注于开发让他们的应用程序独一无二的实际业务逻辑。 这里有一个这样的实际使用的框架例子,它的组件片段见图3: Processors——拦截请求和返回的包。常用于打印日志,但也有一些短路请求的功能(例如,强制在整个程序中不强制执行不变量) Action——程序业务逻辑,接受请求,返回一个对象,可能有一些副作用。 Exception handler——将一个未捕捉的异常转换为一个响应对象。 Response handler——序列化一个响应对象给客户端。 虽然程序能从框架的扩展点中收益,程序代码的绝大部分都是以响应的形式执行,包括程序特有的业务逻辑。...

December 5, 2022 · Skyan