Serverless and Go

本篇内容是根据2019年8月份Serverless and Go音频录制内容的整理与翻译,

JohnnyMatJaana 和特邀嘉宾 Stevenson Jean-Pierre 讨论了 Go 世界中的Serverless。什么是Serverless,Serverless适用于哪些用例,有哪些权衡,以及如何在Serverless环境下使用 Go 进行不同的编程?

过程中为符合中文惯用表达有适当删改, 版权归原作者所有.

Johnny Boursiquot: 大家好,欢迎来到 Go Time。这个节目会邀请多元化的嘉宾和特别来宾,讨论所有与 Go 语言相关的内容,包括代码基础设施、分布式系统、微服务,尤其是今天我们要讨论的无服务器(Serverless)技术。

我是 Johnny Boursiquot,今天和我一起的还有一群非常出色的嘉宾,就像 Mat Ryer 常说的那样,首先是 Mat Ryer 本人,打个招呼吧,Mat。

Mat Ryer: 大家好!

Johnny Boursiquot: 欢迎 Jaana B. Dogan 的回归,也就是 JBD。你最近怎么样,Jaana?

Jaana Dogan: 嗯,还不错。你呢?

Johnny Boursiquot: 我也很好。希望你准备好了,因为这次讨论会很精彩。最后但同样重要的是我们今天的特别嘉宾,Serverless专家 Stevenson Jean-Pierre。Sak pase, Stevenson?

Stevenson Jean-Pierre: Map boule... 你怎么样?

Johnny Boursiquot: [笑] 很好,很好,感谢你的问候。对于那些细心的听众,Stevenson 和我都是海地人,所以刚刚是我们的一点小互动。

今天的节目对我来说非常特别,因为我非常喜欢使用现在被称为无服务器的技术。我们会深入探讨这到底是什么意思,为什么我们称它为无服务器技术,虽然这更像是个营销术语......但我们会深入分析。

我们先来设定一下基础。什么是无服务器技术?这个术语从哪里来?它真正想传达什么?我们知道这是个营销术语,但它的真正意图是什么?当你使用无服务器技术时,为什么会选择它?它到底是什么?我们先做一些基础设定。

Mat Ryer: 从我的角度来看,所谓无服务器就是不需要太担心部署问题。我经常使用 Google App Engine 的标准环境(译者注: 国内前些年有著名的SAE, S是新浪Sina),我很喜欢它。实际上,这也是我最初开始使用 Go 语言的原因,因为当时要使用 App Engine,你要么得写 Java,要么得写 Python,而这两个语言我都不懂......当时还有另一个小语言,上面写着 Go,还有个"实验"标签。我对这种新鲜事物总是充满兴趣,于是我就开始使用它,这也是我第一次接触 Go 的契机。

App Engine 的承诺是,你编写好 Go 代码后,只需把代码交给 Google,他们会负责在需要时运行它。作为开发者,这对我来说非常好,因为我对基础设施并没有太多兴趣或技能......所以至少这个承诺对我来说是非常吸引人的。

