acm-header
登录

ACM通信

实践

并行处理承诺


带有承诺的并行处理,说明

来源:Shutterstock.com

回到顶部

在当今世界,编写并发软件的原因有很多。提高性能和提高吞吐量的愿望导致了许多不同的异步技术。然而,所涉及的技术通常是复杂的,并且是许多微妙bug的来源,特别是当它们需要共享可变状态时。如果不需要共享状态,那么这些问题可以通过一个更好的抽象来解决承诺https://en.wikipedia.org/wiki/Promise_(编程)).这允许程序员将异步函数调用挂钩在一起,在运行链中的下一个适当函数之前,等待每个函数返回成功或失败。

使用本文中描述的设计模式,程序员可以构建简单且易于推理的线程或进程的协作系统承诺.作为承诺可以用多种语言实现,这种设计模式可以用最合适的语言实现,甚至可以用多种语言实现每个流程。

在基本设计中,工人要求完成的每一件工作都会创建一个承诺那项工作。相关的工作与此挂钩承诺并在开始之前等待它完成。如果工作再次被请求,新的worker只需连接到电流上承诺链和等待结果与其他一切。简而言之,承诺对锁定进行抽象,并允许程序员将注意力集中在覆盖的模式上。通过限制系统可以达到的可能组合集,处理所有可能的情况变得简单、明显,并消除并发程序固有的最大一类错误。

开发人员经常需要跨机器、进程或线程集群分发工作。这些分布式系统通常需要复杂的代码,以确保每一项工作只有一个工作人员完成,并且不会遗漏任何工作,但当涉及的一些工作依赖于之前的工作时,情况只会变得更糟。这些片段是如何同步的?什么时候一个工人会等待另一个人去做工作,什么时候他会把工作自己拿走?

本文展示了这些问题和其他问题如何轻松地自行解决,而不需要复杂的管理系统,而是使用承诺创建一个状态机,极大地简化了情况。与许多分布式编程算法不同的是,这些工作人员进行协作,以确保其中只有一个工作人员完成一个独特的工作单元,并与需要该数据的工作人员进行通信。这消除了工作者管理的需要,可扩展到任意大的集群,最重要的是,使编写和推理代码变得容易。

在这样的系统中可以做什么样的工作?答案碰巧是任何可以唯一命名的作品。使用一个简单的数据结构来保持工作的名称与关联的承诺防止任何未完成的工作被重新处理,并允许新请求与未完成的工作挂钩承诺.这简单而容易地在工作人员之间进行协调,并为程序员提供了一个简单的API,无论他们喜欢哪种语言,只要它支持承诺.该API在服务器软件中特别有用,因为它的工作可以是Web服务器的请求处理、MapReduce算法的函数计算或数据库查询。

无论涉及到什么,只要所有的工作线程使用相同的命名方案,它对系统本身都不重要。最简单的解决方案是哈希每一项工作,以获得这个惟一的名称。这对于大多数类型的工作都是有效的,因为它们很容易表示为文本和散列。

当一个工人收到这些工作中的一个时,它请求一个承诺来自系统内的中央权威机构。作为这些的维护者承诺在美国,中央当局把复杂性从工人那里抽象出来。这为系统程序员提供了一个简单的界面,并提供了一个单独的位置来添加分布式锁定算法(如果需要的话)。一旦工人收到承诺的哪一端,它检查是否应该开始处理承诺它收到了。否则,它只是连接适当的函数,以便在完成承诺它收到了。一旦worker实际处理完请求,它将数据发送到承诺.这将通知所有其他工作者结果,并允许它们并行处理它们的完成处理程序。这个系统让程序员可以轻松地编写并发处理系统,而不会陷入锁或复杂的无锁算法的细节中。

在更大的范围内,如果系统设计需要多个服务器或进程,则每个进程中的中央当局必须在它们之间实现某种形式的同步。程序员必须注意确定他们在这样一个分布式系统的CAP谱中的位置。的承诺不要,也不能解决分布式共识系统,事实上,甚至不知道这样的协调正在发生。最好的解决方案是将问题抽象到专门为其设计的外部源。如果工作可以多次完成而不受影响,那么一个可用的和分区容忍的算法将是最合适的。尽可能地,没有工人会重复工作,但在边缘情况下不会发生灾难。对于只会发生一次的工作,应该使用一致且允许分区的算法来代替锁定机制。在网络分区的情况下,一些工作人员将无法处理,但这是为一致性所付出的代价。

