我们如何为多核编程¶
标题:How we program multicores
日期:2016/12/01
作者:Joe Armstrong
链接:https://www.youtube.com/watch?v=bo5WL5IQAd0
注意:此为 AI 翻译生成 的中文转录稿,详细说明请参阅仓库中的 README 文件。
备注:Erlang 作者的演讲。我挺喜欢 Erlang 的,虽然从来都不写。
多核时代来临时,很多程序员只是想,我有一台四核计算机,我可以把我的代码拿来,在这台机器上运行,它的速度就会快四倍。然后他们买了这些机器,却发现事实并非如此。
有个人叫 Alex Guinaris,他与比尔·盖茨共事多年,曾是美国在线(America Online)的首席科学官。他们当时正在构建一个大项目,当多核处理器出现时,他决定将他们所有的机器部件全部更新换代。于是他们购买了这些大规模的多核机器。我记得他说过,他们有些机器好像是 16 核的。他做了所有这些工作,把所有软件都部署到了这些 16 核机器上,并期望一切都能运行得快 16 倍。但结果并非如此。他非常失望。
于是他开始研究不同的技术和不同的语言,每个人都承诺使用不同的技术可以让事情变得更快。他尝试了所有这些东西,但没有一个起作用。然后他尝试了 Erlang,结果成功了。实际上,速度并没有快 16 倍。我们没花太多力气就让它跑得快了不少。所以他对此相当满意。
所以我想谈谈这个话题以及一些编程模型。Peter 的演讲给我留下了深刻的印象,因为他说的一些话与我产生了共鸣,他用了一些我觉得非常好的词。他谈到了那些可怕的词:simultaneous(同时的)、parallel(并行的)和 concurrent(并发的)。所以他为我做了一些铺垫工作。
Peter 一直在尝试做的事情,就像是攀登珠穆朗玛峰,因为创建一个并发内存模型就像攀登珠峰一样。我没那么聪明,所以我不想去攀登珠峰。我只想一步一个脚印地往上走。这样我就能回答这个问题了:为什么我的程序在一台 N 核计算机上运行时没有变得更快?
那是因为要做到这一点,我必须编写一个并行程序。而编写并行程序是困难的,是极其困难的。
当程序员思考并行程序时,首先,大多数程序员不理解并行程序和并发程序之间的区别。在我的一本书里,我花了很长时间来解释并发和并行之间的差异。我有一篇博客文章叫《给一个五岁小孩解释清楚》。我的博客里有一些系列文章就叫这个名字。有一个非常简单的例子可以说明什么是并行,什么是并发。
我们在外面喝咖啡的时候,有三台咖啡机。如果你想快速地供应咖啡,你可以在它们后面排成三个队,这就是并行。而并发则是,如果你只有一台咖啡机,但有三个队在等着它,他们会非常有礼貌地交错着去接咖啡。
所以,并行在现实世界中意味着真正意义上的同时发生。但“同时”这又是一个可怕的词。我曾经是一名物理学家,研究过量子力学之类的东西。那么,“同时”意味着什么呢?嗯,我们其实并不真正知道。我们不是在谈论量子纠缠和“鬼魅般的超距作用”之类的事情。我们先不管那些东西,想一想,“同时”在物理学中是没有任何意义的,没有这个概念。物理学中有光线四处传播的概念,一个事件发生于另一个事件之前。如果你看到两颗恒星爆炸,假设你们是两颗恒星,并且在同一时间爆炸,而我站在这里,我会说,这颗恒星先爆炸。但如果我站在这里,我就会说,那颗恒星先爆炸。所以,事物在同一时间发生这个概念在物理学中是不存在的。
因此,将编程模型建立在可以肆意违反物理定律的想法之上,真的是一个非常糟糕的主意。如果你在构建一门编程语言时违反了物理定律,事情就会变得非常困难。
不幸的是,编程语言被分成了两类:共享内存并发模型和消息传递并发模型。共享内存并发模型严重违反了物理定律,并且极其难以理解。而消息传递模型也违反了一些物理定律,但它违反的要少得多,所以它更容易理解。它具有这种“事件发生在单一地点”的特性。所以我们不谈论同时性之类的事情,我们谈论的是消费事件流的单一地点,这很容易理解。
后者类别中有很多语言。但几乎所有的编程语言,C、Python,所有这些都使用共享内存并发。并发实际上根本不在编程语言本身之中,它存在于操作系统里,然后有一个与之相配的接口。只有一两种语言,比如 Erlang、Elixir、Pony,使用消息传递并发。而在这些语言中,我认为 Erlang 是唯一一个可以说是“稳定”的,意思是你可以用它来构建大型产品。它已经存在了很长一段时间。像 WhatsApp、爱立信的智能数据基础设施之类的东西,都是用 Erlang 编程的,并且它们运行得相当好,拥有数十亿的用户。
所以,我认为程序员们不理解并发程序和并行程序之间的区别。而且他们不喜欢学习新的语言、新的做事方式。因为他们已经投入了大量时间来学习如何用 Java、C 或 C++ 编程。嗯,学习一门新的编程语言并不难,我认为你可以在两三周内学会一门新语言的语法。但要学习它的库、编程范式和思维方式,则需要好几年的时间。改变思维方式才是困难的事情,而不是学习语言本身。语言只是语法而已。
所以,基本上,我前面提到的那些语言,Java、JavaScript、C、C++,所有这些东西,你不需要改变你的思维方式。你只需要改改花括号,再加几个分号。Java、JavaScript、Python 和 Perl 之间没有什么区别。它们都是命令式语言,或多或少有着相同的结构,或好或坏,取决于你的看法。但思维方式是一样的。然后你还有其他的语言家族,你有函数式语言,有逻辑语言,有约束语言。对于这些,我们确实需要用不同的方式去思考。而这正是学习的障碍。
我认为,程序员实际上不喜欢复杂性。或者至少,我作为一个程序员,不喜欢复杂性。所以我喜欢那些我希望能理解的简单事物。我其实很希望能写出正确的程序,但当我们编写非常大的程序时,我已经有点放弃了写出正确程序的想法。我更多地认为,如果我们写大型程序,里面就一定会有错误。这些错误会在运行时发生,我们必须与这些错误共存,必须修复它们并继续前进。而且这些程序必须不断演进。我所想的不是那种被放进一台机器里,然后你停机、修改它的程序。它们会随着时间演进而永远运行下去,并且会有大量的错误。所以它们变得更像是生物系统。我们需要理解这样的系统是如何工作的。
而今天,我认为,我们已经陷入了一团糟。因为我以前在爱立信工作时,如果你在走廊里走动,听程序员们交谈,他们不是在讨论如何解决他们的问题。他们中有一半的人在讨论如何解决他们工具的问题。“哎呀,Git 不工作了”,或者“Jenkins 不工作了”,或者这些东西不工作了。你说,“那么,你在解决什么问题?” 他说,“你说的‘问题’是什么意思?问题就是 Git 不工作了啊。” 不,不,不,不,不。爱立信卖的不是关于 Git 的知识或别的什么。爱立信制造手机之类的东西卖给人们。
好吧,我们想让编写并行程序变得容易。对,但我们做不到。编写并行程序是极其困难的。我完全放弃了“编写并行程序很容易”这个想法。所以,让我们做点不一样的事情吧。
让我们让编写并发程序变得容易。 我们可以做到。我们可以让编写并发程序变得相对容易,尽管我们无法编写并行程序。然后,我们可以自动化并发程序的并行化过程。人们多年来一直试图这样做,但基本上都失败了。我记得从 70 年代开始,人们就努力地想并行化 Fortran。尽管投入了巨大的努力,我认为他们也只成功地自动获得了 15% 的速度提升,尽管在这个问题上投入了巨大的脑力。这是一个极其困难的问题。
但是,对于多核,我们可以做到。我们怎么做呢?
与其让计算机或某个形式化系统来决定并发的粒度,不如让程序员来决定。我认为程序员在这方面相当擅长。他们只需要识别出他们认为是并发的进程,然后告诉计算机。不要让自动化系统来做这件事,从来没有任何一个自动化系统能够成功地做到这一点。我认为这做起来相当容易,特别是如果你正在编程模拟现实世界的东西。
好的,所以程序员决定并发的粒度。是的,对于现实世界的问题,它们通常一开始就是并行的。所以决定这个粒度其实没什么问题。我的意思是,如果你要为一个一百万人构建一个 Web 服务器,你只需要创建一百万个进程,每个进程对应一个人。这是一种稍微不同的看待事物的方式:并发是问题固有的。所以你只需要在系统的架构中反映出它。
我提出的一个观点人们不太理解,那就是如果你用 C、C++ 或 Java 之类的语言编程,为一百万个客户端或一百万个会话构建一个 Web 服务器,那它本质上是一个单一的顺序程序,通过衍生线程和对共享内存做一些取巧的操作,被说服去处理一百万个连接。
而 Erlang 的看待方式是,它不是一个被一百万个客户端使用的进程,而是一百万个互不相干的并行进程,每个进程只处理一个会话。这样思考起来要容易得多。理想情况下,我们会把它们放在完全独立的机器上。如果我们把它们放在物理上分离的机器上,如果我们有一百万台机器,每台机器上放一个服务器,它就是内在可并行的。
所以,要让事物并行化,你需要让它们独立。如果它们是独立的,你就可以对它们进行水平扩展。所以,在你有这种大规模并行化的情况下,你无法让事情变得更快的原因是,你无法进行水平扩展。而你无法进行水平扩展的原因是,你有共享内存。如果你有纯粹的消息传递,你就可以做到。
所以,很多问题的粒度,以及你如何将事物分解成并行进程,可以通过观察现实世界的问题来完成。这里有一个,我不知道有没有人……有人看过这个视频吗?是的,很好。我们在 1992 年为 Erlang 制作了一个宣传视频。视频里有三个人,我、Robert 和 Mike。当时我们三个人在开发 Erlang。我说“你好,Robert”,然后 Robert 说“你好,Joe”,然后我说“你好,Mike”。
如果你想把这个转换成一些程序代码,你可以直接映射它。我们从这里开始,“你好 Robert”,所以给 Robert 发送(bang
)“你好 Robert”。Robert 接收到一个消息,然后他给 Joe 发送“你好 Joe”。Joe 在这里接收到一个消息,然后他说“你好 Mike”。我们是基于观察来做的。所以这里的代码和我们观察到的东西几乎是同构的。
所以,如果我们看着这张图然后问,我们需要多少个进程来模拟这个场景?答案是三个。有三个人参与,不会是五个,也不会是七个,肯定是三个。在很多问题中,将问题映射到并发结构上可以通过观察来完成。对。这就是 Carl Hewitt 所说的物理建模。这是 Hewitt 在描述 Actor 模型时提到的。所以,现实世界中的一个并行操作等于一个并行进程。非常简单,你只需坐在那里观察正在发生什么。
然后你做什么呢?Erlang 系统所做的就是把这个变成一个高效的程序。程序员做第一步:将问题描述为一组并发进程,这相当容易。然后,系统将这些进程分布到你可用的核心上,以完成这项工作。接着你进行测量。速度够快吗?是的,你很满意。如果速度不够快,你有两个选择:你可以手动将进程映射到核心上,因为你比任何其他东西都更了解它。或者你可以改变并发模型,也许引入更多或更少的并发。
那么,你如何将进程分布到核心上呢?嗯,这很棘手,非常非常困难。幸运的是,所有用 Erlang 或这类语言编程的人都不需要做这件事,因为在爱立信有两三个人知道怎么做。所以,与其让每个使用某种语言的程序员都必须做这件事,不如让一两个真正知道怎么做的人来做。如果你用 C++ 编程,地球上每一个 C++ 程序员都必须解决这个问题,这是一个极其困难的问题。而且他们很可能会做错。如果你用 Erlang 编程,Richard Green 和其他几个人知道怎么做。他们对自旋锁(spin locks)和天知道什么东西都了如指掌。
OTP,OTP。这代表开放电信平台(Open Telecoms Platform),它有点像是……你得到 Erlang 时附带的东西。它就像 Unix 和 C 之间的区别。你可以说 OTP 就像 Unix,而 Erlang 就像 C。所以 Erlang 程序员,他们对多核进程如何工作一无所知,他们毫无头绪。而那些编写调度器之类东西的人,地球上只有两三个人知道如何将这些进程分布到核心上。
现在,调度实际上是一个非常困难的问题。如果你听了 Christian 周一关于约束的演讲,这是一个 NP-hard 问题。如果你有……这是一个背包问题。如果你想把这些东西装进……我们这里有什么?在 8 个处理器上运行 10 个任务,有 3.7 x 10^16 种可能性。这是一个 NP-hard 问题。对于任何合理的……宇宙中都没有足够的时间来解决这些问题。这非常困难。
所以,最好的办法是根本不这样做。而是要确保你有很多很多小的进程。把巨大的石头装进容器里是非常非常困难的,但把沙子倒进容器里真的很容易。所以,如果你把进程想象成小小的沙粒,把处理器核心想象成你必须填满的大桶,那么用沙子填满你的桶,你可以把它们装得很好。你只要把沙子倒进去,一切都会搞定。而用巨大的石头来填充是行不通的。
而当我们用 C++ 或任何实际上使用 p-threads 的 Unix 系统编程时,就像是在装石头。因为这些都是庞然大物……它们是进程的最小尺寸。是多少来着?有人告诉我吗?100KB?64KB?差不多是这样?这些都是我们无法很好地装入内存的大石头,因为内存管理硬件的原因。如果你真的想要非常小的进程,你就可以很好地把它们装进去。
那么,我们如何让编写并行程序变得容易呢?
不要用一大堆不同的东西来迷惑程序员。只提供三种原语(primitives),它们存在于语言本身,而不是操作系统中。这三种原语是:
spawn
:创建一个并行进程。send
:向该进程发送一条消息。receive
:接收一条消息。
receive
是一个模式匹配操作。实际上,你不是把消息发送给进程本身,你可以把进程想象成一个有小邮箱的东西,把消息想象成邮件。send
操作就是邮递员发送消息并把它投进邮箱。而 receive
操作就是有人走到邮箱前,翻阅邮件,拿出他想要的,然后去做点什么。做完之后,他们回到自己的小邮箱,打开它,看看有没有什么事要做。
这一切都非常好,因为正在做这件事的进程是纯粹顺序的。而我们理解顺序程序要比理解并行程序好得多。
当我们实现这个时,Erlang 虚拟机会把它编译成单条指令。它最初是一个字节码解释器,现在是一个词解释器(word interpreter)。但这些都是单条指令。send
、spawn
、receive
在我最初的 Erlang 中都是单字节的。硬件厂商没有制造这些指令,他们制造了完全不同的指令:加载(loads)、存储(stores)、移动(moves)和测试(tests)。他们有所有这些我不想要的指令,而我想要的指令他们又没有。
所以,当你在设计一门高级语言时,你会说,“哦,我不在乎机器有什么指令,因为那些对我来说太复杂了,我理解不了。我会发明一个指令集,让我的高级语言更容易实现。” 例如,如果你看 JVM,它是一个简单的栈式机。它的架构和 .NET 机器几乎一样,就是一个简单的推入(push)和弹出(pop)的栈式机,非常容易理解。它们都源于 Viet’s P-code 机器。它就是一个带有推入和弹出的简单栈式机。然后你要做的就是在 C 程序中模拟它。像 Christian 和他那个漂亮的编译器项目那样的人,会研究如何把你想要的指令映射到你不想要的指令上,因为你不想要的那些指令是硬件厂商提供给你的,因为他们从不和高级编程语言设计师交流。如果他们交流的话,他们就会像 Charles Moore 那样,制造出带有你想要的指令的 Forth 芯片。
对。所以,Erlang 没有任何这些东西:它没有共享内存、信号量(semaphores)、互斥锁(mutexes)、监视器(monitors)、自旋锁(spin locks)、临界区(critical regions)、futures、线程安全(thread safety),以及所有这些极其难以搞定的可怕东西。它只是通过纯粹的消息传递让生活变得简单。我们只是发送消息,消息被完整地复制,没有指针之类的麻烦事。你要么得到整条消息,要么什么也得不到。
失败的语义和现实世界中一样。如果你给某人发了一条消息,它要么送达,要么没有,而你永远不会知道。这就像世界的行为方式。
所以,我们必须问自己,我们为什么要用纯粹的消息传递?哦,还有隔离(isolation)。这些东西也必须是隔离的。
好吧,实际上,现在有一种编程模型……我一直觉得有点疯狂的一件事是,我们根据系统的规模,用不同的方式来编程。所以,如果你在一个单核机器上,一台电脑上编程一个应用程序,你用一种方式编程。然后,随着你的问题变得越来越复杂,也许你超出了这台单机电脑的容量,你想在两三台电脑而不是一台电脑上编程,突然之间,整个编程模型就退化了。你编写程序的方式使得你无法在两台电脑上运行它。
所以,我们开始编写很多程序的方式是,首先,我们在一台电脑上写。也许那台电脑的容量不够了,我们就买一台越来越强大的电脑,不断地纵向扩展(scale it up)。然后有一天,突然我们再也无法在一台电脑上做到了,我们需要用两台电脑。但突然之间,我们无法在两台电脑上运行我们的程序了,因为我们编写它的方式决定了它只能在一台电脑上运行。
但如果我们从一开始就使用单台计算机上隔离组件之间的消息传递来编写它,那么当我们把它部署到多台计算机上时,它就能很好地扩展。所以,我真的不想用不同的方式来编程这些不同的东西。我们不想……这里有一个,你展示了这个非常漂亮的图表。我们编程的方式……
写一个小程序,然后考虑为了把它变成一个大程序我必须做的架构上的事情,我建议我们应该从写一个大程序开始,然后把它缩小到小程序的情况。
所以,想象一下你要写一个类似 web 的服务,或者一个物联网(Internet of Things)的东西,或者这类东西。会有两种做法。一种是你可以说,“嗯,我应该为这个系统规划多大的规模?” 你可能会说,“100 个用户。” 于是你为 100 个用户制作了它。然后你部署了这个东西,结果发现你有 1000 个用户。于是你必须开始扩展这个东西,这可能非常困难。
我建议,更好的做法是从一开始就问,“我们最多能有多少用户?” 嗯,我不知道。我通常会说,从 10 的 50 次方个用户开始,因为地球上有 10 的 50 次方个原子。我们说,“好吧,那么我们假设每个原子都想要自己的 web 服务器,或者别的什么。” 于是我们让地址空间能为 10 的……或者你可以为整个宇宙做规划,但那太傻了。你从某个不合理的巨大数字开始,然后让你的架构能为此工作。然后它就会……然后你把它缩小到你的 10 个用户,它仍然能为 10 个用户工作,因为你已经把它规划为能为 10 的 50 次方个用户工作了。你把它缩小到 10 个用户,它运行得非常好。当然,这会比那些从问题开始就说“我要做一个能处理 10 个用户的系统”的人效率低一些。
哦,糟糕,我不小心按到按钮了。天哪,抱歉。
所以,当你展示这些东西时,Erlang 的东西,与 C++ 的东西相比,人们的第一反应会是,“啊,是的,但我的 C++ 程序比 Erlang 的快 5 倍,或 10 倍,或 30 倍。” 我会说,“是的,但你只是在一个未经扩展的领域里这样做。当你进入这个大的领域时,你会看到相反的效果。” 这似乎是真的。基本上,因为你无法用 C++ 构建非常大的系统,因为你必须开始解决我们在 Erlang 中已经解决了的同样问题。
例如,如果你部署一个拥有数十亿用户、运行时间很长的系统,要保持它的一致性是不可能的。你不能通过停止系统、更改所有软件然后重启的方式来更改软件。你必须接受演进,必须接受错误。错误会发生。我们正在进入一个时代,我们将有发光二极管相互发送消息,Wi-Fi 速度达到每秒 10、20 吉比特。每个灯泡里都会有一个处理器,其计算能力可能相当于 100 台 Cray-1 超级计算机,我们将构建这个巨大的、超强的消息传递超级计算机,它可以做我们想做的任何事情。然后我们只需要想出一些有用的事情来做。实际上没人能想出任何有用的事情来用它做,所以我们可以远程控制一个水壶之类的。但我们拥有了这个奇妙的结构。我们希望用同样的方式,一种可扩展的方式来编程它。
通过纯粹的消息传递,我们可以处理失败。我稍后会更多地谈论这个,因为我有一些关于这个的单独幻灯片。
具体细节。最初的 Erlang 是如何工作的?大约在,哦,很久以前,大约 1989 年左右,我们做出了一个好的设计决定,那就是每个进程都应该有自己的栈和堆,不应该有任何共享的东西,没有共享的垃圾回收,没有任何共享的东西。
这被证明是一个非常好的决定。奇怪的是,当时正在开发 Erlang 的 Robert 和我一直相信,总有一天会有人带着一个需要共享内存的应用来找我们。当那一天到来时我们会怎么做呢?
嗯,如果他们来找我们说,“嗯,我们真的需要共享内存,因为它不够高效”,那么我们的策略是,我们会用我们的方式重写它,并希望它能工作。如果它不工作,那我们就投降,做一些共享内存。但我们从未这样做过,那一天从未发生。哦,嗯,那也不完全对。Erlang 有 ETS 表,用于构建大型数据库。因为如果你有一个代表整个宇宙的数据库,并且你在里面改变了某些东西,你并不真的想复制整个宇宙。嗯,你不能再造一个……你知道,宇宙就是一切,所以你不能有两个……嗯,除非你相信膜宇宙(brane universes),但我们反正也无法访问它们。
所以,这样做的最初原因与可扩展性无关,也与性能无关,它与容错(fault tolerance)有关。共享内存的问题不在于……Peter 提出问题在于证明或演示当你进入一个临界区时会发生什么。我对共享内存的问题,与其说你无法正确地锁定东西并进入临界区,不如说是如果一个程序在临界区内崩溃了会发生什么。因为它以某种方式改变了内存,然后它崩溃了。嗯,当然,一个监视器(monitor)可以检测到这个事实,并且它可以告诉其他进程。好吧,现在它从临界区被移除了。第二个进程过来,说,“哦,是的,但是那个进程发生了什么?” 嗯,像 Eric 这样的人会说,“哦,我们有事务内存(transaction memories)来解决这个问题。” 哦,太好了。那会解决你的问题。是的,但它不能在规模上解决问题,它只能在局部解决。所以当我在瑞典的东西和在美国的东西通信时,我仍然有那个规模上的问题。我该把共享内存放在哪里?我是把它放在大西洋中间的一艘潜艇上吗?它不存在。那是两个不同的内存在相互通信。
所以,失败。处理失败是困难的。你必须问自己,我如何编写容错软件?如果整台计算机都崩溃了会发生什么? 我不是在说一个线程里的除零错误,我说的是整台计算机崩溃。嗯,你需要两台计算机,或者多于两台,如果你想做到非常容错的话。所以,如果你只用两台计算机,让一台在另一台失败时接管,等等,它们可以互相修复错误。
如果你想一下,如果存在共享内存,这是不可能的。如果远程机器上有悬空指针,你就做不到。但如果你有纯粹的消息传递,你只需通过消息把所有东西都发送过去,你就可以处理失败。你只需确保你发送到其他机器上的东西足以在发生故障时进行恢复,并且你要监控正在发生的事情。
SMP Erlang。这是稍后出现的。现在我们实际上有了物理上分离的核心。所以,基本上,在单核上运行的相同模型现在运行在多核类型的架构上,即并行处理器架构。有一些特殊的东西叫做调度器(schedulers),它们的工作是定期在核心之间移动进程。所以它们会时不时地中断系统,移动进程,并调整这些东西。希望,如果你做得好,你只花少量的时间在……哦,我需要站在这里指。你只花少量的时间在这里(调度),而大量的时间在这里(执行)。你监控正在发生的事情并移动东西。
目标,实际上……哦,在后面一张幻灯片里。但在 Erlang 系统中的目标是,如果你用合理的进程平衡编写了你的应用程序,并且你有 n 个核心,那么它应该在你什么都不做的情况下加速 0.75 倍 n。所以,这意味着如果你有一台 100 核的计算机,希望它能在你不对程序做任何改动的情况下,快 75 倍。这几乎等同于说,好吧,假设 25% 的工作将在这个可怕的部分完成,也就是你实际决定移动东西的地方。
还记得我之前那张幻灯片说的你可以固定(pin)进程吗?那就是你说,“嗯,因为我知道我的问题是如何工作的,我实际上会把这些东西固定在这个核心上,我不会用调度器来移动它们,我会告诉它。” 有人推测,虽然尚未得到证实……在一些片上网络(network-on-chip)架构中,做一些像把 TCP 相关的事情放在芯片边缘,然后把数据库放在芯片中间之类的事情是明智的,然后把它们移动并实际固定到正确的核上。因为存在内存问题和传播延迟,你想要穿过核心等等。
但这样做并不是一个好主意,因为那样的话……通过把东西固定在核心上,你并没有让你的软件面向未来(future-proof)。所以当一个新的架构出现时,你可以忘掉那些,你将不得不重做它。所以最好还是让系统为你做这件事。
对。所以 Erlang 只是……这里有一些 CPU,五个 CPU。每个都有自己的内存,这些彩色区域是它们的共享内存,然后有一些锁之类的树状结构。我不会用细节来烦你们,但它们挺复杂的。
那么这个迁移逻辑是做什么的呢?它做负载均衡、进程迁移、调度进程间消息传递。所有这些的目标是制作一个可以移动东西的小核心,并且尽可能地做到非锁定(non-locking)。
所以,一旦我们将计算隔离开来,把它们放在独立的核心上,独立的物理计算机上,我们就能得到四个额外的好处。
第一个好处是可靠性(reliability)。因为如果一台计算机在给定时间内的故障概率是 10 的负 3 次方,那么如果你有两台,它们在同一时间间隔内都发生故障的概率就是 10 的负 6 次方。所以如果你想要“九个九”(99.9999999%)的可靠性,你只需要 34 台计算机,让它们全部并行运行。这真的很容易。但这一切的关键是独立性。你真的需要有完全隔离的计算。实际上,在那个级别的可靠性下,你还需要隔离的电源和其他一切,因为地球的电源供应不会达到那么高的可靠性,它们可能每百万年就会出故障。
可扩展性(scalability)。如果所有进程都是独立的,我们可以很容易地进行水平扩展。我们可以用它来解决大规模并行问题。现在,大规模并行问题极其普遍。万维网、互联网,就是一个大规模并行问题。它的本质就是有 20 亿人在互相交谈。爱立信说将会有 500 亿个连接设备都在互相聊天。所以我们确实有 500 亿个东西在同一时间都在进行。所以它已经是并行的,它已经是分布式的,并且它已经在发送消息了。愚蠢的是,我们正在用语义上与此完全不同的语言来编程。这根本不是个好主意,这使得事情被人为地变得困难。
我们还通过隔离事物和使用消息传递,使它们变得更简单(simpler)。因为理解交换消息的小东西比理解不交换消息的大东西要容易。原因很简单。如果你有一个黑盒子,你想理解黑盒子里发生了什么,你所需要做的就是观察输入和输出,把它们确定下来,然后写下一些方程和关于黑盒子里发生的事情的推理。在所有计算机科学的原则中,我认为我最喜欢的是这个观测等价性(observational equivalence)原则。如果从观察进出消息的角度无法区分两个系统,那么它们就是等价的。你如何编程黑盒子的内部并不重要,你可以用 Fortran、COBOL、JavaScript、C++ 或任何其他语言来编程。只要它的行为方式相同,就完全无关紧要。这非常好,因为描述接口上发生的事情比描述内部发生的事情要容易得多。
当然,如果我们打开这个东西,它应该是组合式的(compositional)。对。所以在这个黑盒子里面,我们应该能找到更多的黑盒子。我们会递归地描述这个系统,它就变得可组合了。我们可以立即看到,如果我们想用硬件或软件来做,在每个抽象层次上,都是在说,“好吧,在这个抽象层次上,我需要三个并行进程来建模这个东西并编写程序。”
现在所有这些东西都只是通过消息进行通信。这实际上就是我们构建硬件的方式。我们拿到芯片,它们都并行运行,它们都有时钟,都并行运行。我们把它们连接起来,一切就都工作了,非常棒。
这么做的另一个好处,发送消息之类的,是我们遵守了物理定律。我曾经是个物理学家,所以我喜欢这个。我说我们遵守物理定律,那么我们遵守了哪些物理定律呢?
第一条是,消息的传播速度等于或小于光速,除非我们相信量子智能……哦,昨天有人做贝尔实验了吗?我做了。没有?没人?都睡着了?嗯,那很好玩。那是一个关于贝尔假说的大规模实验,看看量子纠缠是否有效。昨天在世界各地都进行了。我参与了,通过在我的电脑上随机敲击 0 和 1。
因果性(Causality)。这是一个很好的原则。如果 B 的状态依赖于 A,并且 A 和 B 在空间上是分离的,那么你必须先向 B 发送一条消息,B 才能做任何事情。在你得到一些已经改变的信息之前,你什么也做不了。
另一条物理定律是,在宇宙中两个不同地方的同时性是不可能的。我们只有在时空的同一点上才能谈论同时发生的事情。关于这一点有很多混淆。
大多数问题在于……我想说,要进行计算,程序和数据必须在时空的同一点上。对于不同的时空点,你无法进行计算。所以你要么把数据移到程序那里,要么把程序移到数据那里,或者把它们都移到中间某个地方。我们传统上、习惯上做的是把数据移到程序那里,这就是为什么我们有巨大的服务器集群。我们不把程序移到数据那里。那很蠢,因为所有这些都消耗能量。把数 GB 的数据发送到数据中心进行分析实际上是很傻的,而分析它的程序可能只有几 KB。把程序发送到数据那里要明智得多。
也许更明智的是把它们俩都送到中间某个地方,以优化延迟或安全性或任何你想要的东西。把数据发送到某人的服务器的另一个缺点是,你必须向不仅是分析它的人,而且向世界上所有的安全机构透露你的数据,他们会同时快速地窥探一下,看看它是什么。但如果他们把分析程序发给你,你就不需要向任何人透露你的数据。所以,把你的数据留给自己,向服务器请求程序,而不是反过来,要明智得多。这也避免了被苹果、谷歌和所有这些公司敲诈的问题,他们会把你所有的照片都存储在云端的某个地方,然后每年向你收取一百美元,以免删除它们。
当然还有消息传递。哦,我应该说,我们之所以必须强调这一点,是因为人们不理解。我们并不真正了解事物现在的样子,我们只了解上一次有人告诉我们时它们的样子。所以我不知道我的妻子是否健康,身体状况良好,但上次我见到她时她是。我的意思是,她可能已经死了,我不知道。我生活在她现在很快乐地在银行工作的幻觉中。但她当然可能已经死了。但既然我不知道这个事实,我就很开心。我不认为她死了。如果我接到一个电话,我就得非常迅速地离开这个讲座。
当然还有 Alan Kay,他创造了面向对象编程(object-oriented programming)这个术语。我喜欢这个。几周前我和他谈过,我采访了他,我们非常一致地认为,面向对象编程的核心思想是消息传递(messaging)。它不是关于抽象数据类型、类、方法和所有这些可怕的结构,它是关于消息传递的。而像 Java 和 C++ 这样的面向对象语言唯一不做的事情就是消息传递,这相当奇怪。这就是为什么我告诉人们 Erlang 是唯一的面向对象语言,因为它正确地实现了消息传递。
对。所有这些都管用吗?是的。嗯,我的意思是,这是个好问题。是的。所以当我在开发 Erlang 时,你会说,“嗯,你是否需要改变最初的设计来让你的程序跑得更快?” 如果他们说“不,它就是变快了”,那么我就会说它管用。
我参加过一次会议,我们有一台 Tilera 64 计算机,一台 64 核的机器,在上面运行了,我记不清是什么应用了,SIP-SAC 之类的。我们开了这个会,一个学生做了这个东西,它在一台 64 核的机器上快了 33 倍,而他们根本没有改动软件。管理层对此并不太高兴。他们说,“为什么?为什么它没有快 64 倍?你有一台 64 核的机器。” 然后我说,“嗯,你们是为它快了 33 倍而高兴,还是为它没有快 64 倍而不高兴?” 他们说,“嗯,我们为它没有快 64 倍而不高兴。” 然后我说,“嗯,提醒我一下,那个用 C++ 做的项目,它快了多少?” 结果它还停留在 1 倍,根本没有变快。所以我认为我们还是实现了一个小目标的。而我们的目标,我之前说过了,是达到 0.75 倍的速度提升。如果你有正确的并发模型,并且用合理的方式编写了你的程序,但这个责任在程序员身上。没有任何自动化系统能为你做到这一点,你必须审视它,弄清楚你需要什么样的并发。
所有这些都管用吗?是的,嗯,它对像 WhatsApp、瑞典的 Klarna、爱立信这样的东西管用。我刚刚补充一下,我上周很幸运地被邀请到中国,并了解到他们说的是世界上最大的网站阿里巴巴,使用了很多 Erlang 来实现。我还去了一家叫“环信”(EaseMob)的公司,它有点像中国的 WhatsApp。当他们邀请我去那里时,我非常惊讶。是的,它对那种东西管用。
我的意思是,我知道你几年前的演讲,你知道,像通过各种巧妙的调整和计算时隙来求逆矩阵的超级方法。它对那种东西不管用。但大多数人不想……嗯,我不知道。我本想说大多数程序员不想求逆矩阵之类的。但无论如何,如果他们想快速地做这些,他们会用一个库来做。但他们想写这些有趣的 web 应用。
它能很好地扩展吗?嗯,这是 WhatsApp。这是一个在,哦,我不记得是哪一年的演讲中展示的,上面写着 2012 年。它展示了 WhatsApp 的扩展情况。WhatsApp 是用……或者说服务器是用 Erlang 写的。他们对此相当满意,扩展得很好。
然后上周我了解到,这个来自环信的人展示了一张幻灯片,因为我在北京参加了一个 Erlang 下午茶活动。我当时在那儿。这是一个我从未听说过的公司。我想他们认为自己是中国的 WhatsApp。但他们不向最终用户提供服务,他们做的是这个:他们提供一个用 Erlang 编写的平台,用来连接各种应用。有付费服务和免费层级。在中国,有 82,149 个应用是使用这个后端编写的。我当时想,不,是 89,000 个用户。不,是 89,000 个应用。而这些应用服务于超过十亿的用户。所以他们有点像是在 WhatsApp 那个领域,并且扩展得相当好。
所以在这个阶段,我想我们应该说,嗯,你知道,我们未来要去哪里,我们应该解决什么问题?Alan Kay 说我们应该把所有这些东西都扔掉,从头再来。我想如果我们这样做,我们就不会犯我们过去犯过的错误。嗯,你没说的一件事是,你知道,为什么不干脆把所有这些东西都扔掉,然后把它做好呢?嗯,我们无法把它做好,但我们可以再次尝试这样做。
我们现在到处都有超级计算机,而我们正在用可能充满 bug 和不正确的遗留软件来压垮它们。我们对它做什么或如何工作一无所知。它是用没人能理解的规范编写的,完全是一团糟。
那么,大的问题是什么呢?
我们想要最小化能源消耗。
我们想要永久存储东西。嗯,不是永远,直到太阳变成红巨星。我们当然不想失去我们的历史,通过把所有东西都放到云上。如果在两三百年后我们回过头来说,“嗯,所有这些东西都在哪里?” 然后他们说,“嗯,抱歉,我们把它放在云上了”,那将是相当悲惨的。
我想起了 Peter 的回答。我遇到他了。我当时要去 Facebook 做个演讲,我遇到了 Facebook 的一位高层技术人员,我想是技术主管。我说,“那么,我们死后,我们拍的所有照片和东西会怎么样?你知道,200 年后人们……我们的亲戚之类的要怎么找到这些东西?” 他说,“这是个非常好的问题。” 所以他们实际上没有答案。所以我有点担心我们会失去所有的历史。所以我们必须想出一种方法来把它存储一段时间。
安全。我们需要想出如何使这些系统安全。目前的系统非常脆弱。如果人们……它不是……它是可攻击的。如果攻击速度高于修复速度,我们可能会摧毁整个互联网。所以这些人已经在灯泡、冰箱、熨斗和喂狗设备中安装了木马。一旦他们控制了那些东西,我不知道你是否还能夺回控制权,因为对于非常便宜的设备,似乎没有升级机制。所以一旦这些东西被劫持,它们就永远被劫持了。如果他们能放入新的东西并攻击得比我们清除这些东西更快,我们可能会潜在地摧毁整个互联网,然后你将不得不关闭它的整个部分并进行隔离。那就像……就像黑死病一样。它会导致数百万人死亡,而唯一的办法就是隔离它,然后从头开始重建一些东西。这实际上是一个真实的问题。
我们需要想出如何制造能够演进的系统。
我们需要隐私。
然后,因为所有这些东西都在取代工作岗位,我们需要考虑到我们可以用计算机而不是人来做很多事情的经济模型。
对。那么我们想要什么?我希望硬件厂商做什么?
我语言中的那些单字节或单指令的东西,我希望他们能在硬件中实现。所以我想要异步消息解析。我希望所有消息的解析都在硬件中完成。大部分时间都花在这上面了。只要放一些 FPGA 或一些 VLSI 来解析 JSON 或 XML 或你发送的任何垃圾。根本不要在软件中做。在硬件中做消息队列。在硬件中做故障检测。在硬件中做一些安全数据锁。在硬件中给我加密校验和。以及带有少量指令的小型 CPU。我不需要 10 条指令,我不需要很多。我实际上需要一些内存,几 MB。而且我肯定 Peter 会很高兴,如果我们只有带 10 条指令的机器,那会让每个人的生活都容易得多。我们想要低功耗。
我们想要会话类型(session types)之类的东西来描述正在发生的事情的边界。我们需要演进机制,这样我们就可以永远运行系统,它们会不断成长,我们可以随时间改变它们。我们需要,随着它们演进,伴随着安全性,我们需要使所有这些都安全。
所以我想至少还有五分钟的提问时间。我想这个,我想 Eric 的意思是五分钟。
那么,有什么问题吗?
我知道那里有一个,但我们有来自现场的吗?
你好。Peter。
Peter 的问题:消息传递的抽象是一个非常有吸引力的、非常简洁的模型。但我一直很困惑,我们的系统实际上最终变成了一堆交替的层次结构。我们有硬件,它是用消息传递互连实现的,架构师在上面构建了共享内存抽象,然后你又在上面构建消息传递抽象,想必其他人又在上面构建了分布式共享内存抽象。所以我看不出这两者中哪一个更基础。所以,如果你在一个消息传递的基础上构建这些大型系统,我想问你,你是否实际上把一大堆复杂性推给了分布式协议?我写过一些相当复杂的分布式协议,它们仅仅因为是用消息写的就变得容易理解,这可不一定。而且它们的失败行为也不一定容易理解。那么,当人们在做这些大事时,他们最终实际上构建了什么?
Joe Armstrong 的回答:我认为基于消息传递的东西的问题是,是的,你在某种程度上确实想要共享内存。然后你就需要在一两个你需要在其上构建的基础抽象上。例如,领导者选举(leadership election)就是这些关键抽象之一。这是一个极其难以理解、实现和确保正确的东西。我们甚至不知道它们是否正确。所以,我认为,最好是把那个问题交给一些理论家,说,“好吧,你们能好心为我们证明这个算法是正确的吗?” 那将成为我们用来构建东西的小积木之一。
然后我会对人们说,如果可能的话,你能不能重新架构你的系统,让你不需要,例如,领导者选举或全局内存?通常你可以重新架构你的问题,使得你不需要那样做。所以,举个例子,如果我的银行账户细节保存在我自己这里,而不是银行,那就会容易得多。正是因为银行在做这件事,而我可以通过不同的渠道访问它,才使得它不安全。所以,如果你能改变架构……你知道,这有点像黑魔法。魔术师做了些什么,挥了挥手,稍微改变了问题,使之成为一个可处理的问题。但是,是的,我的意思是……
主持人追问:对于这些大规模分布式系统,我听过谷歌的人的演讲,描述了他们为了回答一个网络查询而有多少种不同的服务在交互。所以如果他们用 Erlang 来构建那个,你认为会特别简单吗?
Joe Armstrong 的回答:不,不,不。我不这么认为。但是你看,我认为如果……Carl Hewitt 举了一个很好的例子。他说,“嗯,如果你有中央服务器,然后你问一些事情,你会向它们泄露很多关于你自己的信息。” 所以如果你去……假设我想订一架飞机,或者,你知道,我要去度假。我想订一辆租车、一家酒店和一架飞机。我去了某个预订网站说,我想订这架飞机。问题是每个人都在监视我,听着这个,广告服务就进来了。所以当我再去尝试买一辆租车时,他们已经知道我这么做了,然后他们会据此来调整他们的东西。因为当你进行计算时,你一直在泄露东西。
所以 Carl Hewitt 说,最好是把所有数据都留给自己,然后说,如果你想订一架飞机,你可以去那 10 家你愿意飞的航空公司说,“你们能给我一个小程序吗?我可以在我的电脑上运行,它的有效期是半小时或 10 分钟。” 你可以在本地运行那个。然后你可以告诉他们,你可以得到租车,你可以得到酒店,你可以做所有这些。你不会泄露任何数据,没人知道你在做什么,你只是在请求程序。
所以我想,如果你把所有的数据都留给自己——这也是你在你的讲座中提出的观点——如果你有这个所有数据所在的单点,那就容易多了,而那个单点应该是你。它应该在你的手表里或者某个地方。你可以把所有你的个人数据都放在那里。你不应该把它放进……你不应该让苹果或谷歌或任何其他人拥有它。你也不应该让他们为你提供所有的服务。你应该定义数据是什么,然后你应该让他们……交互的问题仍然存在,它不会消失。那些本质上是同步问题,但也许我们应该让一些其他的……
主持人:是的,我们开始进入宗教问题了。屏幕上有几个问题,左边那个。所以,如果你把 Erlang 放在一个单一的共享内存机器,Linux 机器上,你仍然有什么优势?最明显的优势是什么?
Joe Armstrong 的回答:编程更容易。是的,但是失败(的處理方式)是一样的。是的,是的。然后你可以开发你的……我的意思是,开发程序的好方法是,你在一台机器上开发,然后你只需把它部署到多台机器上。它会有相同的语义,只有消息传递的时间或延迟会改变,但其他一切都会保持不变。
主持人:你对企业总线架构(enterprise bus architecture)有什么看法?它是否以同样的方式工作?
Joe Armstrong 的回答:我认为那更多的是一个营销术语,而不是别的什么。我从未真正理解过企业总线架构是什么。他们展示的……他们展示的框图就像 Peter 的第一个图表一样,我从未真正理解过它们是什么。而且我从未真正用过企业总线架构,所以我真的不知道。
主持人提问:我有个问题。我知道,你知道,你在 80 年代中期开始……(此处应为口误,原意可能是“开始做这个项目”)……它花了很长时间,真的花了很长时间,Erlang 才开始在团队之外被采用。那是一段很大的挣扎,有时相当令人沮丧。最大的转折点是什么?为什么人们突然开始在外部使用 Erlang?是因为……这发生在多核之前,不是多核促成的。
Joe Armstrong 的回答:让它在外部开始的原因是爱立信禁止了它,因为那导致它变成了开源。当它变成开源后,它就开始传播了。所以那是它开始传播的主要原因。然后它就年复一年地慢慢发展起来。
通常,是一些像 WhatsApp 这样的东西。我的意思是,WhatsApp 被收购……它是由 12 个没有经验的程序员用 Erlang 写的,他们以 190 亿美元的价格卖掉了那家公司。那带来了很多正面的宣传。有一些报纸文章说,自由职业的 Erlang 程序员是硅谷薪水最高的程序员。正是那种东西导致它传播。所以是正面的宣传之类的。这一直有点像一场艰苦的斗争,因为你必须用稍微不同的方式思考。你知道,它有不同的语法,而语法让很多人担心。但是,当然,多核也对采用有很大帮助。
主持人追问:因为你这里谈了很多关于可扩展性的问题,对吧?
Joe Armstrong 的回答:我不知道它是否真的有帮助。我的意思是,所有这些都是同时发生的,所以你不能真正指向任何……我认为更多的是……水平……我的意思是,我们可以在没有多核的情况下进行水平扩展。我认为最初有益的是那种可扩展性。我的意思是,WhatsApp 使用了数千台机器进行水平扩展。正是他们能以一种方便的方式构建的这种水平结构吸引了他们。也许不是多核本身那么多。
主持人:让我们回答墙上的最后一个问题。Erlang 程序员需要管理饥饿(starvation)问题吗?他们是否可能造成饥饿或死锁(deadlock)问题?
Joe Armstrong 的回答:令人惊讶的是,我的意思是,每个人都说,“嗯,你能证明没有死锁或活锁(livelock)之类的吗?” 不,我们当然不能。它在实践中经常发生吗?我会说极其罕见。这很令人惊讶。在实践中,它发生的频率惊人地低。我认为原因之一是,那些库,有一些实现了服务器、决策树和监督树(supervision trees)之类的通用库,它们的编写方式使得这种情况不会发生在它们身上。它们不会……你不会用它们得到死锁之类的。
我的意思是,理论上它可能发生。实践中它不常发生。而且通常,如果它真的发生了,情况会很明显,你可以很快发现并修复它。
主持人:我想我们将从提问环节过渡到午餐。非常感谢。
Joe Armstrong 的回答:谢谢。