RustConf 2023 - Async building blocks: A streaming Data Drama in Three Acts

欢迎来到异步构建模块,这是一个分为三幕的流式数据戏剧。或者正如Nell在上一次演讲中所称的那样,是一场异步戏剧或类似的东西。所以,请原谅我。

简单介绍一下我自己。我从2018年开始使用Nix。我也是Nix文档团队的成员,在那里我领导一个工作组。如果你看过Nix文档,我很抱歉。我还是一家名为Flox的公司的工程师,该公司正在Nix之上构建工具,这样你就不必真正了解Nix就能从中受益。但是,关于我的介绍就到此为止。今天,我只是你们谦卑的讲述者。这个故事是关于一个名叫Mary的消息。

正如我所说,这是关于异步构建模块,而不是异步材料科学,所以我们不会深入探讨任何特定的主题。有一个与此相关的示例应用程序在GitHub上,链接将在最后提供。

让我们从设置舞台开始。这就是我们的女主角Mary这个消息所生活的世界。我们有一组气象站,每个站都向一个名为pubsub的消息队列系统发布测量结果。气象站将消息发布到一个名为weather messages的主题中。这是一个队列,到达的消息按到达顺序存储在这个队列中。然后我们的另一个应用程序从这个主题(即队列)中读取数据,处理这些数据,然后将天气预报发布到一个weather predictions主题中。我刚才意识到你们可能看不清这些方框,但那边看起来不错。在这个屏幕上有点奇怪,我只是想确保每个人都能看到我在说什么。

为了做出这些预测,应用程序需要整个世界的快照,即来自每个站点的一条消息,所有这些消息都是在大约相同的时间生成的。获取这组来自大约相同时间的消息将是相当棘手的。在我们讨论原因之前,让我们先来认识我们的主角。

很多很酷的软件演讲都有艺术,通常是手绘的,这需要我没有的精细运动技能。所以,既然AI正在接管世界,我想我不如用Midjourney生成一些艺术作品。

这是我要求的提示,在我的脑海中,我想象的是一个带有脸的信封。但我得到的却是这个。完美!这改变了我在写这个演讲时的语气。它有点让我想起《圣诞节前的噩梦》。

所以,Mary刚刚被创建,带着一些天气数据,被指示去见Pablo pubsub,我们消息队列系统的管理员。让我们讨论一下Mary在前往见Pablo的路上将遇到的一些问题。

第一个问题是发布同步。假设我们的气象站都以相同的速率生成消息,比如每100毫秒一条消息。在理想世界中,它们都会同步发布,所以每个站点的第一条消息都会在同一时间发布。但在现实中,这实际上是不可能的,即使它们尽最大努力这样做。

所以你得到的实际上是这样的情况:如你所见,每个站点仍然以相同的速率发布消息,但它们彼此之间都有偏移。这样做的一个后果是,”同一时间”从一个瞬间(或者我们尽可能接近的瞬间)变成了一个窗口。

另一个后果是,不清楚这个窗口应该在哪里。它应该在这里还是在那里?你看,在站点3中,消息1和2都落在了窗口边界上。所以,这两条消息中哪一条应该被计入?这就由你来决定了。

你遇到的下一个问题是,在气象站和pubsub之间存在这个讨厌的东西叫做互联网。有些消息会从气象站顺利地到达消息队列系统,没有任何并发症。其他消息可能会丢失或需要重新传输,你可能会得到重复的消息,或者完全缺失一条消息。还有一些消息可能会绕道而行,由于网络拥堵而晚于预期到达。

所有这些问题结合起来,就形成了消息排序的问题。理想的消息顺序可能是这样的:你有来自站点1、2和3的消息1,然后是来自站点1、2和3的消息2。但是由于所有这些问题,你实际得到的是这样的情况:所有消息都混在一起,你可能有重复的消息。这意味着,如果你只想要下一组消息,你不能只是从队列中读取下一组消息,因为正如我所说,它们可能都混在一起。

所有这些问题组合起来,构成了我们今天要探讨的问题空间。但首先,让我们介绍我们的下一个角色,Pablo pubsub。

受到我之前失败的启发,这次我试图更具体一些。我甚至要求它看起来可爱。在我的脑海中,我想象的是一堆气动管,就像你可能在药店或银行的免下车窗口看到的那种(如果你年纪大,还去实体银行的话)。但我得到的却是这个。

