设计Ruby无服务器运行时

2021-01-24 04:15:07

上周,Google宣布了Ruby运行时针对Cloud Functions(Google的功能即服务(FaaS)托管平台)的公开测试版。在过去的一年左右的时间里,对Ruby的支持已经落后于其他语言,但是现在我们已经赶上了,我想我会分享该产品背后的一些设计过程。

本文不是传统的设计文档。我不会逐步介绍设计本身。相反,我想讨论我们面临的一些设计问题,做出的决定以及为什么做出这些决定,因为这是弄清楚如何将Ruby约定与公共云的约定融合在一起的有趣练习。我认为,我们做出的一些权衡标志着整个Ruby社区随着行业的发展而面临的挑战。

为无服务器产品提供Ruby支持比您预期的要复杂得多。从最基本的角度来看,语言运行时只是Ruby的安装,并且可以肯定的是,配置Ruby映像并将其安装在VM上并不难。但是,当您将“无服务器”加入其中时,事情会变得更加复杂。 Severless不仅仅是自动维护和扩展。这是对计算资源的完全不同的思考方式,这与过去15年中我们学到的有关部署Ruby应用程序的许多知识背道而驰。当Google Cloud的Ruby团队承担为Cloud Functions设计Ruby运行时的任务时,我们还承担了提出Ruby的无服务器方式的艰巨任务。在保持社区熟悉的Ruby习惯用法,实践和工具不变的同时,我们还必须重新思考如何在几乎每个级别上进行Web应用程序开发,从代码到依赖,持久性,测试等所有方面。

本文将研究我们在设计的五个不同方面的方法:函数语法,并发性和生命周期,测试,依赖项和标准。在每种情况下,我们都将在保持忠实于Ruby根源的重要性与拥抱新的无服务器范式的愿望之间取得平衡。我们非常努力地保持与传统Ruby处理方式的连续性,并且还从其他Google Cloud Functions语言运行时中汲取了线索,并借鉴了其他云提供商的无服务器产品所树立的先例。但是,在少数情况下,我们选择另辟trail径。当我们认为当前的方法滥用语言功能或误导并鼓励有关无服务器应用程序开发的错误想法时,我们就这样做了。

其中某些决定最终有可能甚至最终被证明是错误的。这就是为什么我现在提供这篇文章,以讨论我们已经做的事情,并开始关于我们作为Ruby社区如何实践无服务器应用程序开发的对话。好消息是Ruby是一种非常灵活的语言,随着我们的学习和需求的发展,我们将有很多机会适应。

因此,让我们看一下我们做出的一些初始设计决策和权衡,以及做出这些决策的原因。

“功能即服务”(FaaS)当前是较流行的无服务器范例之一。 Google的Cloud Functions只是一种实现。许多其他主要的云提供商都拥有自己的FaaS产品,并且也有开源实现。

当然,这种想法是使用一种编程模型,该模型不以Web服务器为中心,而是以函数为中心:无状态的代码段,它们接受输入参数并返回结果。这似乎是一个简单,几乎显而易见的术语更改,但实际上具有深远的意义。

Ruby面临的第一个挑战是,与许多其他编程语言不同,Ruby实际上没有一流的功能。 Ruby首先是一种面向对象的语言。当我们编写代码并将其包装在def中时,我们正在编写一种方法,该代码将响应发送给对象的消息而运行。这是一个重要的区别,因为形成方法调用上下文的对象和类不是无服务器抽象的一部分。因此,它们的存在会使无服务器应用程序复杂化,甚至在编写应用程序时会误导我们。

例如,某些FaaS框架使您可以在Ruby文件的顶层编写带有def的函数:

尽管这段代码看起来很简单,但重要的是要记住它的实际作用。它将此“函数”作为私有方法添加到Object类(Ruby类层次结构的基类)上。换句话说,“功能”已被添加到Ruby虚拟机中的几乎每个对象中。 (当然,除非应用程序在加载文件时更改主要对象和类上下文,否则该技术会带来其他风险。)充其量,这会破坏封装和单一职责。最坏的情况是,它有可能干扰应用程序的功能,其依赖项甚至是Ruby标准库的风险。这就是为什么在大型Ruby应用程序中不建议在简单的单文件Ruby脚本和Rakefile中使用这种“顶层”方法的原因。

Google Ruby小组认为此问题非常严重,我们选择了另一种语法,将函数编写为块:

这提供了一种类似于Ruby的方法来定义函数,而无需修改Object基类。它还有一些附带好处:

名称(在这种情况下为“ handler”)只是一个字符串参数。它不必是合法的Ruby方法名称,也不必担心它与Ruby关键字冲突。

块比方法具有更多的传统词法作用域,因此其行为与其他语言中的函数更相似。

块语法使管理函数定义更加容易。例如,可以干净地“取消定义”功能,这对于测试很重要。

