自 2014 年 AWS 发布 Lambda 以来,Serverless 技术的采用率逐年上升。这是因为 Serverless 提供了云服务开发人员无法抗拒的产品,其主要优势如下:
将服务器管理的工作抽象给了供应商
随用随付模式,您只需要为你使用的量来付费
可自动扩展并且高可用
这些好处是通过这种技术具备的一些特性来实现的。Serverless 的应用程序是无状态的分布式系统,可以根据需求进行扩展,提供的是基于事件的异步开发模式。正是因为这些特性,这种技术才为云环境提供了理想的解决方案。
不过,它是否真的像期望的那样理想吗?
经过进一步的调查可以发现,毫无疑问,在 Serverless 的应用过程中,很多开发者也陷入了该模式的一种反模式里面,特别是那些高度利用 Serverless 的场景。随着越来越多的行业从中获得好处,我们必须要清醒地认识到哪些东西是有效的,哪些是无效的。毫无疑问,Serverless 是有效的,不过错误的使用方式也会让它引发很多问题,使行业远离此项技术。
因此,本文的主要目的是揭示那些困扰 Serverless 体系架构的反模式以及如何规避他们,从而推动 Serverless 应用的成功,提升其采用率。
异步特性中的瑕疵
在异步的场景中,Serverless 应用程序往往最合适。埃里克·约翰逊(Eric Johnson) 在伊斯坦布尔 ServerlessDays 演讲中提出了一个概念,即“Serverless 的异步思考方式”。之后,他去了 ServerlessDays Nashville 上发表了更长的演讲版本。
不过,受到追捧的异步特性也是其最大的反模式。在理解这句话之前,你需要记住,Serverless 是即付即用的模式。因此当一个 function(译者注:Serverless 中,每个函数被称为 function,下同。)或者一个服务在等待另一个异步调用的 function 或者服务返回响应结果时,第一个 function 是处在空闲状态的。只需要等待第二个 function 的响应即可。
这是从单体架构转换到无服务架构过程中,不关注细节导致的结果。例如,在一个单体的系统中,一个方法可能需要执行读写 DynamoDB 的操作。然而,为了避免等待此项操作而阻塞整个控制流,可以使用异步的方式进行调用。允许该方法继续调用另一个方法执行其他任务,只是在方法的最后等待 DynamoDB 的响应结果。第二个方法可能是按照自己的方式开始操作 S3。
当上面的逻辑迁移到 Serverless 上面时,不能用相同的方式处理。因为每个方法会映射到每个单独的 Serverless 的 function 上面,但是你需要记住的是,每个 function 都有可能会超时,又或者只完成了他们剩余的任务,之后变成空闲状态等待回调。
结果就是这个处在空闲状态的 function 也会被收费,因为在技术层面上它是活跃状态的。尽管 function 只是在等待,仍有一个 worker 节点使用所有必需的基础架构为该 function 提供服务。
当很多 function 连接在一起的时候,这个问题会更加严重。在流程中,一个 function 对另一个 function 进行异步调用,等待响应,而第二个 function 对另一个 function 进行调用,又或者在对存储服务进行读写操作。
这大大增加了出现不可靠情况的概率,因为第一个 function 有可能会超时。当函数调用的是云服务供应商之外的存储设备或者本地存储服务时,这个情况有可能会更糟糕。
解决方案
解决方案并不是放弃异步的模式,因为问题的关键点不在于异步调用,而是在于合并此类调用的方式。例如,在分解单体系统时,通常存在控制器的 function,这只会增加不必要的开销,而且还会增大超时带来的 function 不可靠的概率。
在这个例子里面,解决方案很简单,只需要重新考虑控制流程。因此上图的函数构造可以转换为如下图所示的结构:
从图里面可以看出,现在存在三个执行一般任务的 function,每个 function 都是由流程中的前一个 function 触发。除了 Serverless 之外,在任何平台上面拥有三个单独的 function 都可能会被认为是效率低下的。但是,必须记住的是,在 Serverless 的场景下,成本取决于执行的时长,而不是 CPU 的资源。因此如果改用 EC2 实例来安排这个任务,可能跟上图的结构就会大大不同。
因此可以看出来,异步按照正确的方式来处理时会带来极大的好处。它可以减少执行的时长,同时在必要的地方支持并行化。不过,如果不加考虑,异步不仅会损害系统的需求,而且还会损害 Serverless 的整个收益模型。
共享不是收养
使用 Serverless 进行构建的目标是,用独立的且高度分离的 function,来拆解业务逻辑。不过说起来容易做起来难。而且开发人员经常会遇到必须在 function 之间共享某些库或者业务逻辑,又或者只是一些基础代码的场景。从而导致了某种程度的依赖和耦合关系,违背了 Serverless 架构的初衷。
不同的 function 之间依赖于一些共享的代码和逻辑,会带来一系列的问题。最突出的问题就是影响到了伸缩性。随着系统规模和功能之间不断地相互依赖,错误、停机和延迟的风险成倍增加。微服务诞生的出发点就是要避免这些问题的。此外,Serverless 的一大卖点也是它的可扩展性。通过共享逻辑和代码库将功能耦合在一起的系统不仅不利于微服务,而且不利于 Serverless 可伸缩性的核心价值。
下图描述了这个场景,因为 function A 中数据逻辑的变更,导致 function B 中,数据通信和处理方式也要发生必要的改变。根据实际的使用情况,function C 也可能会受到影响。
解决方案
在研究解决方案之前,必须要承认的是,在某些场景下可能没有解决方案,不得不共享代码逻辑或者代码库。此类问题出现在机器学习的应用程序中,其中大型库必须在用于处理测试、验证以及训练数据的各种 function 之间共享。在这个过程中需要使用相同的模型,在训练的数据集上进行验证和强化。
在大多数情况下,共享代码库和逻辑需求不仅是一种反模式,而且也同样是 Serverless function 的一种技术限制。例如,AWS 的 Lambda function 在 /tmp 目录下存储上的限制是 512MB。这意味着,开发人员在构建他们 AWS Lambda function 代码的时候,必须要意识到这件事情并且合理运用。毕竟,/tmp 目录主要用于临时存储,因此,一旦 serverless 节点被移除,/tmp 目录下的内容也会不复存在。
AWS 最近通过发布备受期待的 Amazon EFS 和 AWS Lambda 集成解决了这个问题。这种新的集成方式允许 function 通过集成 Amazon EFS 实例的方式,访问共享的类库或者数据。然而,这不能成为 function 之间相互依赖合理性的一种依据。目前可以实现某些事情并不意味着它是上述反模式陷阱的最佳解决方案。
因此,这个解决方案目前只是一个最基本而且有效的解决方案,是一个需要不断构建架构意识的起点。耦合和相互依赖性并不是因为引入 serverless 而带来的新问题。业界各个团队已经提出并实施了很多提高感知度的解决方案。
例如,目前最流行的解决方案之一是 DRY(Don’t Repeat Yourself),这个概念在 1999 年,是由 Andrew Hunt 和 David Thomas 在他们的图书 《实用程序员》 一书中首次提出。取代 DRY 的方式是 WET(Write Everything Twice)。
总体来说,将 function 之间紧紧的耦合在一起,会让微服务和 serverless 带来的收益归零。了解如何构建云架构模式是可以有效避免这种问题的唯一方案。将业务场景拆分成单独的 function ,在概念上可能并不那么容易,不过这仍是必须进行的一项活动,而且还需要谨慎行事。
到底多小才算特别小
将大型紧凑的业务案例拆分成较小、单独的 function 概念这件事,最终证明,有很大概率会到一种有害的粒度级别。拆解单体系统无疑是有好处的,不过也要注意一些开销的问题,否则最终会出现间接费用超过带来收益的情况,所以必须找到这个平衡点。
可以预见的最大开销之一就是这些实例直接通信的开销需求。这个是可以想象到的,因为 serverless 之间是事件驱动的。因此,就像单体大型系统具备的流量控制那样,需要确保事件可以在架构的不同组件之间流动。
在各个 function 之间交流事件的需求,会让人直接想到 Webhook 和 API。这会无形中增加工作量、安全风险和等待的时间。随着 function 的数量不断增加,这个问题会几何倍数的增大。
Serverless 的主要目标是将复杂的基础架构抽象化,从而让人们把更多的关注重点放在业务逻辑上面。但是很明显的是,随着业务逻辑被拆分成各个 function,慢慢达到了平衡点,额外开销也开始逐渐超过了拆分带来的收益。因此这种情况可以被列为一种反模式。
解决方案
云供应商已充分认识到使用无服务器 function 的开销。例如,AWS 发布了 serverless event bus 服务—— AWS EventBridge。该服务减轻了与 function 之间传递事件有关的问题,甚至允许第三方工具将事件发送到 AWS 架构。但是,这不能完全解决问题。
想要找到解决方案,需要先知道什么场景会出现这种问题。使用 serverless 的 function 来提高开发便捷性这件事已经是众所皆知了。构建 function 相对来说非常容易, 因此开发人员更倾向于不断创建新的 function ,导致了过度设计。
因此解决方案应该从开发过程开始的时候,就开始对架构设计进行思考,并且深入理解业务逻辑。这可以先分析客户预期如何使用应用程序,根据其使用的场景来考虑性能问题以及用例,从而实现需求。
主要的目标还是了解用户期望采用的流程,以及在哪个区域的应用程序期望承受更高的负载。因此,对这些要求更为清晰的了解将有助于确定实际所需要的 function 以及它们的作用域。当务之急是与产品经理或其他人一起,制定出业务的目标和流程。
递归可不是朋友
递归是计算机科学中不可或缺的一个概念,它会降低计算机计算时的复杂性,相关的文献中广泛使用的“大 O 表示法”就是递归的典型应用。不过在 serverless 中,递归可能产生无法预期的影响。特别是它的伸缩性会极大地加剧这种影响,应该抵制使用递归,尤其是在递归算法导致无限递归的场景。
在对容器或其他以 CPU 为中心的实例进行编程时,核心问题在于,由于递归会增加处理能力,从而使 CPU 利用率达到最大化。一个个 function 会不断地自我触发,这可能导致在各个线程中触发的 function 数量呈指数增长。
在 Serverless 的场景下,实例的 CPU 利用率达到最大并不是问题,因为它可以自动伸缩。从理论上讲,可以增加无限数量的工作节点,甚至可以满足最严格的递归算法的需求。但是,从成本的角度出发,递归会导致 DoW(Denial on Wallet) 的攻击。
时刻需要记住,在 Serverless 的场景下,计费里面不止包含计算能力,还包括调用和运行时间。结果就是递归导致你云供应商收费激增。选择 Serverless 模式是为了节约成本,而这种使用方式却抵消了这种优势,你以为费用下降了,但实际却恰恰相反。
解决方案
显而易见的解决方案是在构建无服务器应用程序时注意代码库中的递归算法。但是,可能有些应用程序仍需要递归操作。例如,在机器学习的应用中,重复训练模型直到在训练数据或验证数据上达到一定有意义的精度。但是,问题是可以提供多少次递归?
就像前面说的,递归函数最大的威胁点是无限递归的可能性。不过在大多数情况下,这是一种意料之外的结果,程序理论上不需要特殊处理无限递归。因此如果需要使用到递归,请确保进行严格的测试,尽可能完全避免这种问题。
此外,您也可以在自己的 function 之间传递数据,以保持一个递归计数,并设置一个失效保护的开关,以便当递归计数达到一定的数量时停止正在运行的 function。这将使系统知道递归发生的次数,并允许可配置的限制随着时间的推移而变化,同时考虑到无服务器应用程序的成本和其他因素。这样你的系统就知道递归实际发生的次数,并允许配置一个限制,可以随着时间的变化而变化,同时牢记成本和无服务器应用程序的其他因素。
结论
无服务器无疑正在彻底改变应用程序在云服务上面的构建方式。但是,随着这种新模型和体系结构的出现,它具有了自己独特的反模式。
如果不小心谨慎的话,这些反模式很容易浮现,抹去选择无服务器架构获得的所有好处。实际上,根据问题的严重性,无服务器架构可能对业务应用程序有害无益。
但是,该技术的优势极大地促进了其应用。因此多年来,云社区已经看到了很高的采用率。我们有必要积极了解如何在无服务器场景下构建软件,同时注意避免反模式。俗话说,强大的力量伴随着巨大的责任!
原文链接:
https://medium.com/thundra/breaking-down-serverless-anti-patterns-c91cdfd2d6a2
评论