它确实更可爱了,但仍然是那种可爱中带着恐怖。所以我又尝试了几次,最后我放弃了,就说这就是Pablo pubsub了。你可以看到,它在某种程度上给了我我要求的东西。他那可怕的脸是由气动管组成的。他有一些非常狰狞的牙齿,然后他的眼睛也不知道指向哪个方向。

Mary穿越互联网到达消息队列系统,来到pubsub,寻找Pablo。Pablo说:”嘿,我能为你做什么?”Mary说:”我有一些天气数据要传递,我应该去哪里?”Pablo说:”啊,你在找天气应用程序。我会指引你正确的方向。如果你在途中迷路了,我就再发送一份副本。”

Mary看起来有点困惑,说:”你说如果我迷路了是什么意思?等等,你说你会发送另一份副本是什么意思?”

Pablo说:”你知道,在互联网的恶劣环境中,各种事情都可能发生。消息经常丢失或延迟,所以我们保留副本,以防出现问题时需要发送另一份。”

这时,Pablo用他摇摆的气动管手臂模糊地指向一堆克隆舱,Mary随即陷入了存在主义危机。”我怎么知道我是真正的我?我是原始的我吗?外面是不是已经有更多的我了?”

Pablo看到这种螺旋式下降正在实时发生,说:”好了,该走了”,然后把她推出门外。

这就把我们带到了第二幕,即应用程序。让我们开始深入探讨我们如何处理这个问题。这里是我们系统各个组件的高层概述。

我们有这些混在一起的incoming messages,第一个组件叫做demuxer(解复用器)。demuxer的工作是把这些混在一起的incoming messages组织成一种对系统其余部分来说更方便处理的格式。

下一个组件是grouper(分组器),它会把这种更有组织的格式转化为一组来自”同一时间”的消息(在这个上下文中,”同一时间”的含义可能会有所不同)。

一旦你有了这些分组,它们就会通过processor(处理器),这个处理器会进行预测部分,就像我们之前提到的。然后publisher(发布者)会接收这些预测,并将它们发送回pubsub。

让我们讨论一下我们如何模型化我们的incoming messages。在同步世界中,你用iterator trait来模型化一系列项目。这有各种很好的组合器,比如take、skip、while等等。在异步世界中,你用stream来模型化这个。我对stream的心理模型基本上就是一个返回Futures的iterator。

实际上,stream trait是这个的低级接口,所以在实践中你自己不会经常使用它。我提到的那些对iterator trait很好的组合器实际上在一个单独的trait中,叫做stream_ext或stream extension,那里有take、skip、while等所有东西。

然后还有一个叫做async_stream的crate,它提供了几个宏,允许你写命令式代码来创建你自己的stream。它基本上提供了一个yield关键字,你给它一个表达式,这个表达式就会成为你的Stream的下一个项目。

关于消息排序,正如我之前提到的,我们有这个问题:消息会乱序,消息会晚到,而我只想要下一组消息。解决方案是什么?我们要缓冲这些消息,这就引出了Beatrix buffer。

Mary到达天气应用程序,看到一个女人把其他消息整齐地排成队列。她开始走近,但在她能这么做之前,有人对她吹哨子并跑过来。我们稍后会谈到那个角色。

对于Midjourney,我给出了一个看似无害的提示。我想要一个拿着剪贴板和挂绳的女人,用stanions(那种看起来像安全带材料的东西,在机场里让你排队的东西)把人排成队。这就是我得到的结果,这个例子中的恐怖并不那么明显。

我要求的是stanions,就像我说的,那是一种看起来像安全带的材料。但我得到的却是一条链子,而且不是普通的链子,而是一条不与任何特定物体相连的链子,漂浮在空中,除了可能连接到这个男人的皮带上,还有不幸的是,这个男人的裆部。

关于缓冲器,我们基本上要把它们建模成一个hashmap。我们将沿着两个轴对消息进行排序,一个是消息来自的站点,另一个是消息创建时间。站点1在最左边,站点n在最右边,较早的消息在顶部,较晚的消息在底部。

我们hashmap的键将是站点编号,值是那个特定站点的实际缓冲器。我们在这里使用VecDeque而不是Vector,因为消息可能会晚到,需要插入到队列的开头,在Vector中这样做会导致其他所有东西都被移动,这不是很有效率。VecDeque允许你更有效地做到这一点。

这就引出了Dino demuxer。你可以看到,他有点像体育教练的角色。你不需要再看提示了,只需知道我已经尽力了。我要指出的是,这些孩子看起来像是在恳求而不是欢呼。还有一只游离的手臂,然后这个孩子,显然他的下半身完全由篮球和棒球组成。