它需要一个库来提供用于将功能定义为块的接口。 (此处,Ruby通过使用Functions Framework库遵循了Cloud Functions的其他语言运行时。)

并发很难。这是一般无服务器,尤其是功能即服务的设计所基于的主要观察之一:我们生活在一个并发的世界中,我们需要应对之道。功能范例通过坚持功能不共享状态(通过队列或数据库之类的外部持久性系统除外)来解决并发问题。

实际上,这是我们选择使用块语法而不是方法语法的另一个原因。方法隐含以实例变量形式携带状态的对象,这些状态在无状态FaaS环境中可能无法按预期工作。回避方法是一种微妙但有效的句法方式,可以阻止我们知道存在问题的做法。

就是说,如果您需要共享资源(例如数据库连接池)怎么办?您何时会初始化此类资源,以及如何访问它们?

为此,Ruby运行时支持可初始化资源并将其传递到函数调用中的启动功能。重要的是,尽管启动功能可以创建资源,但普通功能只能读取它们。

需要" functions_framework" #使用on_startup块初始化共享客户端并将其存储在#全局共享数据中。功能框架。 on_startup确实需要" google / cloud / storage" set_global:storage_client,Google :: Cloud :: Storage。 new end#所有功能调用均可通过全局共享数据访问共享的storage_client。功能框架。 http" storage_example"做要求| bucket =全局(:storage_client)。桶"我的桶"文件=桶。文件" path / to / my-file.txt"文件。下载 。发送

注意,我们选择定义特殊方法global和set_global与全局资源进行交互。 (顺便说一句,这些不是对象上的方法,而是我们用作函数上下文的特定类上的方法。)同样,我们可以使用更多传统习语(例如Ruby全局变量,甚至构造函数和实例变量)来将信息从启动代码传递给函数调用。但是,这些成语会传达错误的信息。我们不是在编写正常共享数据的普通Ruby类和方法,而是编写无服务器功能(即使有可能共享数据也是危险的),并且我们认为语法必须强调区别。特殊的方法是经过深思熟虑的设计决策,在存在并发的情况下阻止实践可能很危险。

强大的测试文化对于Ruby社区至关重要。流行的框架(例如Rails)承认了这一点,并通过提供测试工具和框架作为框架的一部分来鼓励主动测试,而Google Cloud Functions的Ruby运行时则通过提供无服务器功能的测试工具来效仿。

FaaS范式实际上非常适合测试。功能本质上易于测试;只需传递参数并声明结果即可。特别是,您不需要启动网络服务器来运行测试,因为网络服务器不是抽象的一部分。 Ruby运行时提供了一个辅助方法模块,用于创建HTTP请求和Cloud Event对象以用作输入,否则大多数测试都非常容易编写。

但是,我们遇到的主要测试挑战之一与测试初始化​​代码有关。确实,这是Google的一些Ruby团队成员在其他框架(包括Rails)中遇到的一个问题:测试应用程序的初始化过程非常困难,因为框架初始化通常在测试运行之前就在测试之外进行。因此,我们设计了一种测试方法,以隔离功能的整个生命周期,包括初始化。这使我们可以在测试中运行初始化,甚至可以重复多次以进行不同方面的测试:

需要"最小/自动运行"需要" functions_framework / testing"类MyTest< Minitest ::测试#包括测试辅助方法,包括FunctionsFramework ::测试def test_startup_tasks#运行生命周期,并单独测试启动任务。 load_temporary" app.rb"做全局变量= run_startup_tasks" storage_example" assert_kind_of Google :: Cloud :: Storage,全局变量[:storage_client] end end def test_storage_request#重新运行整个生命周期,包括启动任务,并#测试函数调用。 load_temporary" app.rb"做请求= make_get_request" https://example.com/foo"响应= call_http" storage_example" ,要求assert_equal 200,响应。状态结束结束结束

load_temporary方法将功能定义加载到沙箱中,将它们及其初始化与其他测试运行隔离开。该方法和其他辅助方法在FunctionsFramework :: Testing模块中定义,可以包含在minitest或rspec测试中。

到目前为止,我们实际上仅提供了针对Ruby运行时的基本测试工具,并且我希望随着用户开发更多应用程序并确定更多常见测试模式,我们将大大增加该工具集。但是我坚信测试工具是所有库的重要组成部分,尤其是那些声称是框架或运行时的库,因此从一开始它就是设计的核心部分。

大多数不平凡的Ruby应用程序都需要第三方gem。对于使用Google Cloud Functions的Ruby应用程序,我们至少需要一个gem,即functions_framework,它提供用于编写​​函数的Ruby接口。您可能还需要其他gem来处理数据,进行身份验证并与其他服务集成等等。依赖性管理是任何运行时框架的关键部分。

