好久没有系统性地翻译文章了,最近看到这篇 Serverless Best Practices 感觉还不错,就尝试一下,翻译完这一篇后的感受是:阅读和翻译是两码事~~~
原文链接:Serverless Best Practices
在社区里(译者注:Medium),「最佳实践」已经被我们讨论很多年了,但在过去的时间里,只有很少一部分被我们接受。
绝大多数关注这些实践的 Serverless 从业者都在工作中使用了可伸缩的软件架构。Serverless 承诺在伸缩性和突发性的工作负载上发挥作用,所以很多的「最佳实践」是从「可伸缩」的角度来描述的。例如,零售业里的 Nordstrom(译者注:一家美国的高档连锁百货店)、IoT 中的 iRobot(译者注:一种扫地机器人)。如果你的目标不是应用可伸缩,那么你其实无需遵循这些「最佳实践」。
需要注意的是,「最佳实践」不是「唯一实践」。「最佳实践」依赖于一系列的前提假设,如果这些假设和你的场景不匹配,那么说明这些所谓的「最佳实践」并不适合你。
我主要的假设是:我们构建的应用是可伸缩的(即使最终它从没有扩展过)。
基于上面的假设,我的「最佳实践」如下面列举所示。
一个函数只做一件事情
这部分是讲函数错误和伸缩隔离的。
换句话说,如果你在函数中使用了一个分支语句,那么这种做法可能是错误的。
很多教程和框架都是基于单一路由后的一大块函数代码和分支语句实现的。我讨厌这种模式,因为它不具备良好的伸缩性,而且还有向大型和复杂函数演进的趋势。
使用一个或几个函数运行整个应用的问题在于:当你扩展你的应用时,你操作的目标是整个应用,而不只是特定的几个模块。
如果你的 Web 应用中有一部分的代码逻辑请求达到了百万量级,而另一部分的请求是千量级。那么你就不得不去优化这些函数,除了优化百万量级请求的代码,还需要同时优化千量级请求的代码。这是件很浪费时间的事情。同时,可能优化千量级部分的代码也不容易做到。把它们拆分开来吧,这是值得做的事情。
函数不调用其他函数
在函数中调用其他函数是一种反模式。
在一些边界很少的场景中,这种模式(在函数中调用其他函数)是有效的,但拆分这些场景其实并不是很容易。
基本的原则是:不要这样做。如果你这样做了(译者加),成本会很容易翻番,调试也变得更复杂,函数之间的隔离性也不复存在了。
函数在使用时,需要将数据推送到持久化单元或者队列,这种推送可能会触发另一个函数(如果需要的话)。
尽量少地(最好不使用)在函数中使用库
对我来说,这一条是显而易见的。
函数具有冷启动(第一次启动)和热启动(已经启动,并在 warm-pool 里待启动)两类。冷启动受到许多因素的影响,譬如,ZIP 文件的大小(或是已上传的代码)是其中的一部分,需要实例化的库的数量等。
你使用的代码越多,冷启动的速度就越慢。
需要实例化的库越多,冷启动的速度就越慢。
例如,Java 是一种以热启动方式工作的语言,它在一些平台上的的性能很出色。但是如果你使用了大量的库,你会发现应用冷启动的时间会持续很多秒。有很大的可能你不会用这些库,与此同时,冷启动性能不仅会影响启动速度,还会对应用的伸缩性造成影响。
另一方面,我坚定的认为:开发时如非必要,否则不要使用库。这意味应用通过「零库」构建,「零库」结束——除非这个应用离开了库就无法构建。
类似 express 这样的东西是为构建服务端应用而生的,Serverless 应用无需引入所有元素。为什么要引入所有代码和依赖?为什么要引入不必要的代码?那些不必要的代码和依赖,可能永远都无法运行,同时它们可能会带来安全风险。
「尽量少地(最好不使用)在函数中使用库」被定义为最佳实践的原因还有很多。当然,如果你在使用一个库之前,已经对其进行了测试,了解并信任它,那么就放心大胆用吧。使用的库的关键点还是测试、了解、信任,这一点和你根据一个教程来使用库是不一样的。
避免使用基于连接的服务(如 RDBMS)
除非你真的需要,否则不要使用基于连接的服务。
这一条最佳实践可能会给我带来很大的争议。很多 Web 开发人员会在 “but RDBMS are what we know” 的浪潮上跳起来。
其实重点不是 RDBMS,而是连接。
Serverless 应用如果是基于「服务」而不是连接运行,能取得最佳的效果。
「服务」旨在对请求做出快速响应,其后的数据层可以对数据的复杂性做很好的处理——这在 Serverless 领域里具有巨大的价值,这也是为什么像 DynamoDB 这种系统非常契合 Serverless 的设计范式。
坦率地讲,Serverless 的支持者们并不反对使用 RDBMS,他们反对的是使用连接。连接需要时间,想象一下,如果你需要对一个函数的使用范围进行扩展,那么在每个函数的工作环境,都需要新建一个连接,这样会在冷启动时引入系统瓶颈和IO阻塞。这是没必要的。
所以,如果你不得不使用 RDBMS,那么请在中间(译者注:RDBMS和应用之间)设计一个连接池服务吧。如果有一个可以自动扩缩容的容器服务来做这件事就更好了。
在这里最值得强调的一点是:Serverless 架构需要我们重新思考应用中的数据层。这不是 Serverless 架构的错。如果你复用了现有的数据层,但是它并不起作用,那有可能是你缺乏对 Serverless 架构的理解。
一个函数一个路由(如果使用 HTTP)
如果可能,尽可能避免单一的函数路由。因为这种设计不能很好地进行伸缩,同时无法隔离故障。在有些场景下,你可以忽略这条实践建议,例如,如果有一系列路由背后的函数和一个数据表严格绑定,且这部分函数和应用的其他部分是隔离的。但是,上面例子中的情况属于边缘案例,至少在我工作中涉及到的绝大多数应用是如此的。
「一个函数一个路由」会给管理上带来复杂性,但如果这样做,的确会有助于隔离应用在伸缩时出现的错误和问题。按照你的意思去做吧!
但是,既然你已经使用了一系列的配置管理工具了,譬如 CI 或 CD 工具,那么使用 Serverless 相关的 DevOps 工具也是有必要的。
学习使用消息队列(异步 FTW)
如果 Serverles 应用是异步的,那么它往往能取得最好的运行效果。这一点对 Web 服务并不适用,因为 Web 服务倾向于工作在请求-响应型模式下,同时有大量的查询操作需要处理。
可以回想一下上面提到的「函数不调用其他函数」这条实践建议,这样需要重点指出的是,这是(译注:「学习使用消息队列(异步 FTW)」)一种组织函数调用链的方式。队列可以在调用链中充当熔断器的角色,因此,如果某个函数失效了,你可以很轻松地清空备份队列,或者把失败消息发送到死信队列。
从根本上说,你需要去了解分布式系统的若干原理。
在客户端应用-Serverless 后端这种模式中,最好的方法是引入 CQRS(译注:命令查询职责分离模式(Command Query Responsibility Segregation, CQRS))。将数据查询同输入数据分离,是这种模式的关键。
数据流,不是「数据湖」
在一个 Serverless 系统中,数据在整个系统中流动。数据可能最终会中止于一个「数据湖(data lake)」当中,但也可能是:数据虽然在 Serverless 系统中,但却处于某种流动的状态。因此,可以把所有的数据视为动态的,而不是认为它们会停下来。
通常这很难做到,但是在 Serverless 环境下,尽量避免从「数据湖」查询数据。
Serverless 架构需要你重新重点审视数据层。刚接触 Serverless 的新人面临的最大问题是:他们会倾向于将数据指向 RDBMS,而不是将其淘汰掉,同时他们系统中的数据结构也会趋于变得死板。
你会发现,如果使用数据流,那么数据流会跟随着应用的伸缩而同时改变。你所需要做的,就是重定向一个数据流而已。可要是存在一个「数据湖」,事情就变得很困难了。
在这一条实践经验上,我比其他人了解的更多一些。但是,这一条的确不是很容易做到。
面向伸缩编程是错误的,你必须考虑是如何伸缩的
我们可以很容易的去构建一个 Serverless 应用,并对其进行扩展。但是如果你不了解你已经完成的工作,那么你会很容易掉进其他自动伸缩方案的陷阱中。
如果你不对应用本身和应用是如何伸缩的进行深入的思考,后面你会遇到很多坑。如果你的应用存在冷启动(如引入了很多依赖,使用了 RDBMS 等)且已经触达使用峰值,你可能会通过增大函数的并发来解决,但这样做,应用的连接数会越来越多,应用运行会越来越慢。
因此,应用不仅是开发就完事儿了,你还需要去想象它在同样负载下是如何工作的。了解你的应用在负载下的性能,仍然是开放设计工作中的一部分。
结论
关于 Serverless 的最佳实践,可以讲的内容还有很多,在这里我主要是阐述了我认为大家最需要了解的一些观点。
诸如如何部署你的应用,如何考虑应用的花费等类似的内容,我在上面都没有提到,因为我觉得它们稍稍超出了主题的范围。
我很期待听到其他人的观点。可以很确定的是,会有很多人来告诉我,我关于 RDBMS 的观点是错误的。和容器一样,我不讨厌 RDBMS,但是我更喜欢使用合适的工具做合适的事情。去了解你使用的工具吧!