译 | Concurrency is not Parallelism(三)

本文涉及的产品
网络型负载均衡 NLB,每月750个小时 15LCU
传统型负载均衡 CLB,每月750个小时 15LCU
应用型负载均衡 ALB,每月750个小时 15LCU
简介: 译 | Concurrency is not Parallelism(三)

Some examples

Launching daemons

从启动一个守护进程开始,您可以使用闭包来包装一些您希望完成但不想等待的后台操作。在这种情况下,我们有两个 channel 输入和输出,无论出于什么原因,我们需要将输入传递到输出,但不想等到复制完成。所以我们使用 go func 和 闭包,然后有一个 for 循环,它读取输入值并写入输出,Go 中的 for range 子句将耗尽 channel。它一直运行直到 channel 为空,然后退出。所以这一小段代码会自动耗尽 channel。因为在后台运行,所以你不需要等待它。这是一个小小的范例,但你知道它还不错,而且已经习惯了。

A simple load balancer

现在让我向您展示一个非常简单的 Load Balancer。如果有时间的话,我会给你看另一个例子。这个例子很简单,想象一下你有一大堆工作要完成。我们将它们抽象出来,将它们具体化为一个包含三个整数值的 Work 结构体,您需要对其执行一些操作。

worker 要做的是根据这些值执行一些计算。然后我在此处加入 Sleep,以保证我们考虑阻塞。因为 worker 可能会被阻塞的一定的时间。我们构造它的方式是让 worker 从 input channel 读取要做的工作,并通过 output channel 传递结果,它们是这个函数的参数。在循环中,遍历输入值,执行计算,sleep 一段任意长的时间,然后将响应传递给输出,传递给等待的任务,所以我们得操心阻塞。那一定很难,对吧,以下就是全部的解决方案。

之所以如此简单,是因为channel 以及它与语言的其他元素一起工作的方式,让您能够表达并发的东西,并很好地组合它们。做法是创建两个 channel, input channel 和 output channel,连接到 worker。 所有 worker 从 input channel 读取,然后传输到 output channel;然后启动任意数量的 worker。注意中间的 go 子句,所有 worker 都在并发运行,也许是并行运行;然后你开始另一项工作,如屏幕显示为这些 worker 创造大量的工作,然后在函数调用中挂起,接收大量的结果,即从 ouput channel 中按照结果完成的顺序读取其值。因为作业结构化的方式,不管是在一个处理器上运行还是在一千个处理器上运行,都会正确而完整地运行。任何人使用该资源都可以同样完成,系统已经为你做好了一切。如果你思考这个问题,它很微不足道。但实际上,在大多数语言中,如果没有并发性,很难简洁地编写。并发性使得做这种事情,可以非常紧凑。

更重要的是,它是隐式并行性的(尽管不是,如果你不想,可以不必考虑该问题),它也能很好地扩展。没有同步或不同步。worker 数量可能是一个巨大的数字,而且它仍然可以高效地工作,因此并发工具使得为较大问题构建此类解决方案变得很容易。

还要注意,没有锁了,没有互斥锁了,所有这些都是在考虑旧的并发模型时需要考虑的,新模型没有了,你看不到它们的存在。然而,一个正确的无锁的并发、并行算法,一定很好,对吧?

但这太容易了,我们有时间看一个更难的。

Load balancer

此例子有点棘手,相同的基本概念,但做的事情更符合现实。假设我们要写一个 Loader Balancer,有一堆 Requester 生成实际的工作,有一堆工作任务。希望将所有这些 Requester 的工作负载分配给任意数量的 Worker,并使其达到某种负载平衡,所以工作会分配给负荷最少的Worker。 所以你可以认为 Worker 们一次可能有大量的工作要做。他们可能同时要做的不止一个,可能有很多。因为有很多请求在处理,所以这会是一个很忙碌的系统。正如我演示的一样,它们也许是在同一台机器上。您也可以想象,其中一些线代表了正在进行适当负载均衡的网络连接,从结构上讲,我们的设计仍然是安全的。

