卷1:第15章 Riak与Erlang/OTP

2018-02-24 15:55 更新

Riak是一个分布式、容错和开放源代码的数据库,它展示了如何使用Erlang/OTP来构建大型可伸缩系统。Riak提供了一些其他数据库中并不常见的特性,比如高可用性、容量和吞吐量的线性伸缩能力等,很大程度上,这是借由Erlang对大规模可伸缩分布式系统的支持实现的。

要开发像Riak这样的系统,Erlang/OTP是一个理想的平台,因为它提供了可以直接利用的节点间通信、消息队列、故障探测和客户-服务器抽象等功能。而且,Erlang中大多数常见的模式都已经以库模块的形式实现了,我们一般称之为OTP behaviors。其中包括了用于并发和错误处理的通用代码框架,可以简化并发编程,也能避免开发者陷入一些常见的陷阱。Behaviors由管理者负责监管,而管理者本身也是behavior,这样就组成了一个监管树。通过将监管树打包到应用程序中,这就创建了一个Erlang程序的构建块。

一个完整的Erlang系统,如Riak,是由一组松散耦合且相互作用的应用组成的。其中有些应用是开发者编写的,有些是标准Erlang/OTP发布包中的,还有一些可能是其他的开源组件。这些应用由一个boot脚本按顺序加载并启动,而该脚本是从应用清单和版本信息中生成的。

系统之间的区别在于,启动的发布版本中的应用有所不同。在标准的Erlang发行版中,boot文件会启动Kernel和StdLib(Standard Library,标准库)等应用。而在有些安装版本中,还会启动SASL(Systems Architecture Support Library,系统架构支持库)应用。SASL中包含了带有日志功能的发布和软件更新工具。对Riak而言,除了启动其特定的应用以及运行时依赖(其中包括Kernel、StdLib和SASL)之外,并没有什么不同。一个完整的、准备好运行的Riak构建版本,实际上将Erlang/OTP发行包中的这些标准元素都嵌入其中了,当在命令行调用riak start 时,它们会一同启动。Riak由很多复杂的应用组成,所以本章不应看做一个完整的指南。倒是可以把本章看做以Riak源代码为例,针对OTP的入门指南。图片和数字主要是为了阐明设计意图,故有所简化。

15.1 Erlang简介

Erlang是一个并发的函数式编程语言,用它编写的程序会编译为字节代码并运行在虚拟机上。程序中互相调用的函数经常会产生副作用,如进程间消息传递,I/O和数据库操作等。而Erlang变量是单赋值的,也就是说,一旦变量被给定了一个值,就再也不能修改了。从下面的计算阶乘的例子可以看出,Erlang中大量使用了模式匹配:

-module(factorial).
-export([fac/1]).
fac(0) -> 1;
fac(N) when N>0 ->
   Prev = fac(N-1),
   N*Prev.

在这段代码中,第一个子句(clause)给出了0的阶乘,第二个字句计算正数的阶乘。每一个子句的主体部分都是一个表达式序列,主体部分中最后一个表达式就是这个子句的计算结果。调用这个函数的时候如果传入一个负数会导致运行时错误,因为没有一个子句能匹配负数的模式。不处理这种情况的做法是非防御式(non-defensive)编程的一个例子,这种做法也是Erlang中鼓励的做法。

在模块之中,函数以正常的方式调用;而在模块之外,函数名之前应该加上模块名,如factorial:fac(3)。允许定义同名但是参数数目不同的函数——函数的参数数目称为函数的元数(arity)。在factorial模块的export指令中,元数为1的fac函数通过fac/1表示。

Erlang支持元组(tuple,也称为乘积类型(product type))和列表(list)。元组由花括号包围起来,例如{ok,37}。在元组中,通过元素的位置访问元素。记录(record)是另一种数据类型;在记录中可以保存固定数目的元素,这些元素可以通过名字访问和操作。例如这样的语法可以定义一个记录:-record(state, {id, msg_list=[]})。通过表达式Var = #state{id=1}可以创建一个实例,然后通过这样的表达式可以查看实例中的内容:Var#state.id。如果要使用可变数目的元素,那么我们可以使用列表,列表通过方括号定义,例如[23,34][X|Xs]的表达方式匹配一个非空的列表,其中X匹配头,Xs匹配尾。用小写字母开头的标识符表示一个原子(atom),原子就是一个表示自己的字符串;例如,元组{ok,37}中的ok就是一个原子。通常通过这种方式使用原子来表示函数的结果,例如除了ok结果之外,还可以有{error, "Error String"}这种形式的结果。

Erlang系统中的进程在独立的内存中并发运行,以消息传递的方式进行相互通信。进程可以应用于大量的应用,其中包括数据库的网关,协议栈的处理程序,以及管理从其他进程发送来的跟踪消息的日志。虽然这些进程处理不同的请求,但是进程处理请求的方式却是有相似之处的。

因为进程只存在于虚拟机中,一个VM可以同时运行成千上万个进程,Riak就大量使用了这一特性。例如,对数据的每一个请求——读、写和删除——都采用独立进程处理的模型,这种方式对于大多数采用操作系统级线程的实现而言都是不可能的。

进程是通过进程标识符识别的,进程标识符称为PID;此外,进程还可以通过别名注册,不过注册别名的方式应该只用于长时间运行的“静态”进程。如果一个进程注册了一个别名,那么其他进程就可以在不知道这个进程PID的情况下给这个进程发送消息。进程的创建通过内建函数(built-in function,BIF) spawn(Module, Function, Arguments)完成。BIF是集成在虚拟机中的函数,用于完成纯Erlang不可能实现或实现很慢的功能。spawn/3这个BIF接受一个Module、一个Function和一个Arguments作为参数。这个BIF的调用返回新创建的进程的PID,并且产生一个副作用,就是创建了一个新的进程以之前传入的参数执行模块中的函数。