我们围绕依赖项管理做出了一些设计决策。首先,也是最重要的是拥抱邦德勒。

我知道这听起来有点轻浮。如今,大多数Ruby应用程序仍在使用Bundler,并且几乎没有替代品,几乎没有广泛使用。但实际上,我们进一步走了一步,将Bundler深入构建到我们的基础架构中,要求应用程序使用它才能与Cloud Functions一起使用。我们这样做是因为,确切地知道应用程序将如何管理其依赖项将使我们能够实施一些重要的优化。

良好的FaaS系统的关键是部署和冷启动的速度。在无服务器的世界中,您的代码可能会快速连续地更新,部署和拆除多次,因此消除瓶颈(如解决和安装依赖项)至关重要。因为我们在一个系统上对依赖关系管理进行了标准化,所以我们能够积极地缓存依赖关系。我们认为,实现这种缓存的性能提高以及Rubygems.org基础结构上的负载减少,远远超过了无法使用Bundler替代项所带来的灵活性降低。

Google Cloud Functions的Ruby运行时的另一个功能(也许是怪癖)是,如果gem lockfile丢失或不一致,部署将失败。部署时,我们要求Gemfile.lock存在。这是执行最佳实践的另一个决定。如果在部署过程中重新解析了锁定文件,则您的构建可能无法重复,并且可能无法与测试时使用的依赖项相同。我们通过要求使用最新的Gemfile.lock文件来避免这种情况,并且由于我们需要使用Bundler,因此我们能够执行此操作。

最后,好的设计取决于标准和现有技术。我们不得不进行一些创新,以便在Ruby中定义健壮的函数,但是在表示函数参数时,已经存在现有的库或新兴标准。

例如,在短期内,许多功能将响应Web挂钩,并且将需要有关传入HTTP请求的信息。设计一个代表HTTP请求的类并不难,但是Ruby社区已经为这种事情提供了一个标准的API:Rack。我们为事件参数采用了Rack请求类,并且支持返回值的标准Rack响应。

需要" functions_framework"功能框架。 http" http_example"做要求| #request是一个Rack :: Request对象。记录器。信息"我收到#{请求。来自#{request的request_method}。 url}!" #您可以返回标准的机架响应阵列,或使用#几种便捷格式之一。 [200,{}," ok" ] 结束

这不仅提供了熟悉的API,而且还使它易于与其他基于Rack的库集成。例如,将Sinatra应用程序放在Cloud Functions上很容易,因为它们都说Rack。

从长远来看,我们越来越希望功能即服务能够作为事件系统中的组件来使用。基于事件的体系结构迅速流行,通常围绕诸如Apache Kafka之类的事件队列。事件体系结构的关键要素是描述事件本身的标准方法,这是事件发送者,代理,传输和消费者所理解的标准。

Google Cloud Functions已将其支持支持CNCF CloudEvents(一种用于描述和传递事件的新兴标准)。除了HTTP请求之外,Cloud Functions还可以以CloudEvent的形式接收数据,并且运行时甚至会在调用函数时将某些旧式事件类型转换为CloudEvents。

需要" functions_framework"功能框架。 cloud_event" my_handler"做事件#event是一个由cloud_events gem logger定义的CloudEvent对象。信息"我收到了#{event类型的CloudEvent。输入}!"结束

为了在Ruby中支持CloudEvents,Google Ruby小组与CNCF无服务器工作组密切合作,甚至自愿接管了CloudEvents Ruby SDK的开发。事实证明,这是很多工作,但是我们认为,即使必须自己实现它,使用正式的标准Ruby接口也至关重要。

在过去的几年中,“无服务器”和“功能即服务”托管引起了很多关注。我认为对于大多数工作负载的实用性尚无定论,但可能性令人着迷。 “零”开发,自动维护和扩展,无需维护服务器,仅支付您实际使用的计算资源。我最近将这个博客从一个个人Kubernetes集群转移到了Google托管的Cloud Run服务,并将我的每月账单从几十美元削减到了几美分。

也就是说,无服务器是从根本上不同的方式来考虑计算资源,并且作为一个行业,我们对含义的理解还处于早期。在我的团队为Google Cloud Functions设计Ruby运行时时,我们很注意无服务器范例与常规Ruby实践的交互方式。在某些情况下,就像进行测试一样,它鼓励我们更加深入地研究Ruby文化。在其他情况下,就像如何使用严格说来没有语言的语言来表达和注释功能一样,它也挑战了我们关于如何呈现代码和传达其意图的想法。

但是在所有情况下,设计运行时的经验提醒我,我们处于一个不断变化的行业。无服务器只是一系列中断中的最新一次,包括一般的公共云,甚至包括Rails以及Ruby本身。目前尚不清楚会出现多少无服务器,但今天就在这里,我们有责任以好奇心,创造力和愿意不理会我们想当然的方式做出回应。