分布式系统设计:批处理模式之事件驱动的批处理
本文首发于知乎专栏:进击的云计算本书翻译于:Designing Distributed Systems望小伙伴们多多支持原版。在前面一篇文章中,我们看到了一个通用的作业处理框架,以及一些简单的作业队列处理的程序。作业队列非常适合将一个输入转化为一个输出,但是,有许多批处理应用程序需要执行多个操作,或者需要将单个数据输入生成为多种不同的输出。在这种情况下,我们开始将作业队列连接在一起...
本文首发于知乎专栏:进击的云计算
本书翻译于:Designing Distributed Systems
望小伙伴们多多支持原版。
在前面一篇文章中,我们看到了一个通用的作业处理框架,以及一些简单的作业队列处理的程序。作业队列非常适合将一个输入转化为一个输出,但是,有许多批处理应用程序需要执行多个操作,或者需要将单个数据输入生成为多种不同的输出。在这种情况下,我们开始将作业队列连接在一起,以便使一个作业队列的输出成为一个或多个其他作业队列的输入,以此类推,这样就形成了一系列事件响应的处理步骤。
这种事件驱动的处理系统通常被称作为作业流系统,因为有一个作业流通过一个定向的非循环图来描述这些作业流的各个阶段和之间的关系,如图1所示。
这种类型的系统最直接的应用就是将一个队列的输出连接到下一个队列的输入,但是随着系统变得越来越复杂,出现了一系列不同的模式将作业队列连接在一起,理解和设计这些模式对于理解系统是如何工作的非常重要。这种事件驱动的批处理的操作与事件驱动的FaaS很相似,因此,在不了解这些队列是如何关联在一起的情况下,是很难完全理解系统的运行方式的。
图1 该作业流将复制的工作分为多个队列(阶段2a、2b)来并行处理这些队列,并最后将结果汇总成单个队列(阶段3)
事件驱动处理的模式
除了前一篇文章介绍的简单作业队列之外,还有许多可以将作业队列连接在一起的模式。其中最简单的模式就是,单个队列的输出作为第二个队列的输入,在这里我们不会介绍它,我们将介绍涉及协调多个不同的队列,或者修改一个或多个作业队列输出的模式。
Copier
协调多个作业队列的第一种模式是Copier模式。Copier的任务是将单个作业项流复制到多个相同的流当中,当对同一个作业项有不同的工作要做时,这种模式很有用。例如视频的渲染,当渲染视频时,根据视频的显示位置可以使用多种不同的格式,可能有4KB的高分辨率格式用于硬件播放、1080像素的数据流渲染、用于网速较低的移动用户的低分辨率,以及用于显示部分电影情节的GIF动画略缩图等,所有这些作业项对于每个渲染都可以进行独立建模,但是每个作业项的输入都是相同的,图2显示了用于转码的Copier模式。
Filter
Filter是事件驱动处理的第二种模式。Filter的作用是,通过对作业流进行过滤,筛选出不符合特定标准的作业项,减少作业项流的数目。例如,设置一个用来处理新用户注册服务的批处理作业流,有些用户会勾选复选框,表示他们希望商家通过电子邮件与他们联系以获取促销的信息或者其他的信息,在这样一个作业流中,可以将新注册用户的集合过滤为仅显示选择进行联系的用户。
理想情况下,可以组成一个过滤作业流的源来当作一个大使,用来包装现有的作业队列源,原始的源容器提供了要处理的作业项的完整列表,然后filter容器根据筛选条件来调整该列表,并将筛选结果返回给作业队列的基础架构,图3展示了这种模式。
Splitter
有时候,对作业项进行过滤的时候,并不想把过滤出来的一部分作业丢掉,而是希望通过将作业队列分为两个单独的作业队列来形成两种不同的输入,对于这种任务,就需要使用Splitter。Splitter的作用是对作业的一些标准进行评估,就像Filter一样,但并不是丢掉一些输入,而是根据给定的标准向不同的队列发送不同的输入。
Splitter模式的一个例子就是处理在线订单,人们可以通过电子邮件或者短信的方式来接收快递的通知。给定已经发货的物品的作业队列,Splitter将其分成两个不同的队列:一个负责发送电子邮件,另外一个负责发送短信。如果Splitter需要将相同的输出发送到多个队列中,则它也可以是Copier,例如,在前面的那个例子中,用户同时选择了短信和邮件。有意思的是,Splitter实际上也可以由Copier和两个不同的Slipper来实现,但是Splitter模式是一种更加简洁的表现形式,如图4所示是使用Splitter模式向用户发送快递消息。
Sharder
一个稍微更加通用一些的Splitter就是Sharder,就像我们在之前的文章中看到的分片服务一样,Sharder的作用是,在工作流中根据某种分片函数将单个队列分成平均分配的作业项集合。对作业流进行分片可能有以下几点原因:首先是可靠性,如果发生了由于更新错误、基础架构故障或者其他问题,导致了单个作业流失败,通过对作业队列进行分割,这样仅仅会影响到部分服务而不会影响到所有的服务。
例如,假设你向worker容器推送了一个坏的更新,这会导致workers崩溃,并且会导致作业处理的停止,如果你只有一个作业队列来对作业进行处理,那么你的服务就会完全中断并且影响到所有的用户。相反,如果将作业队列分成四个不同的分片,相当于所有的作业交给了四个不同的worker来进行处理,那么假设有一个分片由于某种原因失败了,意味着只有四分之一的用户会受到影响。
对作业队列进行分片的另外一个原因是为了更平均地分配不同资源的作业,如果你并不关心哪个区域或者数据中心用于处理一些特定的作业项集,则可以使用sharder将作业平均地分布到多个数据中心,以平均所有数据中心/区域的资源利用率。并且,将作业队列分布到多个数据中心/区域,当有故障发生时也能更好的提供可靠性,图5显示了一切正常工作时的sharded队列。
当一些shards由于故障失败时,即使只剩下单个队列,分片算法也会进行动态调整以将作业发送到其余健康的作业队列中,如图6所示。
Merger
事件驱动批处理的最后一种模式就是Merger模式。Merger与Copier相反,它是使用两个不同的作业队列,并将他们变成一个单一的作业队列。例如,假设你拥有大量不同的repositories,并且所有的repositories同时提交了新的commit请求,你想要处理每个commit并对其进行build和测试。对每个repositories创建单独的基础架构是很难的,我们可以将每个不同的repositories建模为独立的作业队列源,这些队列提供一个commits的集合,我们可以使用一个merger适配器,将所有不同的作业队列的输入转换为一组合并输入,这个合并的commits流是执行操作时的唯一来源。Merger是适配器模式中一个很好的例子,虽然在这种情况下,适配器实际上是将多个运行中的source容器合并到一个源中,这个多适配器模式如图7所示。
Publisher/Subscriber的基础结构
我们已经看到各种将不同的事件驱动批处理模式连接在一起的抽象模式,但是当真正去构建这样的系统时,我们需要弄清楚如何去管理数据流。最简单的方法是,简单地将作业队列中的每个元素写入本地文件系统上的特定目录,然后让每个阶段都去监视该目录以进行输入。但是,使用本地文件系统会限制我们的作业流在单个节点上的运行,我们可以引入一个网络文件系统来将文件分发到多个节点,但是这样会增加代码和批量作业流的复杂度。
相反,构建这种作业流的一种很流行的方法是使用Publisher/Subscriber(pub/sub)API或服务,pub/sub API允许用户定义一组队列(有时候称其为topics),一个或多个Publishers向这些队列发布消息。同样,一个或多个subscribers监听着这些队列中的新消息,当一个消息发布时,消息会被可靠的存储在队列中,然后以可靠的方式发送给Subscribers。
当下,大多数的公有云都具有pub/sub API,如Azure的EventGrid或Amazon的简单队列服务。此外,开源的Kafka项目提供了非常受欢迎的pub/sub实现,可以在自己的硬件和云虚拟机上运行。
更多推荐
所有评论(0)