在完成这个系统后,程序员不再需要担心如何同步的细节。他们可以把精力花在他们真正需要解决的问题上。

回到顶部

是什么承诺?

承诺提供该算法使用的核心功能,了解它们是很重要的。从根本上说,一个承诺有两个组成部分,这里称为递延而且期货.一个递延是a的输入边吗承诺实际上做功的部分。一旦工作完成,递延与结果一起解析。如果发生错误,则递延可以用错误来拒绝。在这两种情况下未来是a的输出边吗承诺,接收结果。未来可以被束缚,所以一个未来可以充当递延为其他期货

每一个未来有两个功能:成功和失败。如果递延得到解析,然后使用结果值调用成功函数。如果递延被拒绝,然后用错误调用失败函数。如果一个未来没有定义适当的函数,那么值起泡到任何附加期货

因为期货可以附加一个错误函数,它们不需要在成功函数内部有错误处理代码。这避免了回调的一个问题:将错误处理与成功的处理代码混合在一起。但是如果错误是可恢复的呢?然后是中间产物未来只有一个错误函数。如果发生错误,它可以修复它,然后自行解决。这个调用下一个未来成功的功能。如果没有发生错误,则中间函数只是将值传递给下一个函数未来成功的功能。在这两种情况下,都会进行适当的处理。这允许承诺作为try/catch块的异步等等物工作。


该系统允许程序员编写并发处理系统,而不必陷入锁或复杂的无锁算法的细节中。


承诺还有另一个功能,不像他们的表弟,回调:a递延可以解决或拒绝未来.当这种情况发生时,一个新的承诺在任何之前插入链期货等待当前步骤。此特性的一个重要用途是故障函数中的恢复代码。用a解出来未来在美国,如果试图获取数据失败,可以在没有其他工具的情况下再次尝试期货等待数据,知道发生了错误。通过使用这种技术,开发人员可以将编程任务的各个部分封装到松散耦合的原子函数中,其顺序可以很容易地推断出来,如中所示图1

州。因为承诺必须运行它们的成功函数或失败函数,整个系统由中央当局维护只有少数的状态,必须进行推理。首先是等待未来.一旦工人开始加工未来可能永远处于等待状态,因为它的关联递延尚未解决。为了解决这个问题,中央当局应该向工作者公开一个功能,它可以通过该功能作为健康脉冲接收定期通知。然而,即使使用这些定期通知也会有损失。中央应该把目前未完成的工作一扫而空,拒绝任何工作承诺他们已经等了太久。这将触发整个链条上的重试代码。

一旦承诺的结果进行解析时,它必须开始运行未来链子连着它。在任何这些步骤中,都可能发生错误。幸运的是,承诺将抛出的异常转换为下一个catch函数的调用。在链中向上传递这些错误意味着它们将在catch函数中得到正确处理或传递给外部调用方。这可以防止工作在不知情的情况下失败或在糟糕的状态下完成。

最后,如果工作结束,那么一切都按预期运行,系统已经处理了请求。因为承诺抽象出这三种状态,每一种状态都可以在代码中以明显的方式处理。这可以防止许多困扰分布式系统的微妙bug。

基本的算法设计。协作算法的核心是遵循五个简单步骤,如所示图2

  1. 工作单元将获得一个商定的名称,以便对同一工作的多个请求获得相同的名称未来.如果没有商定的命名方案,没有一个工作人员可以知道另一个工作人员是否已经在处理该工作。
  2. 递延从当前操作请求的散列表中请求。如果没有递延存在,则创建一个,添加到哈希表,并返回给请求者。如果已经有递延,相关的未来被返回。改变每种情况下返回的内容可以让worker知道是应该开始处理还是只是等待。
  3. 捕获函数附加到未来.如果发生错误,这个catch函数会重试,并运行原始函数。通过这样做,新的尝试将抓住并再次尝试。这允许无限的重试,或者通过跟踪重试尝试的计数,它可以沿着链向上失败,而不是恢复。因为每个工人都在处理这个承诺同时附加它自己的捕获器,它们都在失败时重试。他们中的任何一个都可能赢得再次尝试的权利,在相关工人之间提供一个内置的通信系统。
  4. 对输入所做的工作是附加的,因此一旦数据到达,就可以进行处理。如果多个工人同时做这项工作,应该没有关系,因为他们的承诺完整的并行。另外,通过返回这个承诺在美国,工作可以与之前完成的工作联系在一起。
  5. 最后,如果工人收到递延,那么它应该解决递延一旦它完成了工作的处理。如果在处理期间发生故障,则递延错误应该被拒绝,这将通知所有的工作线程他们应该重试。