Dino的工作是把新到达的消息带到那个站点正确的缓冲器中,然后Beatrix的工作是把那条消息按正确的顺序排在队列中。

正如我提到的,Mary刚刚走进门。Dino对她吹哨子并跑过来,他说:”快,你是从哪个站点来的?”Mary有点惊讶,说:”呃,我不知道,站点1?”Dino说:”到那边去,快快快!”因为他是个体育教练,他说话有点粗鲁。

所以他急忙把Mary带到正确的队伍,同时他对着对讲机说话。当然,当他把她送过去时,他让她通过一个障碍course,因为他是个体育教练,这能锻炼品格。

这个消息插入过程,从视觉上看,是这样的:Dino有一条新消息,他要把它带到Beatrix那里,也就是站点1的缓冲器。你可以看到,我们在那个缓冲器中有空间,所以她会把它插入到队列中正确的位置。

Beatrix做的另一件事,我们还没有真正讨论过,是她也会丢弃任何重复的消息,如果它已经在缓冲器中的话。

我们要提前解决一个我们还没有遇到的问题,那就是协调缓冲器访问。Dino和Beatrix负责把消息放入缓冲器,然后过一会儿我们会遇到一个叫Greta grouper的角色,她会从缓冲器中取出消息。所以我们这里有一个经典的竞态条件,一个组件在往缓冲器里放东西,另一个组件试图从里面取出东西。

