Erlang的调度原理(译文)
原文 http://jlouisramblings.blogspot.com/2013/01/how-erlang-does-scheduling.html
免爬墙链接 http://www.dikutal.dk/blog/jlouis/how-erlang-does-scheduling
Jesper Louis Andersen,2013年1月12日
我用这篇文章解释一下Erlang和其他语言运行时相比不同之处。我还要解释为什么Erlang往往会牺牲吞吐换取更低的延迟。
太长了,我懒得读(译者注:原文TL;DR表示too long, didn’t read。原作者可能是在参与和别人的讨论中写的这篇文章,“懒得读”表现出一种“懒得跟你们争了”的有趣心态。校园网爬墙速度太慢,所以没有验证。)——Erlang和其他语言运行时不同之处在于关注的重点不同。本文描述了为什么进程很少的时候往往看上去似乎性能很差,但是进程很多的时候却表现得很好。
老是有人会问Erlang调度的原理。本文只是对Erlang真正调度原理的简单概述,但是描述了Erlang对其进程的操作方式。注意我这里讨论的是Erlang R15。未来Erlang可能会发生很大的变化,但是不论是Erlang还是其他系统,事物总是会朝着更好的方向发展。
从操作系统的角度来说,Erlang通常在机器上的每一个处理器核心上跑一个线程。每一个线程运行一个调度器。这种设定是为了确保机器上所有的核心都可以为Erlang系统卖力。通过+sbt参数可以将处理器核心和调度器绑定,也就是说调度器不会在众多核心之间跳来跳去。调度器绑定仅限于现代操作系统,因此OS X自然做不到。要实现调度器绑定,说明Erlang系统了解处理器的拓扑结构以及处理器相关的亲缘性,由于高速缓存以及迁移时间等原因,这些信息非常重要。设置+sbt参数通常都可以提升系统的速度。而且有的时候还能提升不少。
+A参数定义了异步线程池中异步线程的数量。驱动程序可以通过异步线程池中的线程执行阻塞的操作,这样调度器可以在线程池中有些线程阻塞的情况下依然执行其他有用的工作。最值得注意的是,文件驱动程序通过线程池加速文件I/O,而网络I/O则没有使用线程池。
以上内容是从操作系统内核的角度描述的,下面我们来理清Erlang进程(隶属于用户空间)的概念。通过调用spawn(fun worker/0)可以构建一个新的进程,Erlang系统会在用户空间分配进程控制块。一个进程通常需要大约600多个字节,而且32位系统和64位系统会有不同。可运行的进程放在调度器的运行队列中,之后获得时间片的时候就可以运行。
在深入描述单个调度器之前,我先简要地描述一下迁移(migration)的工作原理。每过一段时间,调度器就会通过一个非常复杂的过程在调度器之间迁移一些进程。这种启发式迁移的目标就是为了平衡多个调度器的负载,使得所有的核心都能得到充分的利用。这个平衡算法还要考虑工作量是否足够大,是否大到需要启动一些新的调度器。如果没那么大的话,那么最好让那些调度器保持关闭状态,因为反正那些线程也没有工作可做。关闭调度器意味着处理器核心可以进入节能状态,甚至关闭核心。没错,Erlang会尽可能地节省电源消耗。如果调度器做完了工作,还会从其他调度器“窃取”工作(work-steal)。细节请参见[1]。
重要:在R15中,调度器的启动和停止是有延迟(lagged)的。因为Erlang/OTP深知启动或停止一个调度器的开销非常大,所以不是真正需要的话是不会采取行动的。假设某一个调度器现在无事可做,那么系统不会立即将这个调度器设置为睡眠状态,而是会自旋等待一阵子,期待有任务会立即到达。如果有任务到达,那么调度器可以以低延迟立即开始处理任务。换句话说,不能使用top(1)这类工具或通过操作系统内核来测量系统执行的效率,而是必须使用Erlang系统的内部调用来测量。正因为这个原因,有不少人错误地认为R15不如R14高效。
每一个调度器都运行两类作业:进程作业和port作业。这些作业运行的时候是带有优先级的,就像操作系统内核一样,因此也会面对和操作系统内核一样的担忧和启发式调度。进程可以标记高优先级和低优先级等优先级。进程作业执行一个进程一小段时间。port作业考虑的是port。如果你不知道port是什么,我简单解释一下port:port就是Erlang中一种和外部世界通信的机制。文件、网络套接字、和其他程序之间建立的管道,在Erlang中都是通过port实现的。开发者可以在Erlang系统中添加“port驱动程序”来支持新的port类型,不过这就要求编写C语言代码了。调度器还要对网络套接字进行轮询(polling),这样才能从网络中读取数据。
进程和端口都有一个“reduction预算”,即2000个reduction。系统中的任何操作都要消耗reduction。这些操作包括循环中的函数调用、BIF(内建函数)的调用、进程中堆的垃圾回收[注1]、存取ETS和发送消息等(发送消息要考虑接收者的邮箱大小,邮箱越大发送的代价越高)。顺便提一下,会扣除reduction的地方遍及整个Erlang系统。比如Erlang的正则表达式库就做了修改,即使这个库是用C语言编写的,其中也添加了扣除reduction的相关代码。因此如果你有一个非常耗时的正则表达式,那么这个正则表达式的操作也会扣除reduction,所以在运行的过程中有可能会被抢占好几次。port也不例外!在一个port上执行I/O操作会消耗reduction,发送分布式消息也会消耗reduction,不胜枚举。开发者花了大量时间确保系统中产生的每一步进度都会消耗reduction[注2]。
实际上,这就是为什么我说Erlang是真正能够实现抢占式多任务并且能真正做好软实时的少数语言之一的原因。Erlang更看重的是低延迟而不是单纯的吞吐量,这在程序设计语言运行时中是不多见的。
准确地说,抢占(preemption)[2]指的是调度器能够强制剥夺任务的执行。所有基于协作(cooperation)的多任务都是做不到抢占的,例如Python的twisted库、Node.js和LWT(Ocaml)等。但是更有意思的是,Go(golang.org)和Haskell(GHC)也都不是完全抢占式的。Go只有在通信的时候会发生上下文切换,因此一个密集的循环就会霸占整个处理器核心。GHC会在内存分配的时候发生切换(不得不承认内存分配是Haskell程序中一个非常频繁的操作)。这些系统的问题在于,将处理器核心霸占一段时间的后果就是影响系统的响应延迟——想象一下这两种语言执行数组操作的时候的情景。
这就引出了软实时(soft-realtime)[3]的概念,软实时指的是如果无法满足时间截止线需求的时候会导致系统服务水准降级(而不是整个失败)。假设在运行队列中有500有100个进程。第一个进程正在做一个耗时50毫秒的数组操作。在Go或Haskell/GHC[注3]中,这意味着任务2-100都需要至少50ms。而在Erlang中则不同,任务1有2000个reduction的预算,相当于大约1ms的时间。然后用完reduction预算后,任务1会被放回运行队列,这样任务2-任务100就有机会运行。这自然意味着所有的任务都有公平的时间份额。
Erlang是为保证低延迟软实时的特性而精心打造的。2000的reduction预算很低,会导致很多小的上下文切换。耗时长的BIF在计算过程中被抢占的代价非常高昂。但是这样可以保证Erlang在系统负载更高的情况下能够优雅地降级。对于像Ericsson这样在乎低延迟的公司来说,这也意味着别无选择了。你不可能神奇地找到另外一种为吞吐量打造的语言同时也获得低延迟的好处,你必须为之付出努力。如果低延迟对你来说很重要,那么平心而论,不选Erlang反而显得很奇怪了。
[1] "Characterizing the Scalability of Erlang VM on Many-core Processors"http://kth.diva-portal.org/smash/record.jsf?searchId=2&pid=diva2:392243
[2] http://en.wikipedia.org/wiki/Preemption_(computing)
[3] http://en.wikipedia.org/wiki/Real-time_computing
[注1] 进程堆是每个进程私有的,因此一个进程不会对其他进程的GC时间造成太大影响。
[注2] 这段话也点明了为什么要小心耗时长的NIF的原因。NIF默认不会被抢占,而且也不会贡献reduction计数器。因此耗时长的NIF会引入系统延迟。
[注3] 这里考虑单核心的情况,多核心能在一定程度上“掩盖”单核心的这个问题,但是问题依然存在。
(2013年1月14日对本文稍有更新)
Erlang的调度原理(译文)的更多相关文章
- golang channel的使用以及调度原理
golang channel的使用以及调度原理 为了并发的goroutines之间的通讯,golang使用了管道channel. 可以通过一个goroutines向channel发送数据,然后从另一个 ...
- python并发编程之进程、线程、协程的调度原理(六)
进程.线程和协程的调度和运行原理总结. 系列文章 python并发编程之threading线程(一) python并发编程之multiprocessing进程(二) python并发编程之asynci ...
- 弄懂goroutine调度原理
goroutine简介 golang语言作者Rob Pike说,"Goroutine是一个与其他goroutines 并发运行在同一地址空间的Go函数或方法.一个运行的程序由一个或更多个go ...
- 图解Go协程调度原理,小白都能理解
阅读本文仅需五分钟,golang协程调度原理,小白也能看懂,超实用. 什么是协程 对于进程.线程,都是有内核进行调度,有CPU时间片的概念,进行抢占式调度.协程,又称微线程,纤程.英文名Corouti ...
- Go的并发调度原理
Go语言是为并发而生的语言,Go语言是为数不多的在语言层面实现并发的语言:也正是Go语言的并发特性,吸引了全球无数的开发者. 并发(concurrency)和并行(parallellism) 并发 ...
- go语言之行--golang核武器goroutine调度原理、channel详解
一.goroutine简介 goroutine是go语言中最为NB的设计,也是其魅力所在,goroutine的本质是协程,是实现并行计算的核心.goroutine使用方式非常的简单,只需使用go关键字 ...
- 欢迎阅读 Erlang OTP 设计原理文档
http://erldoc.com/doc/otp-design-principles/index.html 原文: OTP Design Principles 翻译: ShiningRay 有任何问 ...
- [GO语言的并发之道] Goroutine调度原理&Channel详解
并发(并行),一直以来都是一个编程语言里的核心主题之一,也是被开发者关注最多的话题:Go语言作为一个出道以来就自带 『高并发』光环的富二代编程语言,它的并发(并行)编程肯定是值得开发者去探究的,而Go ...
- 十五,K8S集群调度原理及调度策略
目录 k8s调度器Scheduler Scheduler工作原理 请求及Scheduler调度步骤: k8s的调用工作方式 常用预选策略 常用优先函数 节点亲和性调度 节点硬亲和性 节点软亲和性 Po ...
随机推荐
- DevOps 解读
本文为 转载文章, 非原创 DevOps DevOps(Development和Operations的组合词)是一种重视“软件开发人员(Dev)”和“IT运维技术人员(Ops)”之间沟通合作的文化.运 ...
- mongodb的学习-3-在Mac上的安装配置
1.使用homebrew安装: brew install mongodb 查看安装好的版本: mongo --version MongoDB shell version v3.6.4 git vers ...
- 469 B. Intercepted Message
http://codeforces.com/problemset/problem/950/B Hacker Zhorik wants to decipher two secret messages h ...
- K2 BPM介绍(1)
K2 BPM介绍(1) 官网访问地址: 中文官网 英文官网 它是一个强大的BPM产品 K2 BPM详解 产品特性 与任何内容集成 Integrate with Anything 功能丰富的窗体 Fea ...
- p标签不自动换行原因
前言:发现以前写的就像是笔记,哪像博客啊,这里再次修改. 问题描述: 在固定宽度的p元素里(任何块级元素同理),长单词不自动换行,中文字符会自动换行,效果如:http://codepen.io/ali ...
- 火狐下不能使用非行间样式currentStyle用getComputedStyle获取
用js的style属性可以获得html标签的样式,但是不能获取非行间样式.那么怎么用js获取css的非行间样式呢?在IE下可以用currentStyle,而在火狐下面我们需要用到getComputed ...
- 微信公众号发送消息模板(java)
这段时间接触公众号开发,写下向用户发送消息模板的接口调用 先上接口代码 public static JSONObject sendModelMessage(ServletContext context ...
- Yii2 的安装及简单使用
前段时间第一次使用Yii2框架,碰到了一些问题,这里记录一下. Yii2安装:通过composer安装 1.首先要安装composer,我在另外一篇博客中介绍了如何在Windows下安装compose ...
- SQL优化技巧-批处理替代游标
通过MSSQL中的用户自定义表类型可以快速将需要处理的数据存储起来,生成新的临时表(这里使用变量表),然后根据表中字段进行批处理替代游标. 用户自定义表类型 0 --创建用户自定义表类型 1 Crea ...
- 大数据调错系列之:自己总结的myeclipse连接hadoop会出现的问题
在我们学习或者工作中开始hadoop程序的时候,往往会遇到一个问题,我们写好的程序需要打成包放在集群中运行,这无形中在浪费我们的时间,因为程序可以需要不断的调试,然后把最终程序放在集群中即可.为了解决 ...