当作品由可组合的小部件组成时会发生什么?递归地请求每一项工作意味着递延可以通过未来对于前面的部分。这样就把小的碎片串在了一起,一个工蚁沿着它所需要的东西链往回走,直到找到它可以利用的第一个工蚁或另一个工蚁。因为每个承诺是异步的,工作者不再需要保持它,而作为期货获取数据并准备运行,任何工作人员都可以接收它们。如果只有一条链,它必然一次处理一步,就像没有系统环绕它一样。由于多个工作人员的存在,这些片段不能运行得更快,因为它们依赖于之前的片段,但是共享片段或多个请求可以利用已经在处理的部分。

回到顶部

分布式系统中会发生什么?

如果架构不允许共享一个承诺例如,在不同的服务器上运行,或者用不能共享的语言编写的应用程序承诺跨进程。在这些情况下,需要一种锁定机制来保持分布式中心中心的同步,以便处理承诺.Redis服务器或chubby集群可以用来提供这种锁,因为这两个系统都用于分布式锁。必须谨慎地确定所选系统提供必要的一致性或可用性需求。此外,一种向其他工人广播的方法有助于加快完成时间。不需要等待他们检查锁,可以传递一条消息表示工作已经完成。在内部递延,则必须向锁定服务发出请求。

如果锁定服务将锁提供给请求者,它将解析递延像以前一样。另一个承诺是并行添加的。完成,这承诺向分布式对等体广播完成消息。对等体监听这样的公告并解决它们的本地消息递延通过从共享数据存储或从广播消息中获取数据。

这些步骤必须添加在本地递延请求。由于本地worker不知道其他worker是否已经启动,所以它必须在启动时尝试获取锁。获取锁失败表明其他工作者已经开始处理。这可以对跨节点的工作者进行自我调节。详细的步骤图3显示会发生什么。

保持活着。在任何情况下,处理工作的工作者都需要保持其同伴对其进展的更新。为此,工作人员向中央当局定期发送通知,充当健康脉搏的作用。如果没有这个脉冲,那么中央当局就会为其他工作人员触发重试代码,确认工作人员的死亡。使用已经建立的代码路径来指示故障意味着可以推断的状态更少,而且工作人员可以确定,如果锁失效了,就会有人正在处理或即将处理。

此外,承诺保护系统不受工人假死的影响。如果worker未能继续发送运行状况脉冲,则另一个worker将接管该工作,但原来的worker将继续处理该工作,并可能在原来的worker之前完成。最初的递延然而,已经被失去的健康脉搏拒绝,促使另一个工人接管。承诺只能完成一次,那么原工什么时候解原递延,什么也不会发生。所有的承诺有了一个新的承诺插入到他们的链条中,也就是说,他们都被告知原来的工人已经失效,新的工人正在处理。没有人需要知道或关心原来的工作人员是否已经死亡或最终能否完成任务。

作为图4显示,系统有两个主要状态:工作状态和等待状态。如果一个工人成功地得到递延,进入工作状态。在那里,工人必须向中央当局发出通知,表明它仍然活着。如果系统使用锁定服务,则通知还会更新锁定时间,以指示工作正在进行。这种健康脉搏可以防止其他工作人员永远等待。没有它,一个失败的工人将永远挂起系统。

处于等待状态的工作人员将获得已完成的工作并完成,否则将被拒绝并运行重试代码。如果中央中心超时等待工作人员的健康脉搏,那么它将与锁定服务进行检查,看它是否存在。如果没有服务或锁定时间未能得到更新,则等待的工作线程将拥有它们的递延拒绝和重试错误,导致所有等待的工作者遵循基本的错误恢复步骤。一个新的worker将开始做这项工作,如果原来的worker实际上没有失败,它将解决之前拒绝的工作承诺.解决或拒绝已完成的承诺是noop(没有操作)并被忽略,如图5