我们是Rust开发者,我们有原则和编译器强制的道德优越感,所以我们不会在这里raw dogging可变指针,或者让宇宙的量子波动来决定赢家和输家。我们要使用互斥锁(mutex

好的,我会继续整理和翻译剩余的内容:

正如你所看到的,Mitch mutex是这个裁判类型的角色,Midjourney很有帮助地把他制服上的条纹应用到了他的脸上。为什么不呢?AI就是未来。

在应用程序启动时,Mitch mutex会拿一个对讲机,上面贴着写有字母”ARC”的胶带,他会把这些对讲机分别交给Dino和Greta grouper。任何想要访问缓冲器的人都需要先通过对讲机与他确认。这就是Dino之前在做的事情。

你可以想象,在某个时候,可能会有大量消息涌入,Dino会开始疯狂地对着对讲机说:”我有一条新消息,我有一条新消息,我有一条新消息。”这时,Mitch会吹哨子说:”把对讲机让给别人,任务已阻塞,直到另行通知。现在不是你的回合,Dino,冷静点。”

我们的消息,或者说我们的缓冲器,在这个Arc mutex组合的背后。这里有一点需要指出,标准库和Tokio中都有互斥锁,所以问题是你应该使用哪一个?在这种情况下,有一些神奇的词,如果你知道的话,就和我一起说:”这要看情况。”

所以,如果你知道这些词,恭喜你,你是一名高级工程师。享受你的加薪吧。

让我们看看我们系统的更详细的图片。这就是我们刚刚讨论的组件。我们有输入流进入demuxer,然后demuxer和我们稍后会讨论的grouper都通过Arc引用缓冲器,缓冲器由这个互斥锁保护。

这就引出了我们系统的下一个方面,即并行执行。我们希望能够独立运行系统的不同组件。Future可以在单个线程上并发执行,这就是重点。但有时你仍然希望它们同时执行。所以如果你有一个多线程运行时,比如Tokio,你可以将这些Future调度为任务,它会在不同的线程上同时运行它们。

这在机械上的工作方式是,对于我们的每个组件,我们会创建一个future,然后把这个future交给Tokio的spawn函数,它会把它变成一个任务,然后这个任务就飘进了我们运行时的以太中,去做运行时做的任何事情。你会得到一个这个任务的句柄,然后你可以等待它,或取消它,或做其他任何事情。

一旦我们为系统的所有组件都这样做了,我们就使用futures::select!宏同时等待它们全部。就像我说的,我们不会在这里深入细节,在最后会提供一个示例应用程序,如果你想看到更多细节的话。

我们终于可以讨论分组消息的问题了。正如我所说,预测算法需要整个世界的快照,所以我们需要大约同一时间的消息。这里实际上有很多棘手的细节,比如,你要等待晚到的消息多长时间?如果你只是想要每一条消息,你就无限期地等待。如果你有延迟要求,显然你不能这样做。

我们还有这个问题,就像我之前讨论的,什么是”同一时间”?你有一个窗口,消息可能落入这个窭口,你如何处理这种情况?我上一份工作的很大一部分就是处理这个确切的问题。从经验来看,我可以告诉你,仅仅这个问题就可以做一整场演讲。

为了这次演讲,我们就简单地说,每次我们想要一组消息时,我们就等待一段时间,一旦这个计时器结束,我们就抓取缓冲器中最旧的消息。让我们直观地讨论一下这是什么样子,这样你就有个概念了。

这就是我们的缓冲器的样子。我们要做的第一件事是等待。当我们等待的时候,Dino和Beatrix正在努力工作,往缓冲器里塞东西。我们会继续等待,在某个时候计时器会结束,这时我们就可以做我们的工作了,就是从缓冲器中取出那些最旧的消息来形成一个组。

对于Midjourney,我要求的是(你待会儿就会明白为什么)一个女人用弹射器把人弹射到墙上的门户中。我得到的是一个红发版本的《午夜凶铃》中的女孩,坐在一个悬空的弹射器上,就好像那是一个秋千架。当然了。

Mary已经排队一段时间了。最初,她被插入到队列的某个地方,她等了一会儿,最终她成为了她所来自的站点的队列中的第一个人。Greta走近等待的消息组,试图从每个队列中拉出第一个人,要求他们站在地板上用胶带标出的某个位置。

在某个时候,她要求他们在那里站一会儿。然后她退后,走到墙边,按下一个没人看见的按钮,因为她有特殊的知识。这时,一只机器人手臂从天花板上降下来,用塑料包裹把消息包成一个漂亮的小包裹,地板翻起来,把消息弹射到墙上的一个蓝色门户中。

太棒了。让我们讨论一下这个门户是怎么回事。你如何从任务中获取数据?当你调用一个函数时,你会得到一个返回值,这很简单。一个任务,就像我说的,你用Tokio的spawn创建它,然后它就飘进了你运行时的以太中。所以任务本身有一个返回值,但只有在任务完成后你才能得到它。如果我们希望我们的系统永远运行并持续获取数据,这显然不行。

那么,你如何从任务中获取数据呢?一个选择是写入外部数据结构,这就是我们在demuxer中做的事情。它在运行,当它运行时,它不断地把数据存储在那些缓冲器中。另一个选择是使用channel。

在幕后,它做的是同样的事情,但实际上,你可以把channel看作是一个计算机虫洞,当你创建一个channel时,你得到两端。一端是你把东西塞进去的地方,一旦塞进去,它就消失了,你不再需要关心它。另一端是数据就落在你腿上的地方,现在你有了数据,你可以用它做一些事情,你不需要关心它来自哪里。

这不仅对从任务中或任务之间获取数据很好,从架构的角度来看也很好,因为它允许你解耦系统中哪些组件必须关心什么。

回到我们系统的细节视图,我们刚刚讨论了这部分。grouper使用arc来引用缓冲器并从中获取数据,制作一个组,然后通过channel将数据发送到下一个组件。

这就把我们带到了第三幕:变形。Mary和她的消息同伴即将经历一些奇怪的事情。我们即将讨论的系统组件就在这里,处理器。它会接受两个channel,一个用来读取消息组,然后它会做它的工作,然后把结果预测放到下一个channel上。

这就是这里发生的事情。你拿一组消息,通过一些算法运行它(我不会深入算法的细节,因为像所有算法一样,它是魔法),你得到一个预测,然后把它放到另一个channel上。

这就引出了处理器。这个就出来得很棒,不恐怖。让我们都为Midjourney鼓掌,给我们一个小小的调色板清洁剂。做得好,Midjourney。

塑料包裹的消息组毫不客气地从门户的另一端弹出,落在被称为处理器的实体脚下。它坐在一个由技术制成的王座上,膝上放着一本书,书脊上用金色字体刻着”算法”。

处理器向消息组问好说:”你好,我确信你们到这里的旅程一定很奇怪和危险。然而,没有什么能让你们为我即将给你们的选择做好准备。如果你们同意,你们将上升到一个更高的存在状态,一个预测。你们将不再单独存在,但应用程序将实现其目的,未来的天气将被知晓。你们怎么说?”

Mary看起来有点困惑,说:”你知道,我们就不能只是告诉你我们的数据说了什么吗?我们真的需要做所有这些吗?”

处理器严厉地看着她说:”当然可以,但那就不那么酷了。”

Mary看着其他消息说:”你知道,他说得有道理,那确实不那么酷。”所以他们都点头表示同意。这时,处理器按下他王座上的一个按钮,一束光从天花板上倾泻而下,照在消息上,它们融合成一个单一的发光球体,被称为预测。房间另一边的一扇门打开,显示出另一个门户,发光球体飘过门户。

对于Midjourney,这是提示:一个发光的球体,上面有几张脸。这只能进展得很好。

它并没有进展得很好。对于那些不知道的人,Midjourney会为你给出的每个提示提供四个例子,在这种情况下,四个中有三个似乎是某种形式的南瓜灯,然后左上角的那个只是一个大块头的男孩。它们都同样可怕,选择你最喜欢的,让我们继续。

这就把我们带到了最后一个组件,Padma publisher。浮动的光球从门里出来,发现自己在一个装卸码头。有一个女人微笑着拿着一个棕色纸袋。光球飘到她身边,Padma自我介绍说:”你好,我是Padma。哎呀,你真是一个壮观的景象。”

预测,因为变成集体意识的过程中失去了幽默感,说:”我不知道,这里没有镜子。”

Padma说:”好吧,你现在要回到Pablo那里了,为什么不带上这个呢?”Padma打开棕色纸袋,预测飘过来往里面看,看到一个花生酱果酱三明治和一些橙子片。

“谢谢,Padma,”预测说,”我们已经上升到一个更高的存在状态,不再需要食物来维持我们的物理形态。”

Padma回答说:”我知道,这只是让我感觉有用。好了,你该走了。”

于是,预测飘进Padma的卡车后面,它们驶入互联网的虚空。

这个组件相对简单,因为通常如果你与某种消息队列系统交互,会有某种库为你处理IO,无论是从消息队列读取还是写入消息队列。在这里,如果你使用futures channels,你实际上可以把一个channel变成另一个stream。所以这里没有太多可以展示的。

关于Midjourney,我要指出两件事。一是挡风玻璃不知何故在Padma身后,我们不要想太多。二是我们有确凿的证据证明Padma是个巫师,因为她的左手没有连接到任何特定的东西上,这意味着她知道法师之手咒语,这是给所有我的D&D朋友们的。

已经上升到更高存在状态的预测现在可以看到现实的结构,可以看到所有任务并行执行。它们漂浮穿过互联网,不受网络拥堵、配置错误的服务器和恶意BGP劫持事件的影响。

预测回到消息处理设施,回到Pablo那里。它说:”Pablo,我们已经穿越了互联网并存活下来。我们已经准备好迎接接下来的事情了。”

Pablo说:”嗯,US East 1现在暂时down了,所以我猜你得和我在一起待一会儿。”

所以,一张由嵌在墙上的气动管组成的脸和一个寄居在浮动光球中的集体意识,真是一段注定要成为传奇的友谊。但是朋友们,那是另一个故事了,我们这个故

好的,我将继续整理和翻译剩余的内容:

但是朋友们,那是另一个故事了,我们这个故事到此结束。

我感谢你们的关注,希望你们玩得开心。这里有几个地方你们可以找到我,就像我说的,代码在GitHub上可以找到。我不确定它现在是否公开,但我会在演讲后公开它。幻灯片也将在GitHub上提供。所以,谢谢你们的关注。

这就是整个演讲的内容。总的来说,这次演讲介绍了一个异步流式数据处理系统的设计和实现,使用了一种生动有趣的叙事方式,将系统的各个组件拟人化,并通过一个名叫Mary的消息的”旅程”来解释系统的工作原理。

演讲涵盖了以下主要内容:

  1. 系统概览:气象站发布消息到pubsub系统,应用程序处理这些消息并发布天气预报。

  2. 消息同步和排序问题:由于网络延迟和其他因素,消息可能会乱序到达或丢失。

  3. 系统组件:

    • Demuxer:将混合的消息流组织成更方便处理的格式
    • Grouper:形成来自”同一时间”的消息组
    • Processor:处理消息组并生成预测
    • Publisher:发布预测结果
  4. 并发处理:使用Rust的异步特性和Tokio运行时来并行执行各个组件。

  5. 数据流:使用缓冲器和通道(channels)在组件之间传递数据。

  6. 挑战和解决方案:如何处理消息排序、分组和并发访问等问题。

演讲者使用了生动的比喻和AI生成的图像来使得技术概念更加易于理解和有趣。整个演讲充满了幽默感,同时也传达了复杂的技术内容。

最后,演讲者提到相关的代码和幻灯片将在GitHub上提供,供听众进一步学习和探索。

文章目录