《Elixir in Action》是由Manning 所出版的一本新书,本书为读者介绍了 Elixir 这门语言以及 Erlang 虚拟机,同时也讨论了与并发编程、容错以及与高可用性相关的话题。InfoQ 有幸与本书的作者 Saša Jurić进行了一次访谈。
《Elixir in Action》的内容源自于 Jurić在 Erlang 方面的经验,他为此特意创建了一个博客,为来自面向对象背景的程序员展现Erlang 的优势。Jurić之后转而使用Elixir,这是一种函数式的并发编程语言,它的目标是提供一种比起以Prolog 为启发创建的Erlang 更简便的语法,以及更高等的抽象能力。
本书的内容采取了一种渐进式的方式,首先介绍了Elixir 的语法与它的基本特性,例如宏、模式匹配、模块、多态等等,随后介绍了如何创建一个容错的、高可用的、并发的分布式系统。在全书的一些核心章节中涵盖了Erlang 平台的大量知识,其中的主题包括管理进程、持久化、通过supervision 树进行运行时错误处理的多种方式,以及“任其崩溃”(let it crash)等设计哲学。
总的来说,《Elixir in Action》一书不仅为如何使用Elixir 语言与Erlang VM,同时也为读者如何迈进高可用性系统这一领域打下了坚实的基础。InfoQ 与Saša Jurić进行了一次访谈,以了解这本书背后的更多知识。
InfoQ:是什么原因促使你编写了本书,它与现有的一些针对 Elixir的书籍又有何不同之处呢?
Saša:《Elixir in Action》的目标是帮助那些具有编程经验,但缺乏 Erlang、Elixir 或函数式编程背景知识的程序员开发能够在生产环境中运行的系统。在这本书的构思阶段,我就假设本书的大部分读者都来自于面向对象语言的背景。对于这些读者来说,书中的许多内容都是新颖的:新的语言、运行时与生态系统,包括函数式编程与类似于 Actor 的并发编程思想。读者需要打下大量的知识基础,而这可能会令人望而生畏惧。
我相信,通过一种更为专注的方式,初学者的学习过程将能够得到极大的简化。因此,我并不打算编写一本完整的参考书,而是决定专注于那些对于多数目标读者来说有些不寻常的核心概念:函数式编程、并发,以及 OTP 框架的核心思想。我相信一旦读者开始对这些主题感觉到“心意相通”,他们在学习本书并未叙述的一些内容时就会更容易。举例来说,如果读者掌握了 Erlang 中的并发知识,并且理解了大多数最重要的 OTP 概念(GenServer、Supervisor、Application),那么他们就能够较容易地自行学习其它的抽象概念,例如 Task、Agent 或 GenEvent。
我相信,这是目前唯一一本以这种方式进行撰写的 Elixir 书籍。任何一位想要使用 Elixir/Erlang 技术打造可伸缩的、高容错性的分布式系统,都必须学习 Elixir in Action 一书中所介绍的各种材料。当然,你也可以通过其它资源学习这些内容,但我认为,本书是目前唯一一本面向 Elixir 介绍以上所有主题的书籍。
InfoQ:你能否简单地介绍一下你在 Elixir方面的经验?它对于你实现工作目标提供了哪些程度的帮助?
Saša:对我来说,Elixir 最重要的一方面实际上是 Erlang VM,这是一切功能的基础,这也是我首次接触 Erlang 时对于我帮助最大的内容。大约 5 年以前,我当时需要实现一个基于长轮询的服务器推,它需要不断地将频繁变化的数据传输给数以千计的连接用户。经过一段时间的评估之后,我们最终选择了 Erlang,我从中也收获了许多经验。Erlang 以一种结构化的方式帮助我克服了重重阻碍:使用它创建解决方案的过程非常顺利,即使我对它的开发平台还有些陌生。最终设计出的系统具备了高伸缩性、高效性以及灵活性。我能强烈地感觉到 Erlang 在支持着我,即使我犯下了各种错误。这套系统能够处理各种预料之外的状况,而我甚至还没有在这方面做过什么计划。
InfoQ:Elixir特别适合于哪些类型的项目?
Saša:在我看来,Elixir/Erlang 适合于任何类型的服务端系统:即需要持续运行,并且尽可能持续提供服务的软件。这方面一个很明显的示例就是基于 web 的系统,用于处理传入的 HTTP 请求,但也需要进行其它活动,例如后台的、周期性的作业或缓存管理。在这种系统中,许多活动在某个具体时间往往处于挂起状态。而 Erlang 应对这种并发情况的途径让开发者的担子轻了许多。如果每个独立的活动都能够分配至少一个独立的 Erlang 进程(不要与 OS 的进程混淆了),我们就不仅能够实现可伸缩性,还能够改善容错性:一个单一的进程故障不会对整个系统的绝大部分产生影响。并且还有许多方法能够检测到这种故障,然后从故障中恢复。
我发现这种处理方式非常直观,并且任意一种类型的后端系统都能够从中受益。我看到某些观点说 Erlang 只适合于大规模系统,或某些特定的领域,例如电信领域。我不同意这种观点。Erlang 能够帮助系统实现大规模化,而这种特质在任何类型的生产系统中都是必要的,无论其规模与领域如何。因为如果某个系统做不到高可用 ,那么它就会频繁地产生故障。即使你不需要实现传说中的 9 个 9 的可用性,但你也应当希望让你的系统停机时间尽量减短吧。实现这一点是一个艰难的挑战,而 Erlang 则能够帮助你实现这个目标。
InfoQ**:你怎样描述 Elixir与 Erlang两者之间的关系呢?**
Saša:我的看法是,Elixir 是在 Erlang 与 OTP 所提供的强大基础之上所扩展的能力,旨在提升开发者的生产力。我曾经进行过大量的全职 Erlang 开发工作,虽然我十分热爱这门语言,但有许多任务也显得过于繁琐,我不得不无谓地处理一些低层次的机械性工作。而 Elixir 在这方面提供了大量实用的特性,包括语言(例如通过宏进行元编程,以及通过协议实现多态)与工具(例如构建项目的多种工具搭配,以及十六进制包管理器)两方面,这让我们能够专注于处理一些更为实际的问题。
我个人的感受是使用 Elixir 进行开发比起使用纯粹的 Erlang 进行开发要简单许多。Elixir 使可伸缩性与开发者的生产力之间这种刻意的权衡大大降低了,甚至是完全消除了。仅仅因为某个开发平台允许我们创建高并发、可伸缩、高容错的分布式系统,并不代表它就应当难以使用。同时,Elixir 的运行时并没有彻底远离 Erlang 的哲学。作为一种函数式语言,它的语义与 Erlang 非常接近。Elixir 能够无缝地集成各种 Erlang 库,因此开发者能够完整地访问整个 Erlang 生态系统。
InfoQ**:在决定直接使用 Erlang或 Elixir时,这两者有哪些缺陷是开发者需要考虑进去的?**
Saša:我不认为它们有任何的缺陷。它们所具有的优势主要来自于 VM 本身,以及已经过充分测试的 OTP 框架,而你可以从其中任何一门语言中收获这些益处。因此主要的决定因素在于其它的一些附加价值。Elixir 加入了一些额外的特性,因此实际上它是一门比起 Erlang 更加复杂的语言。而这门语言的优势在于它的代码更加简洁,在样板代码方面的负担较少。与之相比,Erlang 是一门更为简单的语言,因此所涉及的代码更多,但它也因而显得更为明确。
从我个人来看,Elixir 在样板代码的减少与语言的明确性方面找到了一个很好的平衡点。它没有 Ruby 等语言表面上那么神奇,而仍然提供了各种实用的特性,其中最值得留意的就是元编程与多态性。
InfoQ:创建一个高可用、高容错的并发系统是一项复杂的任务,尤其是从 CAP定律的角度来看。要实现这一目标,选择一门合适的语言与运行时环境的重要性有多高?
Saša:这实际上取决于个人的观点。人们在各种语言上都实现过大规模的系统,因此不用 Erlang 也是完全可能的。不过对我来说,问题不仅仅在于是否可能,还在于某个工具能够在多大程度上帮助我们完成这一过程。毕竟,工具的目的就是为我们提供服务。
而这也是为什么我很看重 Erlang 的一个原因。在我看来,它为系统化地处理编写高可用系统所面临的挑战提供了简单而又非常强大的构建块。它的主要工具是 Erlang 进程,它让我们能够将工作分解为几千个,乃至上百万个独立的部分。通过使用多个进程,我们就获得了可伸缩性与容错性。它的崩溃传递机制能够让我们有机会处理这些故障:如果某个部分崩溃了,系统中的其它部分会收到它的通知,并进行相应的处理。最后,无共享并发机制能够实现分布式系统,即使在一台独立的机器上也不例外。其实在本质上,通过将整个工作分解为大量隔离的、完全独立的实体(进程),我们已经实现了分布式工作。当然,将系统在多台机器上实现集群仍然不是一件简单的事,毕竟分布式系统在本质上就存在着复杂性。但至少 Erlang 已经为我们解决了一些低层次的工作,我们可以始终利用相同的基元功能实现协作,即进程与消息传递。因此我们就能够专注于业务上内在的挑战,而不是将大量的精力消耗在低层次的细节上。
总的来说,我认为 Erlang 能够简化实现高可用性的挑战。你也可以使用 Erlang 以外的技术应对这些挑战,但很可能会因此付出更多的努力。
InfoQ**:Erlang的一个核心思想是使用非常轻量级的进程模型,这使得上下文切换的开销非常低。另一方面,在许多系统中仍然用线程处理可伸缩性,而线程的可伸缩性往往会成为系统的瓶颈。为了避免这种瓶颈,可以使用一个完整的异步模型配合一个小型的线程池,这种做法也有成功案例(这里有一个参考示例)。你能否详细地分析一下 Erlang的处理方式与完整的异步方法相比所具有的优势?**
Saša:我认为 Erlang 为我们创建高并发的系统提供了一种优秀而整洁的抽象,而任何一种需要持续处理各种不同任务的系统在本质上都是并发的。Erlang 的处理方式非常适合于这种类型的问题,你总是可以通过进程来应对各种类型的任务,无论是 I/O 密集型还是 CPU 密集型任务,并且你可以信任 VM 会高效地分派工作。在使用 Erlang 不太会出现许多顿悟的情况,也不太会搬起石头砸了自己的脚。它能够让减轻我们的负担,让我们专注于实际的业务问题。
反之,如果你打算自行设计一种线程池,那么就不得不自己处理许多问题。打个比方,如果你在某个线程中执行一个时间很长的计算,那么你会阻塞在同一个线程上挂起的其它活动。如果某个线程因为一个 bug 而产生故障,该线程上运行的所有活动都会失败。这一点当然是能够解决的,但你或许要为投入大量的时间,以实现一种类似于 Erlang VM 的功能。既然如此,为什么不依赖于某个已被证实的解决方案呢?如果单纯的处理速度或是内存占用确实极端重要,那么采取自定义的实现方案可能还有一些益处,但在我所遇到的这些情形中来看,基本都不属于这种情况。
InfoQ**:Erlang的另一个基本原则也被 Elixir保留下来了,那就是“任其崩溃”。这种做法目前已经演变为将例行公事般地干掉进程作为一种确保系统能够容忍这一事件的手段了。这种策略对于打造一个具备容错性的 Erlang/Elixir**系统有多大的重要性?
Saša:Erlang 设计的一个前提就是在生产环境中的系统有可能产生错误,但系统作为一个整体不能够中止:它应当尽力保留所有的服务,并尽快地从故障中进行自我修复。
任其崩溃在这种场合扮演了一个核心角色,它是一种简单的技术,能够让我们以一种有条不紊的方式处理系统的错误。在这种情形下,我们会选择让进程崩溃,并依靠 Supervisor 修复问题。这种做法的好处是该进程的主体代码可以不必操心错误处理的问题,例如编写 try-catch 或“if err != nil”这样的代码块,而只关注主路径上的逻辑。我们甚至还可以通过模式匹配的方式优雅地对各种期望进行断言。
在我看来,这种方式比 try-catch-ignore 的做法更优秀,因为一旦进程中止,它的状态也就消失了。而问题的根源很可能来自于有问题的状态。在进程重启之后,新的进程会生成全新的、稳定的状态,因此进程能够再次运转。至少它可以稳定地运行一段时间,直到状态再次出现问题为止。这种做法能够让系统中有问题的地方浮现出来,多数服务在这种方式下都会表现出偶尔的故障现象,直至问题的根源解决为止。
与任其崩溃相辅相成的一点是通过 Supervisor 进行恢复。如果你建立了一个细粒度的 supervision 树,那么所需重启的部分也相对较少。一旦产生故障,你可以试着重启系统中的一小部分如果问题仍然没有解决,你可以逐渐增加这部分的区域,直到系统中有问题的那部分被重启为止。相反,如果你采用了 try-catch-ignore 方式,就有可能导致错误的状态始终延续,最终产生了一个永无休止的故障循环。
InfoQ**:“任其崩溃”是仅属于 Erlang的一种独特功能吗?可否将其移植至其它不使用 Erlang VM的环境中呢?**
Saša:问得好!首先我要强调一点,OTP 是用纯粹的 Erlang 构建的,它依赖于 Erlang VM 的基础功能。理解这一点非常重要,因为我曾经看到过一些说法,认为 OTP 能够以某种方式“移植”到其它运行时环境中。但我认为这是不太可能的,除非目标运行时平台能够提供一些严格的保障。
具体到任其崩溃和 supervisor 来说, Erlang 的 VM 为它们提供了一些重要的保障。
- 每个进程的状态都是私有的,一旦进程中止,它不会留下任何垃圾状态,从而也不会干扰其它的进程。
- 当一个单一的进程崩溃时,其它进程不会受到影响,它们的运行不会被打断,除非你有意这么做。
- 其它进程能够收到某个进程崩溃的通知,并进行一些相应的处理。
- 可以无条件地中止一个进程(即使是进程正在进行一个密集的 CPU 运算)。
- 进程可以拥有外部资源(例如文件句柄或 socket),一旦进程中止,它所拥有的资源会自动回收。
前两点特性能够帮助我们将故障的后果局限在一定范围内:如果有部分出现问题,整个系统的大部分依然能够继续提供服务。第三点保障能够让我们对某个故障进行响应,当发生崩溃时,Supervisor 能够对其进行纠正。最后两点保证了适当的系统清理,如果没有这两点保障,系统可能会产生孤儿进程或是资源无法释放的问题。
如果缺乏这些保障,我认为是无法实现 Erlang 的容错性能力的。即使你能够尽力接近,但永远也做不到 100% 的功能,总会有些隐秘的功能是你无法察觉的。这并不是说你必须要使用 Erlang VM 才能够实现任其崩溃的做法,只是说你需要一种能够提供这些保障的 VM。
InfoQ:你是否能够分享一下你对于 Elixir目前在业界的使用情况的展望?
Saša:虽然 Elixir 还是一门新生的语言,但它的基础(Erlang)已经非常稳定,其能力近 20 年来在各种不同的大型系统中都得到了证实。成功的案例包括 WhatsApp、RabbitMQ、Riak、实时竞价(AdRoll),以及财务系统(Klarna)等等。至于 Elixir,我已经看到它越来越多地出现在各种解决方案的生产环境中,例如游戏的后台或物联网(IoT)。可以在这里找到在生产环境中使用 Elixir 的公司的一个列表,我很期待看到它今后的发展。
关于本书作者
Saša Jurić是一位软件开发者,他在使用 Elixir 和 Erlang 打造高负载、高并发的服务端系统方面具有丰富的经验。
评论