Request 现在看起来很不一样了。有一个任意数量函数的闭包,表示我们要做的计算;有一个 channel 可以返回结果。请注意,不像其他一些类似 Erlang 的语言,在 Go 中 channel 是 Reuqest 的一部分,channel 的概念就在那里,它是语言中 first-class 的东西,使得可以到处传递 channel。它在某种意义上类似于文件描述符,持有 channel 的对象就可以和其他对象通信,但没有 channel 的对象是做不到的。就好像打电话给别人,或者通过文件描述符传递文件一样,是一个相当有影响的概念。想法是,要发送一个需要计算的请求,它包含一个计算完成返回结果的 channel。

以下是一个虚构但能起到说明作用的版本的 Requester。所做的是,有一个请求可以进入的 channel,在这个 work channel 上生成要做的要做的任务;创建了一个 channel,并放入每个请求的内部,以便返回给我们答案。做了一些工作,使用 Sleep 代表(谁知道实际上在做什么)。你在 work channel 上发送一个带有用于计算的函数的请求对象,不管是什么,我不在乎;还有一个把答案发回去的 channel;然后你在那个 channel 等待结果返回。一旦你得到结果,你可能得对结果做些什么。这段代码只是按照一定速度生成工作。它只是产生结果,但是通过使用 input 和 output channel 通信来实现的。

然后是 Worker,在前面的页面,记得么?有一些 Requester,右边的是Worker,它被提供给 balancer,是我最后要给你看的。Worker 拥有一个接收请求的 channel;一个等待任务的计数,Worker 拥有任务的数量代表其负载,它注定很忙;然后是一个 index,是堆结构的一部分,我们马上展示给你看。Worker 所做的就是从它的 Requester 那里接收工作。Request channel 是 Worker 对象的一部分。

调用 Worker 的函数,把请求传递给它,把从 Requester 生成的实际的函数通过均衡器传递给 WorkerWorker 计算答案,然后在 channel 上返回答案。请注意,与许多其他负载均衡架构不同,从 Worker 返回给 Requester 的 channel 不通过 Loader Balancer。一旦 RequesterWorker 建立连接,图中的“中介”就会消失,请求上的工作直接进行通信。因为在系统运行时,系统内部可以来回传递 channel。如果愿意,也可以在里面放一个 goroutine,在这里放一个 go 语句,然后在 Worker 上并行地处理所有的请求。如果这样做的话,一样会工作的很好,但这已经足够了。

Balancer 有点神奇,你需要一个 Workerpool; 需要一些 Balancer 对象,以绑定一些方法到 BalancerBalancer 包含一个 pool;一个 done channel,用以让 Worker 告诉 Loader Balancer 它已经完成了最近的计算。

所以 balance 很简单,它所做的只是永远执行一个 select 语句,等待做更多来自 Requester 的工作。在这种情况下,它会分发请求给负载最轻的 Worker;或者 Worker 告知,它已经完成计算,在这种情况下,可以通过更新数据结构表明 Worker 完成了它的任务。所以这只是一个简单的两路 select。然后,我们需要增加这两个函数,而要做到这一点,实际上要做的就是构造一个堆。

我跳过这些令人很兴奋的片段,你们已经知道什么意思。

Dispatch, dispatch 要做的就是找到负载最少的 Worker,它是基于堆实现的一个标准优先级队列。所以你把负载最少的 Worker 从堆里取出来,通过将请求写入 request channel 来发送任务。因为增加了一个任务,需要增加负载,这会影响负载分布。然后你把它放回堆的同一个地方,就这样。你刚刚调度了它,并且在结构上进行了更新,这就是可执行代码行的内容。

然后是 complete 的任务,也就是工作完成后,必须做一些相反的事情。 Worker 的队列中减少了一个任务,所以减少了它的等待计数。从堆里弹出 Worker,然后把它放回堆中,优先级队列会把它放回中它所属的位置,这是一个半现实的 Loader Balancer 的完整实现。此处的关键点是数据结构使用的是 channel 和 goroutine 来构造并发的东西。

Lesson

结果是可伸缩的,是正确的,很简单,没有显式的锁,而架构让它得以实现。因此,并发性使此例子的内在并行性成为可能。你可以运行这个程序,我有这个程序,它是可编译、可运行的,而且工作正常,负载均衡也做得很好。物体保持在均匀的负载下,按照模块量化,很不错。我从来没说过有多少 Worker,有多少问题。可能每个都有一个,另一个有数10个;或者每个都有一千,或者每个都有一百万,扩缩容仍然有效,并且仍然高效。