实现经验。这里描述的设计最初是出于在Mongo数据库集群上集成查询缓存系统的需要。分布在不同服务器上的多个Node.js进程将同时为用户运行查询。不仅结果需要被缓存,而且大型的、长时间运行的查询如果并行运行多次会锁定Mongo集群。因为每个查询都是要执行的步骤的管道,所以查询也可以建立在彼此的基础上。一个匹配特定文档的查询,然后对它们进行排序,可以在之后添加分组步骤。

自然,通过这些步骤向后递归并哈希当前段提供了一个完美的命名方案。如果用户已经运行了查询,那么可以在用于存储结果的Redis数据库中查找该名称,而不需要运行查询。否则,按照这里描述的设计,适当的承诺请求。如果另一个用户已经开始了查询,那么不需要做任何工作。这可以防止Mongo数据库连续使用相同的查询重载,并且可以更快地获得对同一查询的多个请求的结果。

通过查询段集向后工作,工作者可以钩到任何已经缓存或正在运行的查询。但是,在获得中间部分的结果之后,需要进行额外的Mongo查询工作。mungedb-aggregate库(https://github.com/RiveraGroup/mungedb-aggregate)提供了一组一对一的操作符,允许工作者完成任何额外的查询段。如果没有这个库,整个查询将不得不在Mongo中运行,运行可组合段将是不可行的。唯一有用的是缓存结果,并且已经有了这样做的解决方案。

Node.js进程有自己的中心权限来保持一个单一的承诺对于单个查询的任何并发请求。当单个进程用于小型部署时,这种功能运行良好。多个并发请求将链接在一起,并且它们将透明地在工作上协作。不幸的是,node . js承诺无法跨越进程和服务器边界。在向外扩展到多个服务器时,这就需要开发系统的完全分布式版本。由于Redis已经有了缓存数据库的位置,它对所需的分布式锁进行了自然的抽象。Redis被认为是最好的算法,因为它是可用的和分区容忍的。如果不同的服务器失去联系,那么让每个服务器运行查询的单一副本被认为是正确的做法,因为不同的运行的结果是相同的。请求者总是得到结果,并且系统尽可能地减少工作负载。

一旦开始实现,遇到的唯一主要问题是如何为当前处理工作提供健康检查。幸运的是,这个问承诺库(https://github.com/kriskowal/q)为其实现提供了一个通知函数。这为中央当局监听提供了一个方便的钩子。listener函数通过Redis数据库中的更新时间来更新远程Node.js进程。

在生产中实现的整个生命周期中,没有一个未处理的错误。即使有许多查询同时冲击系统,Node.js进程在处理过程中突然死亡,系统仍然按照设计继续工作。与程序员难以推理的复杂锁定代码不同,承诺链提供了一种清晰、明确的方式来协调查询。在阅读代码时,程序员可以清楚地看到它将工作,以及哪个部分负责哪个任务。其他系统可能提供相同的实用程序,但很少有这样易于使用和修改的。

回到顶部

结论

由这些定义的系统承诺提供了强有力的保证。解析期间的异常导致拒绝通过承诺,所以工作不会挂。承诺链,所以工人等待之前的工作,然后开始做他们的工作决议。通知侦听器提供超时以防止工作挂起。做这项工作的人越少越好,第一个成功的人就是提供给所有人的权威答案。最重要的是,该算法提供了一种简单而正确的编写协作系统的方法。

ACM队列的q戳相关文章
queue.acm.org

软件和并发革命
赫伯·萨特和詹姆斯·拉鲁斯
http://queue.acm.org/detail.cfm?id=1095421

与雷·奥兹的对话
http://queue.acm.org/detail.cfm?id=1105674

事务性内存并行编程
乌尔里希Drepper
http://queue.acm.org/detail.cfm?id=1454464

回到顶部

作者

斯宾塞Rathbun是里维拉集团的高级软件架构师。他的专长是为主要关注软件架构领域的客户群设计大数据解决方案。为此,他专门使用结构化/半结构化数据为现实世界的问题设计创造性的解决方案。

回到顶部

数据

F1图1。插入的承诺。

F2图2。基本高速缓存算法。

F3图3。带锁定的缓存算法。

F4图4。缓存通知。

F5图5。示例代码重试。

回到顶部


版权归所有人所有。授权ACM出版权利。

数字图书馆是由计算机协会出版的。版权所有©2015 ACM, Inc.


没有发现记录

登录为完全访问
»忘记密码? »创建ACM Web帐号
文章内容:
Baidu
map