Stevenson Jean-Pierre: 对我来说,无服务器技术是两者的结合。首先,它通常是事件驱动的;我认为它是无服务器的,它是由某个行动触发的,而不是一直等待某种请求进来......这是计算层面。然后还有一些数据存储层面的无服务器技术,比如 RDS Serverless 或 [Aurora Serverless]((https://aws.amazon.com/cn/rds/aurora/serverless/ "Aurora Serverless")),你不需要担心底层引擎,也不需要担心配置......它随时准备扩展,当你需要它时,它就在那里。

Jaana Dogan: 对我来说,和 Mat 的看法类似---无服务器技术让我们不需要太多处理基础设施。它更像是一个抽象层,某些事情已经为我们处理好了。

还有一个重要方面,它采用的是按需付费模型。你不使用时,它会自动缩减到零;你按使用量付费。老实说,这才应该是云计算的定义。但这是一个非常复杂的话题,因为无服务器技术已经成为一个大范围的术语,我认为它代表了一些更抽象的东西......但每个更高层次的抽象实际上比下层更"无服务器"。所以我认为可以这样说:你越不需要关心基础设施的操作和维护,并且按需付费,能够缩减到零---这对我来说就是无服务器技术。

Johnny Boursiquot: 所以大家的共同点是都不需要担心运行你的功能的基础设施,对吧?无论是计算、存储,还是与事件源的集成,例如在 AWS 中,它们有各种可以触发函数的东西。作为开发者,你不需要关心底层的基础设施,不需要自己去管理实例或进行配置。基本上,你就是在把不同的 Lego 积木拼接在一起,让它们在你的环境中响应某些事件。

Jaana Dogan: 是的,承诺就是 "你只需关心业务逻辑,其他的一切我们来处理。这是你可以使用的基本模块。"

Johnny Boursiquot: 当我刚开始接触无服务器时---这个术语对不同的人意味着不同的东西,甚至有一个叫做 Serverless 的框架,但我们今天讨论的不是这个。我们谈的是无服务器的概念,而不是某个特定的技术、框架或产品。当我开始探索无服务器时,我经常看到一些使用案例,比如"将图片上传到 S3,然后生成缩略图。" 这些看起来很简单的应用......但实际上,我们都知道事情从来不会那么简单。我们稍后会讨论在进行无服务器开发时你真正需要做的事情。

那么说到使用案例,大家有没有遇到过一些无服务器技术非常适合的情况,或者某些情况下,可能长时间运行的服务反而更合适?我们可以分享一些例子吗?

Stevenson Jean-Pierre: 对我来说,使用无服务器技术通常是一些非常小的微服务应用,甚至不值得为它们单独部署基础设施。它可能是一个只有 50 行代码的小脚本,执行某些特定的功能。还有我之前提到的事件驱动的应用---一些依赖事件触发的功能......另外一个很好的例子是用作通用的定时任务替代方案。你经常会遇到如何让定时任务高可用的问题,特别是当多个服务共享相同的调度时,彼此可能会互相干扰,你需要实现复杂的锁机制......但通过无服务器函数,你可以依赖云供应商提供的高级定时器,使用一个源来处理定时任务。

Mat Ryer: Steven,你提到过几次事件,并且这些函数是响应事件运行的。哪些东西可以算作事件?可以举些例子吗?

Stevenson Jean-Pierre: 无服务器刚开始时,很多使用案例只是纯粹的 HTTP 请求-响应循环。但后来,云供应商开始提供与其他服务的集成功能。例如,对于 Johnny 提到的 S3 上传的案例,现在你可以直接从 S3 获取事件通知,告诉你"某个文件在这个桶里上传了"或"某个文件被删除了",然后你可以异步地对此做出反应。你可以从任何其他类型的源接收类似的通知,它们会推送事件给你,而不是让你主动去轮询。

你会收到一个负载,告诉你发生了什么,并且这个负载的结构是你的函数能够理解的,然后你对其进行处理。这大大简化了工作,它成为了 Johnny 所说的"粘合层",这些服务会主动告诉你它们在做什么,而你则对它们做出响应。

Jaana Dogan: 从云供应商的角度来看,无服务器技术是如此重要,因为它是唯一的交流协议。你需要为某些事件提供一个任意的执行环境,因为云服务商可以和你沟通,但你无法直接与他们沟通。所以无服务器技术的普及并不令人意外,因为这是他们与你互动的基本方式。

Mat Ryer: 是的。在 App Engine 中,尤其是现在最新版本的标准环境中,你只需编写一个普通的 Go 程序,它实际上就是 package main,你可以使用处理程序,做你需要做的事情......然后你把它上传到 App Engine,系统会自动缩小到零,什么也不运行。然后,第一个 HTTP 请求进来时,实例会被启动,你的程序也会启动,理论上你就可以开始响应这些请求了......所以我通常用它来部署网络服务。

对我来说,能构建一个网站或网络服务并且很轻松地部署到 App Engine 上,不用再操心任何事情,真的很有用......它就会一直正常工作。如果没人使用它,那也没关系,它不会产生费用。

有一个叫 [gopherize.me[(https://gopherize.me/)] 的服务,它允许用户用 [Ashley McNamara](https://github.com/ashleymcnamara "gopherize.me[(https://gopherize.me/ "gopherize.me[(https://gopherize.me/)] 的服务,它允许用户用 [Ashley McNamara")] 的服务,它允许用户用 [Ashley McNamara") (译者注: GitHub 开发者关系高级总监)的艺术作品创建个性化的 Gopher 化身......这个服务是运行在 App Engine 上的,有时候它的使用量会很大,超过我的免费配额,我就得为它支付费用。

Stevenson Jean-Pierre: 我认为你提到了一个很关键的点,那就是把无服务器函数当成 web 服务来处理;你有无状态的计算资源,不确定请求会被路由到哪里,也不会在磁盘上保存状态,你始终将状态外部化,因为你无法预知会收到什么......这是处理无服务器时非常重要的思维方式。

Johnny Boursiquot: 我最喜欢的无服务器使用场景之一是处理队列中的消息。我曾经参与的项目中,有些操作不需要同步完成,用户不需要坐在那里等待响应---不像传统的 HTTP 模型。你可以异步地触发某些操作......用户执行某个操作,然后你把数据放到一个队列中,然后某个地方会有服务响应这个数据。

这让前端团队和后端团队的责任分工明确。前端负责用户界面,后端捕获事件并将数据放入队列中,而另一个团队负责从队列中获取数据并处理。这种异步模型不仅解耦了前后端的关注点,还展示了无服务器技术如何超越传统的 HTTP 模型来触发业务功能。

不过我们也遇到了一些问题。很多人也通过博客等分享了类似的经验......无服务器的成本模型,比如 AWS Lambda 或 Google Cloud Functions,因为你不用为空闲时间付费,很多营销都在强调"你会省下很多钱,因为你不用为那些闲置的资源付费",所以有些人会过度使用无服务器功能。

但我们很快就意识到,如果你采用了无服务器的开发方式,你已经不再是在构建一个单体应用,而是将功能拆分成一个个小函数,每个函数只做一件事,然后频繁地执行这些小函数,这样反而可能会增加成本。

我记得有一篇博客文章提到,他们在使用 AWS Lambda 时,每次执行 Lambda 函数时,处理的工作量很小,结果反而花费更多。后来他们意识到可以在一次 Lambda 执行中使用 Go 的并发原语,比如使用 goroutine,在一次执行中并行处理多个任务。这样,你仍然只执行了一次 Lambda,但在里面处理了更多的工作。这样做可以大大节省成本。

如果你只是盲目地按照营销宣传操作,每次需要功能时就调用一个 Lambda 函数,你可能会发现自己陷入了困境。所以我想问问你们,有没有遇到过类似的陷阱?

Jaana Dogan: 我个人认为 Lambda 类似于 CGI 模型......冷启动和启动时间是非常重要的,如果你希望成本优势得以体现。Google Cloud Run 的一个优点是它没有按函数进行部署,而是允许你交付一个长时间运行的服务......虽然它仍然有执行时间限制,比如在 15 分钟内可能会被终止,但至少你可以将多个功能打包在一起......这样在冷启动时,你可以同时服务多个端点。

Stevenson Jean-Pierre: Johnny 提到了一个非常重要的点,那就是无服务器并不是一个万能的解决方案......你需要根据工作负载来选择适合的实例或方案。如果你的工作负载是 24 小时不间断的高吞吐量,那么无服务器可能并不是最佳选择。

Johnny Boursiquot: 非常好的观点。沿着这个思路---我们一直在讨论不同的云服务提供商都有一些稍微不同的解决方案......虽然它们之间有一些共通性,但你会开始看到一些偏差。比如,Google 的 Cloud Run,你会看到在容器化模型方面有一些差异......如果我说错了,请纠正我,Jaana,这可是你的领域......所以我在想---某个时候,有人会问自己,"好吧,每次我写一段代码,它会作为无服务器函数运行,它会在某个地方运行---有没有可能我能以一种与云无关的方式来编写它?有没有可能我不需要导入某种第三方库,不管是哪个云提供商的包,或者是哪个库---有没有可能我只需要写我的函数,然后它可以在 AWS Lambda 上运行,也可以在 Cloud Functions 上运行,或者在 Azure 上运行......有这种可能吗?" 我知道还有 OpenFaaS,这是前几天看到的一个项目,看起来非常有前途。

所以有这些不同的选项......真的有可能以一种与云无关的方式编写所有的函数,并且不需要不同的构建管道、不同的方式,实际上也不需要导入不同的云提供商的库吗?这有多容易?创建抽象的成本是否值得?

Jaana Dogan: 我能问个问题吗......?我对无服务器的可移植性方面一直持怀疑态度,但最后我意识到,就像我导入一个库一样,不管什么,它实际上只是一个小片段。而函数块和其他部分---可重用的部分实际上就在那儿;你只需要调用可能是第三方库中的两行代码......所以这并不是真正的大问题。

这是我个人的观点,但我们现在正试图重新发明所有这些不同的抽象模型,以使无服务器能在各处运行,包括你自己的内部部署......但我在质疑,真的值得拥有那个抽象模型吗,还是仅仅切换那两行代码并导入一个新的库就可以了?

此外,函数的可重用性是一回事,但我认为总体的编排方面和配置是另一回事,而这些目前肯定是基于专有的......这是另一个需要讨论的话题。我认为很容易重用你的处理程序,但如何在另一个云提供商上启动相同的环境,具有相似的命名、相似的扩展属性和配置?我认为那更难。

Mat Ryer: 我喜欢你提出的问题,质疑这个前提......这有点像我们对以后可以更换一个不同的数据库感到非常兴奋,当我们构建了正确的抽象......但你为什么要这么做呢?我尤其听到有人说,"这是一个 MySQL 数据库,但由于这个抽象,我们以后可以换成 Mongo 数据库,如果我们想要切换的话......",但它们实际上做的事情非常不同。我觉得我们对这种可能性感到兴奋,却没有真正考虑到我们是否真的会需要这么做。

这也是另一个问题---Jaana,你提到的这些functionless services本来是为了保持小巧和轻量的,所以我认为如果你打算转移到另一个提供商,那也是一个重新编写某些部分的好机会,因为这是我们本来就应该做的好实践......但这确实是一个有趣的思考,我认为。

Jaana Dogan: 我认为随着限制的变化,你需要考虑到这一点。ORM 曾经很流行,但实际上没有人这么做,因为每次你更换数据库时,你几乎都需要重新设计,至少是数据层。所以我认为问"实现可移植性真的可行吗?"是很自然的。

Stevenson Jean-Pierre: 我认为,虽然处理程序在无服务器函数中并不那么有趣,但真正决定你与云的绑定的部分是你实际在函数中做的事情,对吧?如果你有一个 S3 事件的处理程序,那么你可能会被 S3 API 绑定住。如果你有一个 Google Cloud Storage 事件的处理程序,那么你可能会与 Google Cloud API 进行交互。所以处理程序可能是最容易替换的部分,但你代码库中与特定云 API 相关的其他技术细节,处理事件的部分,将会更难切换......我很少发现多云的论点最终是值得的。

我记得在 2012-2013 年,大家都在谈论多云......但那最终只是一场向最小公分母的竞争,因为你不得不为最低的通用功能标准化,而那从来不是值得的。所以我觉得拥有小的、易于重写的包,以便替换供应商(或其他什么)是更好的办法,因为核心逻辑会保持不变;只是 API 和你获取数据的方式可能会不同。

Johnny Boursiquot: 我想这引出了下一个关键话题,如果我是一个 Go 开发人员,或者说我是一个碰巧用 Go 编写一些函数的开发人员,我该如何设置或组织我的 Go 项目,以便我的业务逻辑、我的行为与云无关,但入口点,比如我必须导入 AWS 或 Azure 的包,或者其他什么......

就我个人而言,我编写无服务器的 Go 项目的方式和编写其他 Go 项目完全一样,我的意思是这样: 在一个常驻的服务中,我会编写我的 package main,以及 function main,我的入口点---我尽量让它保持简洁。我不会在里面放太多东西。可能我会从环境中读取一些参数,或者从配置中读取一些东西,或者从传递的参数中读取,不管是什么情况。我在处理无服务器技术时不会做任何不同的事情。

而且几乎所有的业务逻辑---我不会把那些第三方依赖引入我的业务逻辑中......比如 S3,我不会把它引入我的业务逻辑中。我宁愿为那个行为、那个功能创建某种本地接口,由 S3 的实现来满足。我不会把 DynamoDB 的包引入我的业务逻辑中。我会写一个本地接口,然后由 DynamoDB 的实现来满足。

所以对我来说---我不会以任何不同于其他 Go 编程的方式来编写无服务器程序......我认为这是我想传达的最重要的一点---你所知道的关于 Go 开发的最佳实践,在你开始做无服务器工作时不会消失。你应该努力遵循我们在其他 Go 项目中谈论的相同的原则和最佳实践。Mat?

Mat Ryer: 是的,我认为这是一个很好的教训,对于那些没有很多无服务器经验的人来说,这是一个相当重要的观点。我们所说的是,是的,行为上可能会有变化,你可能会在无服务器环境中以不同的方式编写代码,但那些事情本身就是好事。对此感到很鼓舞,因为有可能---其实在 App Engine 上,直到最近的发布,你不得不以稍微不同的方式来做事情,所以你被迫创建了一些你可能不满意的抽象,或者对你的项目做出其他更改。他们现在改变了这一点,所以正如我所说的,你只需部署 package main;这就是你部署的内容。还有一些随之而来的变化。

比如,Jaana,你提到了冷启动问题。这是指没有实例正在运行,第一次请求进来时,它需要做一些工作来启动实例运行......而你希望它尽可能快地完成。那可能意味着你会推迟某些处理程序的设置,直到稍后再进行,等等......即使在某些环境中,我可能会部署一个服务器,它是一个长时间运行的服务器,所以我并没有真正获得好处,但我仍然认为这是一个好习惯。所以这是一个例子。

我使用 sync.Once 包与处理程序配合使用,这让我能够确保我只在第一次请求调用时进行设置,并且只进行一次,原子操作。所以即使你接收了多个请求,每个请求都有自己的 goroutine,可能你正在尝试对同一件事进行多次设置,但 sync.Once 可以避免这种情况。这只是一个例子。

Stevenson Jean-Pierre: 我完全同意。我认为无服务器在某种程度上只是一种代码的部署细节。即使对我来说,当我编写无服务器函数时,我不会尝试做任何特定于 Lambda 的事情,或者其他什么,直到最后,几乎是在我准备部署它的时候,因为即使是在本地,我也把我的 Go 文件当作只是一个二进制文件,我通过我的本地文件系统传递一个 JSON 文件来模拟传入的事件......

所以我会完成我的完整测试周期,我会在我的本地机器上做我需要做的一切,然后当我准备好部署它时,我会把那个读取文件的处理程序换成一个读取 AWS 事件的处理程序......但其余的工作流程是一样的。没有什么特定于无服务器的东西需要你在代码库中做来让它工作......我觉得也许人们对无服务器感到畏惧,因为他们听到这些术语,但他们并不真正理解它实际上意味着什么,但实际上没有什么不同,正如 Johnny 所说的,和标准的 Go 开发或任何开发没有区别。只是关于你如何获取事件并处理它。

Jaana Dogan: 我认为云提供商最终希望提供一种惯用的体验,因为当你设置了这些障碍---你需要学习新的组织技巧来推送到无服务器时,这实际上是违背了无服务器模型的。主要的想法是你应该关心你的业务逻辑,你应该能够使用你现有的工具,轻松部署,并轻松维护。

我个人的一个经验是,通常我认为组织上的技巧适用于无服务器,但也取决于---正如 Stevenson 所说的---无服务器是关于部署的......所以它确实改变了我组织模块的方式。如果我要一起部署它们,我会把它们打包在一起;在维护依赖项方面,我希望确保它们由同一个模块文件表示......这是我自己经历过的唯一区别。否则,我可以将其他一切应用于无服务器程序。

Mat Ryer: 另一个问题是全局状态。我们谈到这些东西应该是无状态的,而全局状态在 Go 中几乎所有情况下都值得避免。全局状态,对于那些不确定的人来说,基本上是包空间中的变量。所以如果你确实使用了它们---顺便说一下,在 Go 中我们可以看到很多这样的例子,标准库中也有很多例子,但权衡......它可能更简单,你只需要写一个主函数,并且有一些全局空间中的变量,对于非常小的程序或脚本来说---那种使用场景我可以理解为什么人们会使用它们......但它确实会影响测试;它会引入一些其他的错误,而这些错误可能很难发现和解决,这是另一个你可以扩展的点......

所以不仅仅是"不要使用本地磁盘,因为下一个实例可能无法访问同一个磁盘",还包括"不要使用相同的内存。不要使用全局内存。"不要假设一个实例会在任何时间段内拥有相同的内存。这类事情,实际上也是一种好习惯。

Stevenson Jean-Pierre: 是的,你绝对不应该这么假设,这可能涉及到一些高级话题,但一旦你充分理解了这些权衡和类似的事情,有一些用例可能因为你正在重用同一个容器的可能性......你可以为检查你是否在一个现有的容器中进行优化,如果你有长启动时间,或者启动时间会增加你的延迟......但这是一个更高级的话题,一旦你理解了你在处理什么以及这些实例如何生存或消亡,可能会来来去去。

Mat Ryer: 这是一个非常有趣的观点......我确实在想---这些优化通常涉及到项目中的某种复杂性,代码中的复杂性......

Stevenson Jean-Pierre: 当然。

Mat Ryer: ......它们可能在某个时候有意义,但随着时间的推移,它们可能会失去意义,等等。所以这也是一个非常有趣的事情,你必须时刻记住---不断检查你最终的架构,确保它仍然相关,等等。而且不要过早优化,这是另一个问题。

Stevenson Jean-Pierre: 你提到过的一些内容实际上让我遇到了反向的问题---你说"不要使用全局状态",类似的事情;但仅仅因为它是无服务器的并不意味着你有一个干净的启动环境。我有一个项目,我从 S3 拉取文件并处理它们,并且它们在清理后,因为它是无服务器的,它会自动清理掉容器......然后我大约运行了 30 多次后开始出现失败,是因为我填满了执行环境的磁盘,而没有考虑到"嘿,也许我们正在重用这个代码,如果它在热路径中加载并且一直使用同一执行环境。"所以清理仍然很重要,如果全局状态有问题,重置它也很重要,因为你可能会再次获得同一个容器,这可能会带来问题。

Mat Ryer: 这很有趣。我想那里也有安全隐患,如果你从一个客户那里拉取数据,然后你获得了同一个实例,而你没有考虑到这一点......这是一个非常好的观点;我很高兴你同意参加这次讨论,Steven。我想你可能救了我的命。[笑声]

Johnny Boursiquot: Stevenson,你提到了一些关于测试的有趣内容,以及你如何进行测试。也许我的这句话会有点争议,但就目前所有关于无服务器的测试工作方式而言,老实说,我觉得它还没到位。体验还是太多了。我没有信心在本地测试整个设置,这就是为什么我非常依赖单元测试,我非常依赖调用或模拟调用,使用正确的 JSON 负载......基本上,我尽量以我编写其他 Go 应用程序的方式来编写代码。

但也有一些话要说,关于进行某种集成级别的测试。在某个时候,你会开始说"你知道吗?让我假设我会收到来自我的本地开发环境以外的某个真实事件。"你什么时候跨过这个门槛,什么时候进行这种集成级别的测试?

Stevenson Jean-Pierre: 对我来说,即使是在你接收到来自某个源的事件的情况下,那些事件也是非常明确的,并且遵循某种模式,对吧?你可以测试你收到的不同数据的可变性,但就像我说的,我会在我的本地文件系统上使用 JSON 文件,并断言输出是我期望的,或者断言事件按照我预期的方式发生,但我非常来自那种黑客式的系统管理员背景,我写 Bash 脚本并且直接在脚本中进行测试,并确保期望的输出是真正证明代码有效的证据......

对于 Go 来说,我不会改变我的测试方式;因为我是在本地编写代码,而且我仍然可以像直接构建的二进制文件一样运行它,我会编写 _test 文件,并测试我通常会在函数级别测试的内容,但总体来说,它只是一个集成风格的测试,我只是断言我得到的输出是我预期的,然后我才会去部署并看看会发生什么。

Jaana Dogan: 我能问个问题吗?......在我们谈论测试之前。我们该如何开发无服务器应用程序?因为云是一个实现,而且现在开发栈变得如此令人沮丧地复杂......我发现很难在我的开发环境中保持类似的环境。我觉得无服务器只是增加了另一个巨大的负担,因为它太抽象了。你唯一能模拟它的方式就是在云提供商上运行它。所以在开发时你有什么策略吗?

Stevenson Jean-Pierre: 我不相信能模拟整个环境,因为正如你所说,它很复杂,是多变的,有太多东西在那里......所以我基本上是在测试账户中运行它,我会访问一个测试的 S3 Bucket,我会访问一个测试的 Dynamo 表,并进行完整的测试,因为这是唯一真正能测试你为之编写的代码路径的办法。你必须实际访问这些 API 才能发现某些问题,我认为这是在你为这些函数投入的精力中,正确的测试层次。你想让它们保持小巧,想让事情快速推进,而为此设置一个完整的模拟环境似乎有点过头了。

Mat Ryer: 单元测试更有用。你描述的那些测试,Steven,听起来有点像单元测试;如果无服务器是一个单元,那么你传入一些东西,确保你得到的是你期望的---这有点像单元测试。对我来说,这些是最有用的测试,因为如果出现了问题,它们会像激光一样指出问题出在哪里;理想情况下,如果有一个新错误,只有一个测试会失败......然后你就能直接找到它。

不过我知道 Monzo (译者注: "Monzo银行有限公司是英国的一家线上银行,亦是英国最初的以app为基础的银行之一,成立于2015年,最初名为Mondo")---它是一家银行---是用 Go 编写的;大家应该看看,我觉得他们做的事情真的很酷,而且不仅仅是因为它是用 Go 编写的......但我知道他们在生产环境中进行测试;有点像金丝雀测试,他们实际上在他们的生产环境中模拟真实的行为。这些测试不断运行,它们应该捕捉到指标,并检查系统的性能,类似的东西。

Jaana Dogan: 他们是在他们的测试环境中复制某些请求吗---有点像金丝雀测试,但在它真正成为金丝雀之前?

Mat Ryer: 不,我不知道具体细节,但我猜任何成熟的项目应该都会有相当多的测试策略......不过,在这个特定的例子中,我听到 Monzo 的工程师 Matt Heath 谈论过这个。他在网上有很多关于这个话题的演讲,讲得非常好。(译者注: Matt Heath的github )

他们实际上模拟了真实用户使用银行的卡片,互相转账,以及做所有这些人们真正会做的事情。他们可能确实有另一个环境,在把代码投入生产之前,会在那个环境中运行同一套测试......是的,这很可能是一个更广泛的测试策略的一部分。

Stevenson Jean-Pierre: 谈到在生产环境中测试,有一件事我发现非常关键,那就是获取正确的日志级别并从函数中提取足够的信息。你没有一个可以 SSH 进去的服务器,也没有传统环境中的那些调试工具......所以确保代码是可观察的,确保日志记录,确保你理解了代码选择的执行路径是非常重要的,这样才能快速调试并理解问题可能出在哪里。

Johnny Boursiquot: 顺着这条思路,我想谈两点。Mat 提到了一个问题,就是你有一个函数,它是一个单元,代表某些业务逻辑,但显然有些系统依赖于多个不同函数的多次调用。在你的环境中你需要引入某种编排机制,以确保事情按正确的顺序发生。任何非平凡的、涉及多个函数的系统,你只要需要调用超过两三个函数,那你就需要某种程度的编排。

目前的最佳实践是,你不应该让一个函数调用另一个函数。如果你有这种一连串的同步调用,第一个函数实际上是在等待所有其他函数完成,然后你就有可能面临超时的风险,导致请求被丢弃。所以在这方面有很多注意事项。

我接下来还想谈谈调试方面的问题,在无服务器、分布式环境中,比如服务应用程序,这需要很多额外的基础设施支持---日志、指标、跟踪等等。我们稍后再回到这个话题......但我想知道,当你需要协调多个函数来完成某项工作时,你是如何处理的?

Stevenson Jean-Pierre: 给 AWS 的 Step Functions 点个赞。我不确定 Google 在 GCP 中是否有类似的东西......但 Step Functions 的确为我打开了一个新世界,让我能够将无服务器的东西连接在一起,组成一个大的、连贯的单元。在 Step Functions 之前,情况就像你描述的那样,Johnny,你得通过队列或某种传统机制将函数一个个调用或传递......但有了 Step Functions,你可以让一个函数的输出成为下一个函数的输入,并以这种方式将它们连接起来。

尽管它们依然是解耦的,依然在传递数据,但你可以查看状态之间的转换,理解下一个函数得到了什么数据,最终你可以让两个完全不同的函数按照预期执行它们的任务。你可以独立地测试每个函数的输入输出,只要确保前一个函数的输出是你想要的,那么你就能在同步外部调用和内部异步解耦这两者之间取得平衡。

Mat Ryer: 很有意思,Chris James 在 Slack 频道中提到了环境成本,也就是无服务器和自建项目之间的绿色成本。这是我第一次考虑到这个问题。我假设这种共享资源的思路是,这些资源已经在那里了,随时可以使用......我们理论上只是占用了其中的一小部分,并为此支付了一点费用。所以从绿色角度看,这应该是可行的,但实际上我不确定。

Stevenson Jean-Pierre: 我也猜 AWS 可能正在利用他们的 Spot Fleet 来运行无服务器计算,或者利用那些当时未被使用的系统资源,这实际上是他们的一种优化手段,确保他们已经运行的服务得到了最大化利用。我怀疑他们会为无服务器函数专门启动新的服务器硬件。我想他们更多的是在利用现有的额外容量来运行这些微型虚拟机。

Mat Ryer: 也许一开始确实是这样,但我现在不太确定......这可能取决于 AWS 作为亚马逊业务的一部分有多大......也许他们现在确实在为销售而增加计算资源;我不知道。我好像把话题带偏了。

Stevenson Jean-Pierre: [笑] 是的,我也不确定。我得再查查......但因为我知道他们确实有 Spot 这样的服务,所以他们不利用这些闲置资源来启动微型虚拟机的可能性非常小。

Mat Ryer: 是的,这就是共享基础设施的其中一个承诺,至少是其中之一。

Johnny Boursiquot: 现在让我们回到另一个话题,调试无服务器应用程序。老实说,当我第一次开始做无服务器工作时,这是对我来说最大的启示之一......基本上就是,好的,在传统的单体应用中,就像 Stevenson 说的,如果真的需要的话,你可以 SSH 进入一台服务器,查看所有日志,所有与请求相关的东西都可以在那里找到。当然,在某些情况下,我认为你不应该让这台服务器重新回到集群中,但这不是重点。

但是,传统的可以在一个地方看到所有东西的模型---在无服务器的情况下,这种方式已经消失了。而且有人可能会说,即使在一个大规模的分布式系统中,当你有长时间运行的实例时,你也无法保证请求会始终到达同一个实例。所以在那种情况下,你也有同样的问题,但在无服务器模型中,你几乎没有选择。如果你有一个情况,你需要协调多个无服务器的组件---比如写入存储、从队列中消费、与第三方服务通信等等,如果某个地方出了问题,用户发出请求......

也许你有一个 API,它触发一个 Lambda 函数来处理这个请求,然后你又触发了其他 3-4 个不同的服务,最后返回给用户一个响应。如果某个地方出了问题,比如用户收到了 500 错误......你该从哪里开始?你如何在这种高度分布的环境中推理?这些东西都不在同一个地方,如何处理?

Jaana Dogan: 关于这个话题我有很多想法,但我可以说---我想我们通常会从网络层和分布式跟踪开始,找出问题来自哪个具体的服务......我觉得云提供商在这方面做得还不错,但并不完美。

我认为最大的问题是,作为用户,我不能看到整个端到端的跟踪。云提供商在这方面贡献了很多问题,因为有些跟踪来自存储,有些来自负载均衡器等等......你处于一种中间状态。有时是云提供商出了问题,而不是你。

我们有过这样的情况,客户打电话给我们说,"嘿,你们的服务出问题了,我们的 SLA 出现了问题......"我们不得不不断回去调试他们的服务......但我们发现,如果我们持续输出一个信号,至少是一个分布式跟踪信号,用来代表并引导用户,这将是最理想的。

然后,当你找到自己的服务时,你可以继续深入查看其他信号,比如日志等等。但我认为,作为一个行业,我们在诊断问题或至少引导用户或云提供商找到问题来源方面还处于初级阶段。

Stevenson Jean-Pierre: 我也发现,在使用 Lambda Functions 时,我通常会在 Go 代码中的错误处理块中尽可能详细地输出错误信息,尽量靠近错误源......但传统的关联日志记录和跟踪也很重要,尤其是当你有多个无服务器函数被绑在一起执行特定的工作流时,如果你在整个工作流中使用相同的关联 ID,至少你可以描绘出一个完整的画面,了解整个过程中发生了什么。

即使对于 HTTP 的请求/响应周期,比如 Lambda Functions 的情况,有时你会遇到一些失败的情况,无法向调用方返回响应,这时记录尽可能多的细节非常重要......因为调用方会收到一个 500 错误,但在底层某个地方出了问题,你可以通过日志记录找到原因。所以不要直接抛弃这个函数,记录详细的信息让你能够回溯并找出导致问题的原因。

Jaana Dogan: 一个有趣的事情是,你必须在函数中已有某种程度的监控和记录机制,但人们很难确定该监控什么。我觉得我们在事后调试方面投入的工作还不够,比如在某个实例上设置断点,获取快照,查看一些变量的值等等......

但我觉得我们现在还处于初级阶段......没有好的方法去定位问题,也没有简单的方法去关联其他信号。这也是一个组织性的问题。我在 Google 的监控团队工作时,我们不得不与 50 多个不同的产品团队合作,每个团队对监控的看法都不一样......在这方面也没有太多标准,这真的不好......因为 Google 有自己的做法,然后你去另一个云提供商那里,AWS 又有他们自己的做法......我们无法参与对方的跟踪,也无法关联,从用户的角度来看,这真的很糟糕。

Stevenson Jean-Pierre: 即使没有标准化的做法,你是否认为像关联日志记录这样的常见做法,至少是最基础的事情,对吧?最终,你会得到某种日志,无论是 CloudWatch Logs,还是其他什么日志服务,用户总是能得到一些最基础的东西,哪怕他们必须自己动手实现一个解决方案来追踪和理解执行过程中的问题。

Jaana Dogan: 是的,当我说标准时,我主要指的是跟踪方面的标准......但这目前还没有实现。也许再过几年,我们就能理解彼此的跟踪 ID。这非常基础,因为你用跟踪 ID 来关联一切。至少我们是这么做的。

Stevenson Jean-Pierre: 是的,如果能够标准化,那将会非常好。

Jaana Dogan: 是的,分布式跟踪是一个有机发展起来的工具......一开始各个提供商之间没有什么讨论,后来大家才意识到,如果没有标准,分布式跟踪实际上是无效的,因为我们在互相竞争,无法去要求基础设施团队或云提供商实现这种传播格式。

缺乏共识实际上阻碍了分布式跟踪成为主流工具......所以两年前几乎所有人都聚在一起草拟了一个提案,这个提案现在已经越来越成熟了,它将成为 W3C 下的一个标准。届时将会有一个所有人都认可的一级报头,这将非常棒,因为你可以去 MySQL 上说"嘿,遵循这个报头"或者类似的事情......你可以去任何基础设施工具要求他们做点什么......至少传递这个 ID,不要让跟踪断裂。

Stevenson Jean-Pierre: 听起来非常棒。有没有什么地方可以了解更多相关信息?这个提案现在有在流通吗?

Jaana Dogan: 是的,有一个仓库......我可以分享一下。

Johnny Boursiquot: 你说的是否是 OpenTelemetry 的相关内容?

Jaana Dogan: 我也可以谈谈那个项目。但这是另一个项目。这是一个线上的报头标准项目。它在 github.com/w3c/trace-context 上。你可以阅读提案,已经有了一些讨论和针对某些语言的实现。这将在几年内成为标准输入。

Stevenson Jean-Pierre: 非常酷。

Johnny Boursiquot: 这将是个大突破。

Jaana Dogan: 几乎所有的分布式跟踪供应商,包括 AWS 和 Google 等云提供商,都在为此贡献力量。

Johnny Boursiquot: 这真是个大话题,太棒了。我们一直在讨论技术上的优劣势、一些挑战以及需要注意的事项......我感觉我们更多是在讲述一些警示性的故事。而我们这里的每个人都同意,"无服务器"---我用这个词是因为我想不到更好的词,这是个营销词汇,但我找不到一个更好的词来涵盖无服务器的所有内容......我们都知道,无服务器总体上是件好事。它提供了更多选项,更多的方式将正确的抽象引入你的基础设施,进入你的工作环境,解决你面临的业务问题......但从机会的角度来看,对于 Go 开发者来说,有什么吸引力呢?为什么我要投入时间去学习如何做无服务器?

也许你所在的公司只允许你使用 GCP,或者只能用 AWS,那为什么还要花时间学习任何一个供应商的技术?或者即便你想跨供应商使用无服务器技术,为什么还要投入时间和精力去学习如何正确地使用无服务器?因为这不仅仅是关于语法,不只是关于代码;它需要一种不同的思维方式。你需要学习更多关于构建分布式系统的知识。作为一个 Go 开发者,我为什么要投入时间去做这件事?

Stevenson Jean-Pierre: 我认为很多现代开发者现在做的更多是粘合工作和整合工作,而不只是纯粹的开发工作......因为传统上,有些系统是你必须自己编写的,但现在它们已经被抽象出来了,由供应商提供,所以现在你更多是在做整合工作,做粘合工作。

而只是为了做这些粘合工作而运行基础设施是有点让人沮丧的,因为你还得维护这些东西......但我发现,这就是无服务器对我来说的甜蜜点---能够编写这些集成代码,编写这些粘合层代码,同时让基础设施这一部分也被抽象掉,这样系统就像一个纯粹的供应商解决方案在运作,而不需要你去运行自己的底层硬件或实例。我认为这就是它值得去学习的地方。如果你回顾一下自己的工作量,你会发现你写了很多粘合层代码、集成层代码和粘合层代码。所以我认为这是一个很好的理由去学习它。

此外,我认为它帮助你练习无状态编程,确保你在构建这些分布式应用程序时,不必深入到构建分布式系统的细节中去。因此,这也是一个很好的切入点,帮助你了解这些系统是如何协同工作的,以实现一个共同的目标。

Mat Ryer: 对,我同意这个观点。对我来说,如果我必须编写那些粘合代码,我不确定我是不是在用正确的方式编写。这是一种额外的学科,或者什么的;我可能会犯一些愚蠢的错误,之后会浪费我很多时间或者带来其他问题。

我觉得无服务器对于开发者来说是一种赋能,让你可以专注于让你工作与众不同的部分,并把那些基础的工作交给别人去做。这也是我喜欢它的原因。它让我觉得自己有能力,不需要仅仅为了做那些与我实际工作无关的事情去寻求帮助。

Jaana Dogan: 对我来说,更多的是关于生产力。无服务器的环境是有限的,但如果它能满足我的需求......那我为什么还要关心那些底层的基础设施呢?我只需要发布代码,然后按需付费。

我认为这才是云计算最初的问题所在。所以我会从这里开始,如果我需要更少的限制,我总是可以回到更低的层次。对我来说,一个好的起点是有一个更具意见性的、也许限制更多的环境,然后把部分工作交给我的云供应商,再根据需要回到更低的层次。

Johnny Boursiquot: 同意。让我看看频道里......他在 GoTime Slack 频道中提到,学习如何做无服务器实际上也是学习如何更好地构建非无服务器系统的好方法。我完全同意这一点,因为自从我开始做无服务器工作以来,我开始关注一些以前没有关注的问题。比如"如果我可以做到无状态,那么我就不必使用粘性会话,我不必......"有很多不同的事情---很多我在单体应用时代(部署模型)理所当然的问题,现在我更多地去关注了。我在有意识地做出一些决定,是否要有这个,还是不要那个......

再次提到微服务---你可以称它为无服务器的纳米服务;我们没有陷入那些流行词的泥潭......这确实影响了你如何构建这些后端系统。对我来说,这就是最大的收获---你学习如何解决这些问题,而不必过于担心哪个供应商来解决业务问题,而是让部署模型,无论你使用哪个供应商,让它成为......也许不是最后一个要关注的问题,但也不要让某个供应商提供的框架限制你构建这些东西。你开发你的产品,然后你再去考虑"好吧,如何在这些其他环境中部署它?"

在我们结束之前,还有什么要补充的吗?我觉得我们已经深入讨论了一些领域,也在其他方面提供了一些启发......

Stevenson Jean-Pierre: 我还想再提一个无服务器的一般性问题......无服务器的巨大并行性带来的问题也可能是你会遇到的。例如,Lambda 可以同时扩展到 1000 个执行实例,而你的后端数据库则可能因为要处理这些请求而不堪重负,你会把连接用到上限......

Johnny Boursiquot: [笑]

Stevenson Jean-Pierre: 所以要小心......传统上,你会有连接池之类的东西来限制这些连接,但现在因为你处在这种多并行执行的环境中,你可能会突然有 1000 个连接冲击你的数据库。

所以一定要理解这些并发模型,并确保在请求你的资源和环境时考虑到这些问题,否则它们会反过来咬你一口。

Johnny Boursiquot: 太棒了,太棒了。这是一个非常启发性的节目。我学到了一些东西,而我已经做了很久的无服务器开发了,所以我希望这对你,听众,也是有帮助的。非常感谢 Jaana 回来......我们很想念你,Jaana,很高兴你能回来参加节目。

Mat,Mat Ryer 先生---我有点模仿了你的口音;不知道你有没有注意到,但我在尽量让自己听起来像你一样酷......

Mat Ryer: 其实这也是唯一我能听懂你说的部分。

Johnny Boursiquot: [笑] 很好......很好。还有非常感谢我们今天的特别嘉宾,Stevenson Jean-Pierre,我的同胞, 海地出生的朋友。

Stevenson Jean-Pierre: 谢谢你们邀请我。

Johnny Boursiquot: 好的,这次真是很棒的节目。对于那些正在现场直播的听众,希望这对你们也是有帮助的。我们很喜欢在 Slack 频道里的互动,继续保持。

如果你们有节目建议,也可以在 GoTimeFM 的 Slack 频道上推荐一些节目,我们会尽力去做。也要感谢幕后工作人员......你们没听到他太多声音,但 Jon Calhoun 一直在做一些技术工作,确保这个播客能正确录制,每个人的声音都很清晰......所以感谢 Jon。

Mat Ryer: 顺便说一下,我们没听到他的原因是因为他说"我不知道任何关于无服务器的东西。我只用 DigitalOcean 和 Google Cloud 平台。"其实他真的懂,只是他没意识到。

Jaana Dogan: 这就是重点。 [笑声]

Johnny Boursiquot: 太棒了,太棒了。非常感谢大家的收听,我们下期 GoTime 再见。

相关推荐
hummhumm8 小时前
第 22 章 - Go语言 测试与基准测试
java·大数据·开发语言·前端·python·golang·log4j
运维&陈同学9 小时前
【zookeeper03】消息队列与微服务之zookeeper集群部署
linux·微服务·zookeeper·云原生·消息队列·云计算·java-zookeeper
hummhumm9 小时前
第 28 章 - Go语言 Web 开发入门
java·开发语言·前端·python·sql·golang·前端框架
YMWM_10 小时前
第一章 Go语言简介
开发语言·后端·golang
hummhumm10 小时前
第 25 章 - Golang 项目结构
java·开发语言·前端·后端·python·elasticsearch·golang
好奇的菜鸟11 小时前
Go语言中的引用类型:指针与传递机制
开发语言·后端·golang
Alive~o.011 小时前
Go语言进阶&依赖管理
开发语言·后端·golang
Code_Artist11 小时前
使用Portainer来管理并编排Docker容器
docker·云原生·容器
ifanatic16 小时前
[面试]-golang基础面试题总结
面试·职场和发展·golang
懒是一种态度16 小时前
Golang 调用 mongodb 的函数
数据库·mongodb·golang