One more example

再举一个例子,这个例子有点令人惊讶,但它适合一张幻灯片就可以完成。

想象一下如何复制数据库,你得到了几个数据库,每个数据库中有相同的数据,谷歌称之为分片,称呼相同的实例。您要做的是向所有数据库传递一个请求,一个查询,并返回结果。结果会是一样的,你选择第一个应答请求来加快速度,因为首先要回来的是你想要的答案。如果其中一个坏了,断开了或者什么的,你不在乎。因为会有其他响应回来,这就是如何做到这一点。这就是它的全部实现。您有一些连接数组和一些要执行的查询,您创建一个 channel,该 channel 缓冲查询数据库中的元素数、副本内的副本数大小的内容,然后您只需在数据库的所有连接上执行。对于其中的每一个,您启动一个 goroutine 以将查询传递到该数据库,然后获取答案。但是通过这个 DoQuery 调用,将答案传递到唯一的 channel,这个 channel 保存所有请求的结果。然后,在你执行之后,所有的 goroutine 都只需在底部这行等待。我们等待第一个回到 channel 的请求,就是你想要的答案。返回它,就完成了。这看起来像个玩具,而且有点像。但这实际上是一个完全正确的实现,唯一缺少的是干净的回收。你想告诉那些还没回来的服务器关闭。当你已经得到答案,不再需要它们。你可以做,增加更多且合理的代码,但那就不适合放在幻灯片上了。所以我只想告诉你,在很多系统中,这是一个相当复杂的问题,但在这里,它只是自然地脱离了架构。因为你已经有了并发工具来表示一个相当大的复杂的分布式问题,它运行得非常好。

相关实践学习
SLB负载均衡实践
本场景通过使用阿里云负载均衡 SLB 以及对负载均衡 SLB 后端服务器 ECS 的权重进行修改,快速解决服务器响应速度慢的问题
负载均衡入门与产品使用指南
负载均衡(Server Load Balancer)是对多台云服务器进行流量分发的负载均衡服务,可以通过流量分发扩展应用系统对外的服务能力,通过消除单点故障提升应用系统的可用性。 本课程主要介绍负载均衡的相关技术以及阿里云负载均衡产品的使用方法。
目录
相关文章
|
分布式计算 Spark
通过spark.default.parallelism谈Spark并行度
本篇文章首先通过大家熟知的一个参数spark.default.parallelism为引,聊一聊Spark并行度都由哪些因素决定?
通过spark.default.parallelism谈Spark并行度
|
算法 安全 Shell
译 | Concurrency is not Parallelism(二)
译 | Concurrency is not Parallelism(二)
65 0
|
程序员 Linux Go
译 | Concurrency is not Parallelism(一)
译 | Concurrency is not Parallelism
75 0
|
缓存 Go
译 | Concurrency is not Parallelism(四)
译 | Concurrency is not Parallelism
68 0
|
分布式计算 Java Spark
Optimizing Spark job parameters
Optimizing Spark job parameters
267 0
|
SQL 分布式计算 IDE
Spark AQE中的CoalesceShufflePartitions和OptimizeLocalShuffleReader
Spark AQE中的CoalesceShufflePartitions和OptimizeLocalShuffleReader
456 0
Spark AQE中的CoalesceShufflePartitions和OptimizeLocalShuffleReader
|
JavaScript 前端开发 机器人
workers
web worker 是运行在后台的 JavaScript,不会影响页面的性能。 JavaScript 语言采用的是单线程模型,也就是说,所有任务只能在一个线程上完成,一次只能做一件事。前面的任务没做完,后面的任务只能等着。随着电脑计算能力的增强,尤其是多核 CPU 的出现,单线程带来很大的不便,无法充分发挥计算机的计算能力
262 0
|
Web App开发 JavaScript 前端开发
多线程(Multi-threading)和并行程序(Parallel Programming)详解
多线程(Multi-threading)和并行程序(Parallel Programming)详解
1042 0
多线程(Multi-threading)和并行程序(Parallel Programming)详解
|
JSON Cloud Native 数据建模
Parallel 解析
从 Knative Eventing 0.8 开始,支持根据不同的过滤条件对事件进行选择处理。通过 Parallel 提供了这样的能力。本文就给大家介绍一下这个特性。
1207 0
Parallel 解析