全栈认知:应用框架
【引子】 “探索嵌入式应用框架(EAF)” 的那篇文字是应用框架在嵌入式领域的具体示例,实际上,在服务器领域,应用框架更是俯拾皆是。五一假期的时候, 开始为全栈系列填坑,弥补空间维度的一个重要节点——应用框架,根据日常累积,赘述成文。
软件的应用框架是软件系统的一层抽象, 是提供了通用的软件功能,可以通过用户编写代码有选择地改变, 从而提供特定的软件应用。 应用框架还提供了构建和部署应用程序的标准方式。 作为一个通用的、可重用的软件环境, 它提供特定的功能, 作为软件的一部分, 来促进软件应用程序、产品和解决方案的开发。 应用框架可能包括辅助程序、编译器、代码库、工具集和API , 汇集了所有不同的组件, 便于一个项目或系统的开发。
框架与库
对于研发效率以及产品质量的提升而言,共享库都具有重要的地位。共享库高效地实现了代码的重用,促进了团队间产出的一致性。但是,开发人员仍然需要选择正确的库,并清楚如何正确配置这些库,并将这些库正确地应用在工程中。换句话说,正确并高效地使用库,并不是一件容易的事。而通过预先安装和预先配置好的库,应用框架简化了开发人员的工作量,并进一步提高了一致性。当然,没有免费的午餐,使用应用框架的代价是某些灵活性的牺牲。
应用框架不是单纯的一堆库集合,它一般都管理了整个应用程序的生命周期。有质量保障的应用框架可以实现规模化开发,例如,避免对每个应用程序进行深入的安全性代码审查。另外,应用框架所提供的跨团队乃至跨语言的一致性也是高级自动化和智能系统的必要基础。
应用框架在许多方面具有共享库的优点,两个主要的区别是: 控制反转和可扩展性。
控制反转
在从零开始构建的应用程序中,开发人员来决定程序中正常的控制流程。在基于应用框架的程序中,应用框架自身将实现流程控制并调用开发者的代码,即反向控制或控制反转。应用框架的控制流程在所有应用程序中都有良好的定义和标准。理想情况下,应用程序只实现特定业务的逻辑,而应用框架框架来可以处理所有其他细节。
可扩展性
可扩展性与控制反转密切相关,调用的流程由应用框架拥有,所以改变调用行为的唯一机制是通过应用框架预留的扩展模块或接口。例如,服务器框架可能有某个扩展接口,允许应用程序在每次请求的前后运行一些代码。这还意味着,应用框架的非扩展部分是固定的,不能被应用程序更改。
应用框架的好处
除了类似共享库提供的功能之外,应用框架还有许多好处,整个使用方如开发人员,研发团队和公司乃至社区都会受益。
开发人员
很明显,开发人员是应用框架的直接受益者,使用应用框架提高了生产力、简洁性和与最佳实践的一致性。开发人员可以充分利用应用框架的内置特性,从而减少了代码的编写,而且使编写的代码更简单。应用框架通过提供合理的缺省值和缺省配置,消除了很多繁琐耗时的开发工作,为最佳实践提供了一条简洁的路径。
研发团队
除了提高开发人员的生产力之外,应用框架还有利于整个研发团队,使团队不用构建冗余的基础架构,可以进一步专注于产品的与众不同。另一方面,研发团队同样受益于应用框架带来的底层基础设施的隔离。应用框架提供的额外抽象意味着一些基础设施完全可以由维护框架的人来处理。另外,应用框架可以简化发布和评审流程,标准化也将使系统更容易管理。
公司与社区
在公司层面,通用的应用框架可以减少开发新应用程序所需的时间,也增强了开发人员的使用效率。如果一个公司拥有一个足够大的开发人员社区,那么投资高质量的文档和培训项目是值得的; 反过来,这也有助于吸引社区本身贡献文档和代码。应用框架的广泛使用还意味着,对应用框架的较小改进就可以产生较大的影响。
随着时间的推移,应用框架可以对不断变化的场景做出广泛地应对。例如,如果依赖于一致的微服务/RPC框架,并且带宽相对于 CPU 变得更加昂贵,那么,应用框架默认值可以根据成本权衡来调整参数。
框架的权衡
“欲知轻重而无以,予之以权衡”,虽然应用框架具有很多好处,但同样有利有弊,需要我们仔细权衡。
偏执于框架会阻碍创新
通常,应用框架必须选择支持哪种类型的技术,例如编程语言的选择。支持所有的技术是不切实际的,如果应用框架鼓励使用某些技术或设计模式,针对这些技术或模式有明显的优势。
应用框架的相对固化可以极大地简化开发人员的工作,使他们能够直接开发新系统而不被对系统中那些细微的决策细节所淹没。对于开发人员来说,接受相对固化的应用框架允许他们专注于构建业务系统。拥有一个共同的和一致的技术偏好也有利于整个公司,即使这个技术可能并不完美。
当需要管理大量的应用程序和研发团队的时候,某些产品的需求或团队擅长的技术可能不太适合现有的应用框架。应用框架维护者决定了什么是最佳实践,什么不是最佳实践,判断一个非常规用例是否“有效”,这可能会让参与者感到不爽。
另一个重要的因素是,即使某些东西在今天是最佳实践,但技术发展很快,肯定会存在应用框架无法跟上创新步伐的风险。由于开发者需要学习框架实现的细节或者依赖于框架维护者的帮助,因此如果尝试替代基于应用框架的应用程序,成本可能会更高。
普遍性会导致不必要的抽象
许多应用框架都有着公共控制平面,必须足够通用才能支持绝大多数用例,这实际上意味着应用框架拥有丰富的生命周期管理方式,以及任何应用程序都需要的可扩展性。这些需求必然会在应用程序和底层库之间添加一些中间层,这不但会增加CPU 的开销,还会增加认知成本。对于开发人员来说,更多的软件分层层会使调试更加复杂。
应用框架的另一个潜在缺点是,开发人员必须学习这些框架。新技术的入门不再是“ hello world”,一个功能全面的应用框架可能会使新技术的学习变得更复杂,而不是更好。
一般地,通过努力可以使每个应用框架的核心尽可能简单且高效,并将其他功能作为可选模块,还会提供支持应用框架的工具,利用框架固有的结构来简化调试。然而,最终,应用框架的采用存在一个固有的成本,要确保采用应用框架有足够的好处并且学习成本是否合理。不同编程语言的应用框架也可能有不同的配置方案,从而为开发人员提供了一个决策和成本/收益场景。
应用框架的重要特性
如前所述,控制反转和可扩展性是应用框架的两个基本特性,除此之外,还有一些其他的重要特性。
标准化的应用程序生命周期
控制反转的应用框架意味着拥有并标准化了整个应用程序的生命周期,但是这种结构实际上能带来什么好处呢?一个典型的用例是避免级联故障。
众所周知,级联故障是系统中断的常见原因。当一个分布式系统的部分节点出现故障时,会发生级联故障,也增加了其他部分出现故障的可能性。
一些应用框架有一些内置的防级联故障的保护措施,其中最重要的原则是:
持续服务
如果服务器能够成功地应答请求,肯定是可以持续服务的。如果它只能够成功地服务于某些类型的请求,但不能服务于其他的请求,那么它就应该继续运行并响应能够服务的请求。通俗的说,就是服务降级。
快速启动
服务器应该支持快速启动,因为更快的启动意味着更快的从崩溃中恢复。服务器应该避免依赖外部系统的响应来完成初始化。
在没有应用框架的情况下,自然会出现一种常见的反模式: 库创建自己的 RPC 连接,然后等待连接就绪。随着时间的推移,服务端所使用的库会不断增增加,最终可能会有数十个这样的库存在这样的依赖,结果使服务器的初始化代码变得越来越冗长。当一个相关的后端服务变慢或完全停止运行时,这个问题就会显现出来——现在主服务器的启动被延迟了。如果启动被严重延迟,那么它在有开始处理请求之前就可能会被干掉,最终导致级联故障。
应用框架提供了一个生产环境的正常基准水平,如果只是将一堆互不相关的库粘合在一起,那这个基准水平将很难实现。
标准化的请求生命周期管理
虽然细节取决于应用程序的类型,但许多应用框架还支持细粒度的生命周期管理。对于服务端的应用框架而言,最重要的工作单元是请求。遵循类似的控制反转模型,请求的生命周期管理是将请求中不同方面的职责划分为单独的可扩展代码,从而使开发人员可以专注于编写实际的业务逻辑。
具体地,对网络安全而言,需要防范各种各样的安全漏洞,例如 XSS (跨网站脚本)。XSS 漏洞通常是由于应用程序返回的响应数据没有经过足够的过滤或转义。传统方法一般是添加缺失的转义数据,并希望测试和代码审查来防止出现类似的问题。
从根本上讲,这种方法是行不通的,既然应用程序接受了字符串或类似的非结构化/非类型化数据,针对的底层API就很容易会出现 XSS 之类的错误。例如,JavaServlet 为应用程序提供了一个原始的 Writer,可以向它传递任意的字符。这个方法给开发人员带来了太多的负担,而安全的应用框架会具有上下文转义感知的 HTML 模板系统以及抗 SQL注入的数据库访问API。
一个良好的服务端应用框架在请求的生命周期中补充了这些安全API的使用,从而使程序代码不处理原始的字符串。相反,代码返回的类型保证了格式良好的对象构造。这些响应的转换是响应处理程序的职责,通常是应用框架的内置部分,改善了所有用户的访问安全性。基于标准框架的服务端可以有效地跳过服务端启动所需的许多安全审查,因为应用框架本身可以保证某些行为的安全性。
当然,结构化和可扩展的请求生命周期管理远远超出了将业务逻辑与响应序列化分离的范畴。最基本的好处是,它使每个组件都很小,并且易于演进,这有助于长期的代码健康。应用程序可以在不触及每个具体动作的前提下引入自己的特性,在某些情况下,这些特性是特定于领域的,但可能最终可以上移到应用框架的自身实现。
公共控制层面
公共控制层面包括了所有非特定应用的输入和输出,例如操作控制、监控、日志记录和配置。基于公共控制层面,服务端之间的一致性使故障排除更加容易,跨服务的公共控制面也使自动化的共享成为可能。例如,如果所有服务端都以标准方式导出错误,那么修改发布流水线以执行自动销毁就成为可能: 可以先小流量发布一个新文件,然后再在执行全量发布之前查找错误点。
应用框架实现了跨越应用控制层面的一致性。虽然一般来说,只有少数人关心控制层面的精确组成,但拥有一个统一方式对公司来说具有巨大的价值。一致性意味着可以轻松地实现文件共享和自动化扩展。通过简化与周围生态系统的集成,可以获得标准化使用的多重好处。
实现公共控制层面的一个挑战是,应用框架的维护者通常会发现编程语言库之间的不一致性。例如,大多数编程语言都有一个命令行参数的概念,从积极的一面来看,语法在一定程度上在各种语言之间是兼容的。如今,库的所有者更加意识到跨语言一致性的价值,以及考虑这种一致性的必要性。
模块化
尽管大多数开发人员使用同一个的代码仓库,但是团队之间的工程实践仍然存在较大差异。技术框架的选择通常取决于项目的技术领导,而缺乏有自上而下的授权。可以理解的是,人们倾向于选择自己以前有经验的技术。因此,为了让一项新技术获得广泛的应用,它必须具有明显的价值或者较低的进入门槛; 通常,它必须两者兼而有之。
核心以及请求的生命周期管理是应用框架的核心特性,所有其他功能都可以捆绑到可选的独立模块中,开发人员可以选择将哪些模块添加到他们的服务中。
模块的独立性还允许将特定于应用程序的模块轻松替换为标准框架的模块。因为标准框架模块与特定于应用的模块使用了相同的可扩展API,所以将有用的功能上移到应用框架通常只是移动代码的位置而已。这样,应用框架会成为不断增长的最佳实践集合, 使许多开发人员免于执行复杂或昂贵的迁移,对于许多研发团队来说,这是应用框架最引人注目的好处之一。
应用框架维护者的一个角色是确保模块之间的正确协作,选择默认的模块列表,或者提供关于在不同情况下应该使用哪些模块的建议和约束。尽管开发人员倾向于使用细粒度模块来实现灵活性,但框架维护者很难确保所有组合都能很好地协同工作。
应用框架的进一步抽象——例如 微服务的平台化
应用框架的广泛使用所提供的标准化为更高级别的工具化和自动化带来了机会,例如,微服务的平台化。
微服务之前的单体服务
过去,无论涉及怎样的服务发布,除了编码之外,都要涉及集成测试、启动安全和隐私评审、 上线部署、执行发布、收集和保存日志、AB实验以及调试和解决故障等等,这些都是所有服务必须完成的过程。因此,很多时候不是部署新的服务器,而是向现有服务中添加代码。这样。团队可以专注于他们的业务逻辑,并“免费”获得可用的工程环境。当然,一旦有很多团队采用这种方法,目标服务器就会不断增长,从而变成巨大的且难以维护的单体服务。
单体服务有许多负面影响。在开发人员的生产力方面,必须处理缓慢的构建、缓慢的服务器启动,以及在尝试提交更改时的沙盒测试很可能中断的问题。
当发布版本的时候,按计划整体发布是很有挑战的。随着单体服务的增长,有贡献的开发人员的数量也在增长,这自然会导致更多的异常阻塞。延迟发布可能会使实现下一个发布变得更加困难,最终会形成一个恶性循环。
在生产环境中,单体服务在表面上毫无关联的服务之间构建了一个危险的命运共享,同时也增加了意外交互导致bug的可能性。巨型的单体服务使得资源供应更加困难,独立地扩展这类服务几乎是不可能的。
远离面向服务器的世界
尽管大家都清楚单体服务是不可持续的,但是没有好的替代方案。开发人员应该专注于他们特定于应用服务的业务逻辑,并且其他所有东西都应该尽可能地自动化,从这个目标出发,应用框架的需求最终变得清晰起来:
开发人员应该声明和实现服务的API,而 协调API的运行方式是应用框架平台的角色。
自动化所需的所有元数据,包括生产配置,应与服务代码一起声明。
服务之间的资源和依赖关系应该是明确的且可声明的。理想情况下,我们应该能够通过查看服务全域的元数据来可视化整个生产拓扑。服务应该相互隔离,以便任意服务都可以共同组装到服务器中。而在其他要求中,这意味着要避免全局状态和副作用。
当这些要求得到满足时,几乎所有以前的手工过程都可以自动化。例如,在运行集成测试时,测试基础结构可以是元数据连接服务依赖关系图的一部分。一个使用这些原则的应用框架平台,可以被其他团队有机地采用,并最终分离成一个单独的项目。
如今,基于微服务的应用框架平台几乎成为了新服务开发的行业标准,一个重要原因是它同时吸引了小型团队和大型组织。由于高水平的自动化,小型团队现在可以轻松地在几天内提供高质量的生产服务,而在此之前,最佳实践可能需要几个月的时间。对于大型组织来说,团队间的一致性降低了支持成本,而应用框架平台意味着特定于组织的基础设施团队通常不再是必要的。
转向微服务的另一个好处是鼓励开发人员更多地考虑服务之间的合理分工,这将导致更合理的系统架构。使用诸如 gRPC 和分布式缓存之类的技术作为不同系统之间的边界,迫使开发人员以一种面向不一确定性的方式来考虑API,而不只是考虑在同一进程中使用函数调用的情况。RPC 系统也与语言无关,因此每个微服务的拥有者可以独立决定使用哪种语言。
剩下的挑战,就是提供更高级别的工具来管理日益增多的微服务。例如,以前编写的监控软件能假定只有相对少量的目标对象,这就需要一个新的用户界面来容纳更多监控对象。
微服务与应用框架的关系
应用框架是微服务平台的重要组成部分,原因如下:
应用框架生命周期中固有的控制反转/服务模式自然而然地适用于这种模式,即应用程序开发人员只将他们的服务实现交给平台。
许多平台特性都需要通用控制服务(跨服务器和语言) ,包括发布管理、监视和日志记录。
模块化意味着平台和应用程序代码都可以提供独立的模块,当这些模块组合在一起时,可以以一种健全的方式构成一个完整的服务。
如前所述,应用框架可以提供更高级别的封装,从而简化了应用程序的编写,并提供了与底层库的隔离。与此类似,面向微服务的应用框架平台不仅仅封装了代码,还封装了如生产配置之类的其他组件。这允许更高级别的简化以及故障域间的相互隔离。例如,维护人员可以(如果必要的话)自动应用紧急代码修正(回滚)或配置更改,重新构建所有受影响的文件,并以统一的方式将它们推向生产环境。
然而,使用微服务的应用框架平台确实存在一些挑战。其中最大的一个问题是,强制约束使微服务平台正常运行所需的所有不变量可能是繁重的,甚至可能影响应用程序的编码方式。举个例子,Java 服务共享的某些线程池。所有服务必须彼此隔离的要求意味着不能允许有阻塞请求的线程模型。出于这个原因,服务被要求只能使用异步,然而这种解决方案并不能让所有的团队都满意。
另一个挑战是,在微服务之间添加更多的节点可能会增加总体请求的延迟。在某些情况下,这种延迟可以通过体系结构的改进来减轻。对于微服务的应用框架平台,要确保恰好位于同一服务端上的服务之间的请求使用优化的进程内传输。
小结
虽然,应用框架是一个强大的工具,但也有一些缺点,并不是所有团队都愿意维护应用框架的标准化和定义良好的行为方式,同时不能过度规范化。然而,当应用框架达到合理的平衡时,可以大幅提高研发团队的生产力。
应用框架所提供的一致性对于研发团队来说是一个福音,为构建更高层次的抽象提供了基础,例如微服务的应用框架平台化,它为系统架构和自动化开启了新的机遇。