本章涵盖:
- Erlang概述
- Elixir的优点
关于Erlang
Erlang 是可靠的,响应能力,可扩展以及程序可用性(reliability, responsiveness, scalability, and constant availability are imperative).
其是一个提供特殊技术支持的通用开发平台,非功能性的挑战,如并发性、可伸缩性、容错、分布、和高可用性。
今天流行的系统更多是关于通信和协作,例如社交网络、内容管理系统、多媒体点播和多人游戏。
理想情况下,系统没有崩溃或应下来,即使是在软件升级。它应该是启动和运行,为其客户提供服务。
这是 Erlang 的目的。高可用性是通过技术概念明确支持如可伸缩性、容错和分布。与其他大多数现代开发平台不同,这些概念背后的主要动力和驱动力Erlang的发展。
Erlang 最近获得了更多地关注。最近二十年,它造就了很多大型系统,例如 WhatsApp 应用、Riak 分布式数据库、Heroku云、Chef部署智能化系统、RabbitMQ消息队列、金融系统以及多人后端。无论是在时间和规模上,这是真正成熟的技术。但Erlang背后的魔法是什么?我们看看Erlang如何帮助您构建高可用性,可靠的系统。
1.1.1 高可用性
Erlang 专注于创建支持高可用性系统的开发——系统总是在线的,即使在意外的情况下也向客户提供服务。表面上看起来很简单,但是您可能知道,在生产中许多事情可能出错,为了使系统 24/7 不停机的工作,我们必须解决一些技术上的挑战:
- 容错 Fault tolerance — 不论发生什么,我们想要尽可能的定位错误的影响,从错误中恢复,保持 系统正常运行和提供服务。
- 可扩展 Scalability — 我们应该能够应对负荷的增加以添加更多的硬件资源无需任何软件干预。理想情况下,这应该是可能的而不需要重启系统。
- 分布式 Distribution — 这可以促进整个系统的稳定性:如果一台机器下线,另一台能够接管。而且,这给了我们意味着可以解决负载增加系统添加更多的机器,从而增加单位支持更高的需求。
- 响应力 Responsiveness — 特别是,偶尔冗长的任务不应该阻止其他系统或对性能有显著的影响。
- 在线升级 Live update —在某些情况下,您可能想要将您的软件的新版本而不需要重新启动服务器。例如,在电话系统中,我们不想断开建立电话当我们升级软件。
1.1.2 Erlang concurrency
并发是Erlang系统的核心和灵魂。
- 容错
- 可扩展
- 分布式
- 响应力
1.1.3 服务端系统
Erlang 用于大量应用和系统,有Erlang-based桌面应用程序的例子,它通常用于嵌入式环境中。有意思的是,在我看来,在涉及服务端系统——系统运行一个或多个服务器并服务于多个客户。服务器端系统表明,这不仅仅是一个简单的服务器处理请求。它是一整个系统,除了请求处理,还必须运行大量后台任务以及管理服务器范围内的内存状态,如下图:
Technical requirement | Server A | Server B |
---|---|---|
HTTP server | Nginx and Phusion Passenger | Erlang |
Request processing | Ruby on Rails | Erlang |
Long-running requests | Go | Erlang |
Server-wide state | Redis | Erlang |
Persistable data | Redis and MongoDB | Erlang |
Background jobs | Cron, Bash scripts, and Ruby | Erlang |
Service crash recovery | Upstart | Erlang |
1.1.4 开发平台
Erlang 更像一个编程语言。它是一个成熟的开发平台,包含四个方面:语言、虚拟机、框架和工具。
标准的部分是一个成为 Open Telecom Platform (OTP) 的框架。尽管这样命名,已经这框架也与电信系统无关。 这是一个通用的框架,已经抽象为典型的Erlang任务:
- 并发和分布模式
- 并发系统的错误检测和恢复
- 包装代码库
- 系统部署
- 在线升级代码
没有 OTP 也可以做这些事情,但是没有任何意义。OTP 是许多生产系统中和Erlang中不可分割的一部分,很难在两者之间画一条线。甚至官方分布称为Erlang / OTP。
工具用于大量典型的任务,例如编译代码、启动 BEAM 实例、创建部署版本、运行终端交互、连接运行中的 BEAM 示例、等等。BEAM 和附带的工具是跨平台的,可以在各种操作系统中运行它们,例如 Unix、Linux、Windows 等。整个Erlang是开源的,可以在官网 (http://erlang.org)或者Github (https://github.com/erlang/otp)找到源代码。
爱立信还负责开发过程,定期发布一个新版本,一年一次。Erlang的故事到此结束。但是如果Erlang是如此之大,为什么你需要 Elixir 吗?下一节旨在回答这个问题。
1.2 关于 Elixir
Elixir是一种替代Erlang虚拟机的语言,也许你编写更清晰、简洁的代码,并更好地表达您的意图。用Elixir编写的程序通常在 BEAM 中运行。
Elixir targets the Erlang runtime. The result of compiling the Elixir source code are BEAM-compliant byte-code files that can run in a BEAM instance and can normally cooperate with pure Erlang code—you can use Erlang libraries from Elixir and vice versa. There is nothing you can do in Erlang that can’t be done in Elixir, and usually the Elixir code is as performant as its Erlang counterpart.
Let’s take a closer look at how Elixir improves on some Erlang features. We’ll start with boilerplate and noise reduction.
1.2.1 Code simplification
One of the most important benefits of Elixir is the ability to radically reduce boilerplate and eliminate noise from code, which results in simpler code that is easier to write and maintain. Let’s see what this means by contrasting Erlang and Elixir code. A frequently used building block in Erlang concurrent systems is the server process. You can think of server processes as something like concurrent objects—they embed private state and can interact with other processes via messages. Being concurrent, different processes may run in parallel. Typical Erlang systems rely heavily on processes, running thousands or even millions of them. The following example Erlang code implements a simple server process that adds two numbers.
-module(sum_server).
-behaviour(gen_server).
-export([
start/0, sum/3,
init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
code_change/3
]).
start() -> gen_server:start(?MODULE, [], []).
sum(Server, A, B) -> gen_server:call(Server, {sum, A, B}).
init(_) -> {ok, undefined}.
handle_call({sum, A, B}, _From, State) -> {reply, A + B, State}.
handle_cast(_Msg, State) -> {noreply, State}.
handle_info(_Info, State) -> {noreply, State}.
terminate(_Reason, _State) -> ok.
code_change(_OldVsn, State, _Extra) -> {ok, State}.
Even without any knowledge of Erlang, this seems like a lot of code for something that only adds two numbers. To be fair, the addition is concurrent; but regardless, due to the amount of code, it’s hard to see the forest for the trees. It’s definitely not immediately obvious what the code does. Moreover, it’s difficult to write such code. Even after years of production-level Erlang development, I still can’t write this without consulting the documentation or copying and pasting it from previously written code.
The problem with Erlang is that this boilerplate is almost impossible to remove, even if it’s identical in most places (which in my experience is the case). The language provides almost no support for eliminating this noise. In all fairness, there is a way to reduce the boilerplate using a construct called parse transform , but it’s clumsy and complicated to use. So in practice, Erlang developers write their server processes using the just-presented pattern.
Because server processes are an important and frequently used tool in Erlang, it’s an unfortunate fact that Erlang developers have to constantly copy-paste this noise and work with it. Surprisingly, many people get used to it, probably due to the wonderful things BEAM does for them. It’s often said that Erlang makes hard things easy and easy things hard. Still, the previous code leaves an impression that you should be able to do better.
Let’s see the Elixir version of the same server process, presented in the next listing.
defmodule SumServer do
use GenServer
def start do
GenServer.start(__MODULE__, nil)
end
def sum(server, a, b) do
GenServer.call(server, {:sum, a, b})
end
def handle_call({:sum, a, b}, _from, state) do
{:reply, a + b, state}
end
end
This code is significantly smaller and therefore easier to read and maintain. Its intention is more clearly revealed, it’s less burdened with noise. And yet it’s as capable and flexible as the Erlang version. It behaves exactly the same at runtime and retains the complete semantics. There is nothing you can do in the Erlang version that’s not possible in its Elixir counterpart.
Despite being significantly smaller, the Elixir version of a sum server process still feels somewhat noisy, given that all it does is add two numbers. The excess noise exists because Elixir retains a 1:1 semantic relation to the underlying Erlang library that is used to create server processes.
But the language gives you tools to further eliminate whatever you may regard as noise and duplication. For example, I have developed my own Elixir library called ExActor that makes the server process definition dense, as shown next.
defmodule SumServer do
use ExActor.GenServer
defstart start
defcall sum(a, b) do
reply(a + b)
end
end
The intention of this code should be obvious even to developers with no previous Elixir experience. At runtime, the code works almost completely the same as the two previous versions. The transformation that makes this code behave like the previous ones happens at compile time. When it comes to the byte code, all three versions are similar.
NOTE
I mention the ExActor library only to illustrate how much you can abstract away in Elixir. But you won’t use this library in this book, because it’s a third-party abstraction that hides important details of how server processes work. To completely take advantage of server processes, it’s important that you understand what makes them tick, which is why in this book you’ll learn about lower-level abstractions. Once you understand how server processes work, you can decide for yourself whether you want to use ExActor to implement server processes.
This last implementation of the sum
server process is powered by the Elixir macros facility. A macro is Elixir code that runs at compile time. A macro takes an internal representation of your source code as input and can create alternative output. Elixir macros are inspired by Lisp and should not be confused with C-style macros. Unlike C/C++ macros, which work with pure text, Elixir macros work on abstract syntax tree (AST) structure, which makes it easier to perform nontrivial manipulations of the input code to obtain alternative output. Of course, Elixir provides helper constructs to simplify this transformation.
Take another look at how the sum operation is defined in the last example:
defcall sum(a, b) do
reply(a + b)
end
Notice the defcall at the beginning. There is no such keyword in Elixir. This is a custom macro that translates the given definition to something like the following:
def sum(server, a, b) do
GenServer.call(server, {:sum, a, b})
end
def handle_call({:sum, a, b}, _from, state) do
{:reply, a + b, state}
end
Because macros are written in Elixir, they’re flexible and powerful, making it possible to extend the language and introduce new constructs that look like an integral part of the language. For example, the open source Ecto project, which aims to bring LINQ style queries to Elixir, is also powered by Elixir macro support and provides an expressive query syntax that looks deceptively like part of the language:
from w in Weather,
where: w.prcp > 0 or w.prcp == nil,
select: w
Due to its macro support and smart compiler architecture, most of Elixir is written in Elixir. Language constructs like if and unless and support for structures are implemented via Elixir macros. Only the smallest possible core is done in Erlang—everything else is then built on top of it in Elixir!
Elixir macros are something of a black art, but they make it possible to flush out nontrivial boilerplate at compile time and extend the language with your own DSL-like constructs. But Elixir isn’t all about macros. Another worthy improvement is some seemingly simple syntactic sugar that makes functional programming much easier.
1.2.2 Composing functions
Both Erlang and Elixir are functional languages. They rely on immutable data and functions that transform data. One of the supposed benefits of this approach is that code is divided into many small, reusable, composable functions.
Unfortunately, the composability feature works clumsily in Erlang. Let’s look at an adapted example from my own work. One piece of code I’m responsible for maintains an in-memory model and receives XML messages that modify the model. When an XML message arrives, the following actions must be done:
- Apply the XML to the in-memory model.
- Process the resulting changes.
- Persist the model.
Here’s an Erlang sketch of the corresponding function:
process_xml(Model, Xml) ->
Model1 = update(Model, Xml),
Model2 = process_changes(Model1),
persist(Model2).
I don’t know about you, but this doesn’t look composable to me. Instead, it seems fairly noisy and error prone. The temporary variables Model1
and Model2
are introduced here only to take the result of one function and feed it to the next.
Of course, you could eliminate the temporary variables and inline the calls:
process_xml(Model, Xml) ->
persist(
process_changes(
update(Model, Xml)
)
).
This style, known as staircasing, is admittedly free of temporary variables, but it’s clumsy and hard to read. To understand what goes on here, you have to manually parse it inside-out.
Although Erlang programmers are more or less limited to such clumsy approaches, Elixir gives you an elegant way to chain multiple function calls together:
def process_xml(model, xml) do
model
|> update(xml)
|> process_changes
|> persist
end
The pipeline operator |>
takes the result of the previous expression and feeds it to the next one as the first argument. The resulting code is clean, contains no temporary variables, and reads like the prose, top to bottom, left to right. Under the hood, this code is transformed at compile time to the staircased version. This is again possible because of Elixir’s macro system.
The pipeline operator highlights the power of functional programming. You treat functions as data transformations and then combine them in different ways to gain the desired effect.
1.2.3 The big picture
There are many other areas where Elixir improves the original Erlang approach. The API for standard libraries is cleaned up and follows some defined conventions. Syntactic sugar is introduced that simplifies typical idioms. A concise syntax for working with structured data is provided. String manipulation is improved, and the language has explicit support for Unicode manipulation. In the tooling department, Elixir provides a mix tool that simplifies common tasks such as creating applications and libraries, managing dependencies, and compiling and testing code. In addition, a package manager called Hex (https://hex.pm/) is available that makes it simpler to package, distribute, and reuse dependencies.
The list goes on and on; but instead of presenting each feature, I’d like to express a personal sentiment based on my own production experience. Personally, I find it much more pleasant to code in Elixir. The resulting code seems simpler, more readable, and less burdened with boilerplate, noise, and duplication. At the same time, you retain the complete runtime characteristics of pure Erlang code. And you can use all the available libraries from the Erlang ecosystem, both standard and third party.
1.3 Disadvantages
No technology is a silver bullet, and Erlang and Elixir are definitely not exceptions. Thus it’s worth mentioning some of their shortcomings.
1.3.1 Speed
Erlang is by no means the fastest platform out there. If you look at various synthetic benchmarks on the Internet, you usually won’t see Erlang high on the list. Erlang programs are run in BEAM and therefore can’t achieve the speed of machine-compiled languages, such as C and C++. But this isn’t accidental or poor engineering on behalf of the Erlang/OTP team.
The goal of the platform isn’t to squeeze out as many requests per seconds as possible, but to keep performance predictable and within limits. The level of performance your Erlang system achieves on a given machine shouldn’t degrade significantly, meaning there shouldn’t be unexpected system hiccups due to, for example, the garbage collector kicking in. Furthermore, as explained earlier, long-running BEAM processes don’t block or significantly impact the rest of the system. Finally, as the load increases, BEAM can use as many hardware resources as possible. If the hardware capacity isn’t enough, you can expect graceful system degradation—requests will take longer to process, but the system won’t be paralyzed. This is due to the preemptive nature of the BEAM scheduler, which performs frequent context switches that keep the system ticking and favors short-running processes. And of course, you can address higher system demand by adding more hardware.
Nevertheless, intensive CPU computations aren’t as performant as, for example, their C/C++ counterparts, so you may consider implementing such tasks in some other language and then integrating the corresponding component into your Erlang system. If most of your system’s logic is heavily CPU bound, then you should probably consider some other technology.
1.3.2 Ecosystem
The ecosystem built around Erlang isn’t small, but it definitely isn’t as big as that of some other languages. At the time of writing, a quick search on GitHub reveals about 20,000 Erlang-based repositories, and about 36,000 Elixir repositories. In contrast, there are almost 1,500,000 Ruby repositories and more than 5,000,000 for JavaScript.
Therefore, you should be prepared that the choice of libraries won’t be as abundant as you may be used to, and in turn you may end up spending extra time on something that would take minutes in other languages. If that happens, keep in mind all the benefits you get from Erlang. As I’ve explained, Erlang goes a long way toward making it possible to write fault-tolerant systems that can run for a long time with hardly any downtime. This is a big challenge and a specific focus of the Erlang platform. So although it’s admittedly unfortunate that the ecosystem isn’t as mature as it could be, my sentiment is that Erlang significantly helps with hard problems, even if simple problems can sometimes be more clumsy to solve. Of course, those difficult problems may not always be important. Perhaps you don’t expect a high load, or a system doesn’t need to run constantly and be extremely fault-tolerant. In such cases, you may want to consider some other technology stack with a more evolved ecosystem.
1.4 Summary
This chapter defined the purpose and benefits of Erlang and Elixir. There are a couple of points worth remembering:
- Erlang is a technology for developing highly available systems that constantly provide service with little or no downtime. It has been battle tested in diverse large systems for more than two decades.
- Elixir is a modern language that makes development for the Erlang platform much more pleasant. It helps organize code more efficiently and abstracts away boilerplate, noise, and duplication.
Now you can start learning how to develop Elixir-based systems. In the next chapter, you learn about the basic building blocks of Elixir programs.
https://livebook.manning.com/#!/book/elixir-in-action-second-edition/chapter-1/v-3/1f
网友评论