我们通过Pid ! Msg这种写法将消息Msg发送给进程Pid。一个进程可以通过调用BIF self来得到其PID,之后该进程可以将PID发送给其他进程,这样别的进程就能够利用它与原来的进程通信了。假设一个进程期望接收{ok, N}{error, Reason}这种形式的消息。这个进程可以通过receive语句处理这些消息:

receive
   {ok, N} ->
      N+1;
   {error, _} ->
      0
end

这条语句的结果是由模式匹配语句确定的数值。如果在模式匹配中并不需要某个变量的值,可以像上面例子中那样用下划线来代替。

进程之间的消息传递是异步的,进程接收到的消息会按照其到达顺序放在其信箱中。假设现在正在执行的就是上面的receive表达式:如果信箱中的第一个元素是{ok, N}{error, Reason},那就可以返回相应结果。如果第一个元素并非这两种形式之一,那它会继续保留在信箱之中,然后以类似的方式处理第二个消息。如果没有消息能匹配成功,receive会继续等待,直到接收到一个匹配的消息。

进程终止有两种原因。如果没有更多的代码要执行了,它们会以原因normal退出。如果进程遇到了运行时错误,它会以非normal的原因退出。进程的终止只会对和其“链接”在一起的进程产生影响。进程可以通过BIF link(Pid)链接在一起,也可以在调用spawn_link(Module, Function, Arguments)的时候链接在一起。如果一个进程终止了,那么这个进程会对其链接集合中的所有进程发送一个EXIT信号。如果终止原因不是normal,那么收到这个信号的进程会终止自己,并且进一步传播EXIT信号。如果调用BIF process_flag(trap_exit, true),那么进程收到EXIT信号之后不会终止,而是以Erlang消息的方式将EXIT信号放在进程的信箱中。

Riak通过EXIT信号监视辅助进程的健康状况,这些辅助进程负责执行由请求驱动的有限状态机发起的非关键性的工作。当这些辅助进程异常终止的时候,父进程可以通过EXIT信号决定忽略错误或重新启动进程。

15.2. 进程框架

我们前面引入了这一概念,即不管进程是出于什么目的创建的,它们总要遵从一个共同的模式。作为开始,我们必须创建一个进程,然后可以为它注册一个别名,当然后者是可选的。对于新创建的进程而言,它的第一个动作是初始化进程循环数据。循环数据一般通过在进程初始化时传给内置函数spawn的参数得到。循环数据保存在叫做进程状态的变量中。状态(一般保存在一个记录中)会被传递给接收-求值函数,该函数是一个循环,负责接收消息,处理消息,更新状态,之后将状态作为参数传给一个尾递归调用。如果处理到了‘stop’消息,接收进程会清理自身数据,然后退出。

不管进程要执行什么任务,这都是进程之间反复出现的一种机制。记住这一点之后,我们再来看一下,遵守这一模式的进程之间又有何不同:

  • 创建不同的进程时传入BIF spawn的参数会有不同
  • 在创建一个进程的时候,要考虑是否为这个进程注册一个别名,如果需要的话,还要考虑别名是什么。
  • 初始化进程状态的函数要执行的动作依进程执行任务的不同而不同。
  • 无论哪种情况,系统的状态都用循环数据表示,但不同进程的循环数据会有不同。
  • 在接收-求值循环体中,不同的进程接收的消息是不一样的,而且处理的方式也五花八门。
  • 最后,在进程结束的时候,清理动作也随进程而异。

所以,即使存在一个通用的动作框架,它们仍然需要与具体任务相关的各种动作来补充。以该框架为模板,程序员能够创建不同的进程,用以承担服务器、有限状态机、事件处理程序和监督者等不同职责。但是我们不必每次都重新实现这些模式,它们已经作为行为模式放在类库中了。它们是OTP中间件的一部分。

15.3. OTP行为

开发Riak的核心开发者团队分布在十几个不同的地点。如果没有非常紧密的合作和可操作的模板,那么最终可能会得到各种不同的客户端/服务器实现,这些实现可能还不能处理特殊的边界条件和并发相关的错误。此外,可能还无法形成一种处理客户端和服务器崩溃的统一方法,而且也无法保证来自于一个请求的应答是一个合法应答而不只是某条服从内部消息协议的任意消息。

OTP指的是一组Erlang库和设计模式,宗旨是为开发健壮系统提供一组现成的工具。其中很多模式和库都以“行为”(behavior)的形式提供。

OTP行为提供了一些实现了最常见并发设计模式的库模块,从而解决了上述问题。在幕后,这些库模块可以确保以一致的方式处理错误和特殊情况,而程序员并不需要意识到这些。因此,OTP行为提供了一组标准化的构建单元,利用这些构建单元可以设计和构建工业强度的系统。

15.3.1. OTP行为简介

OTP行为是通过stdlib应用程序中的一些库模块提供的,而后者是Erlang/OTP发行版中的一部分。由程序员编写的具体代码放在独立的模块中,这些代码通过每一个行为中预定义的一组标准回调函数调用。这个回调模块要包含实现某个功能所需要的所有具体代码。

OTP行为中包含工作进程,负责实际的处理工作,还包含监督者进程,负责监视工作进程和其他监督进程。工作进程(worker)行为包括服务器、事件处理程序和有限状态机,在图中通常使用圆圈表示。监督者(supervisor)负责监视其子进程,既包含工作进程也包含其他监督者,在图中通常用方框表示,工作者和监督者共同组成了监督树(supervision tree)。

以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号