建立一个OTP应用

概述

Erlang是一门干净简洁的语言,也容易学习。这只是Erlang,我们现在要讨论OTP。当进入到OTP之后,学习曲线就一下子上去了。各种各样的问题就来了,比如:我怎么启动一个应用,监督员(supervisor)的功能是什么,还有我要怎样使用gen_server?这还只是开始,后面还有更让人迷惑的……“.app文件是啥,怎么发布,还有,有人知道.script和.boot文件是干嘛的吗?”

本教程将带你一起创建一个叫做location_server的OTP应用。这个应用可以储存客户端的位置,允许别人从连接的节点查询这些位置,同时还允许客户端订阅位置变更时间所发出的通知。在整个创建location_server的过程中,我将解答以上那些关于OTP应用的问题,以及一些别的东西。

第一部分:建立服务器

首先要下载本教程的引用构建系统,可以在www.erlware.org上的downloads otp_base-<vsn>中找到。你在自己本地的Unix/Linux机器上下好这个代码之后,将构建系统解压出来。

我们首先要做的是对这个构建系统进行初始构建,只要在otp目录下输入make即可。注意:如果你的Erlang没有安装/usr/local/lib/erlang下的话,你可以创建一个指向你所安装的目录的符号链接,或者也可以将环境变量ERL_RUN_TOP设置为安装目录(确保使用的是绝对路径)。

然后第二件要做的事情是搭好应用程序的骨架。完成之后我就会剖析这个骨架并解释它的各个部分的含义。使用appgen工具来创建location_server的应用程序骨架。

我们已经在lib/location_server中创建好了一个应用、并在release/location_server_rel中创建好了一个发布。现在转入lib/location_server目录看看都有些什么。

其中include目录包含了一个叫作location_service.hrl的文件。我们会把所有共享的宏(macro)和记录定义放在这里。src目录包含了所有的Erlang文件(.erl)。vsn.mk文件包含了make变量LOCATION_SERVER_VSN=1.0,表示我们的应用的初始版本(可以随意更改)。这个版本在处理发布的时候会起到作用,我们会在后面讲到。

src目录包含了一个叫做location_server.erl的文件。注意这个Erlang源文件和我们的应用的名字一样。根据约定,这个文件将包含我们的应用的API。这个文件也包含了-behaviour(application)指令。这表示这个文件可以用作该OTP应用结构的入口点。

start/2函数是一个回调函数,OTP应用系统将用它启动由OTP发布系统所指定的应用。这个函数要求两个参数(你可以阅读关于监督机制的文章来了解更多关于该参数的信息)同时它会返回{ok, Pid}或者{ok, Pid, State},前者更为常见,代表应用成功启动。注意start/2函数调用了ls_sup:start_link/1函数。ls_sup.erl包含了我们的顶层监督员。注意名称ls_sup中的前缀“ls”其实是我们的应用程序名称“location_server”的两个单词的缩写。这种前缀也是一种约定,它用于实现Erlang中的包结构来避免一个发布中的名称冲突。你也可以随意指定一个前缀,这种应用程序名称缩写的方式仅仅是我的建议。

顶层监督员将启动应用程序所必须的所有的工人(worker)以及底层的监督员。它指定了一种策略,详细描述了当进程崩溃的时候如何被重启。这在Erlang中是一个关键的概念,我建议你阅读一下www.erlang.org上的关于监督机制的设计原理。

这个文件导出了两个函数start_link/1init/1。这是遵照监督员行为的两个函数——注意出现在文件顶部的-behaviour(supervisor)指令。指定一个函数start_link是在OTP中的一种约定,表示该函数会产生(spawn)一个进程并且这个函数的调用者,也就是父进程,会被联接到(参见link/1)到子进程。这种情况下OTP应用结构会被联接到监督员进程上。

init/1函数则是真正的监督员核心所在。记住不要将任何程序逻辑放到监督员中,它必须尽可能简单并且不会出错,你应用程序的命运就掌握在这里。让我们将它进行分解:

RestartStrategy = one_for_one表示对于每个挂掉的进程只需要重启该进程而不影响其他的进程。还有其他两种重启策略,one_for_llrest_for_all,你可以在监督员文档中了解到它们的信息。MaxRestarts表示在MaxTimeBetweenRestarts秒内随多允许的重启次数。

这个监督员只有一个ChildSpec表示它仅会启动一个进程。按顺序看看数据结构,ls_server是要被启动的进程的名字,{ls_server, start_link, []}是启动这个子进程要调用的模块函数和参数,permanent表示这个进程除了重启决不允许挂掉(还有其他的选项,参考erlang.org上关于监督机制的文档),1000是在野蛮杀死进程之前等待进程结束的时间,worker表示该子进程并非一个监督员,[ls_server]是该进程所依赖的模块的列表。

监督员回调模块函数init/1返回所有这些信息,同时监督员行为会负责按指定顺序启动所有子进程。

现在进入ls_server.erl文件。这个文件展示了gen_server行为。这表示它将遵循这种行为的规定导出以下回调函数:

另外我们还要创建一下外部函数:

我们开始吧。为所有的外部函数编写文档非常重要。OTP基本构建系统已经通过“make docs”来支持“edoc”,这个指令可以自动遍历并编译应用的所有文档并存入应用目录中的doc目录中(location_server/doc)。回到我们的函数:

这个函数启动服务器,并在本地注册为?SERVER(在源代码里找找-define(SERVER,?MODULE).看)并且指示gen_server行为当前模块导出了必要的回调函数。

stop很简单,不过注意gen_server:cast/2的使用。这个函数基本和使用“!”操作符类似。他用于给一个通用服务器发送一个异步消息。这里我们给标识为?SERVER的本地注册进程发送原子stop

store_location/3是一个非常简单的函数,但是也有不少地方需要引起我们注意。注意我们使用了守护表达式(guard expression),is_atom/1以及is_list/1。要确保传递进来的变量的类型是正确的。这是个不信任边界外事物的例子:在这里检查过之后就不用再检查了。你有没有注意到,这个函数的名字和我们发送的消息中的“标签”是一样的?没有么?我来解释一下:

上面这行中的store_location就是我们的“标签”,它可以将这个消息和其他消息区分开来。{Id, Location}是我们所发送的在标签store_location之下的有效载荷。在gen_server中所发送的消息应该遵循“{标签, 值}”的约定。其实在上面这行还有一个更重要的东西需要注意。注意这个函数的第一个参数是{?SERVER, Node}而不仅仅是我们在前面一个cast中看到的?SERVER。出现这个情况是因为我们想要能够与在网络中任何节点上进程联系而不仅仅是我们自己的进程。要给远程注册的消息发送消息,就可以使用这种语法{注册名,节点}其中节点可以是在网络上的任何节点包括本地节点。

这里要注意的东西是我们没有再使用守护条件——我们不关心——这个函数不会改变服务器的状态,如果它挂了对我们也没什么影响。最重要需要注意的地方是gen_server:call/2的使用。这个函数是向一个gen_server发起一个同步(阻塞的)调用。

现在我们完成了所有的外部程序,让我们充实服务器吧。对于新接触Erlang的人来说,有一个事情很容易混淆,就是:这些外部函数是在调用进程的进程空间中被调用的,虽然他们生成的消息是是被同一个模块中的语句接收的,但是这些语句却是存在于一个完全不同的进程中的。有可能的话,要使用模块来封装服务器协议,这点很重要。

在进程能被外界访问前,由init函数设置进程状态。这里状态储存于一个字典dict中。这个字典可以用于储存形如{Id, {Location, ListOfSubscribers}}这样的键值对。init函数还记录了ls_server进程启动的信息。日志文件的位置和管理将在本教程的第二部分(讲述如何为应用创建一个发布)进行阐述。

这样服务器和location_server应用的所有功能就完成了,但是我们还有最后一步要做。显然,直接让应用程序的客户直接调用ls_server:store_location这种非常笨拙和丑陋。我们所需要的就是针对客户良好定义的外部API。放这些API的地方就是和我们的应用程序名称一样的那样模块——我们的应用行为模块,location_server.erl

毫无疑问必须得导出这些函数。

现在我们完成了location_server的编码,剩下要做的就是到应用程序的目录中,输入“make”和“make docs”,并等待代码和文档编译成功。

注意应用程序根目录下出现了两个新目录“doc”和“ebin”。location_server/doc包含了前面写的代码中的文档。所有的外部函数都会出现在这些文档中。让我们稍微深入研究一下ebin目录。

ebin包含了每个.erl源文件对应的编译好的.beam字节码文件。ebin目录也有一个.app文件和一个.appup文件。.appup文件用于处理升级发布,这超出了本教程的范围。.app文件对于创建和安装OTP发布是必不可少的,所以我们再更深入地研究一下它。

以上代码中的注释已经基本完全解释清楚了。这个.app文件是动态创建由make进程从location_server/src/location_server.app.src文件中自动创建的。这表示你应该基本无须手动更改这个文件。

恭喜你已经创建了首个漂亮的OTP应用。现在让我们进入本教程的第二部分并创建一个发布,一边能运行和部署我们的OTP应用。

第二部分:打造一个发布

在otp目录中:

该目录包含了一个vsn.mk文件,用于储存本发布的版本字符串。还有一个yaws.conf.src文件用于生成yaws.conf文件,如果你选择在发布中包含yaws服务器。目前我们将注意力集中在location_server_rel.config.srclocation_server.rel.src这两个文件上。.config.src文件由make翻译为.config文件。这个文件包含了发布中的必须的各种不同应用程序(诸如截断翻转日志)的所有配置。.rel.src文件则会被make转成.rel文件。.rel文件又会用于创建.script.boot文件,这些文件将告诉Erlang系统当启动的时候要载入哪些代码。下面是我们的.rel.src文件。

这个文件指定了我们的发布的名字,并且跟了一个版本串,当进行make的时候会将vsn.mk文件中的版本串填入这个地方。接下来指定了我们的发布中要包含erts,以及kernel, stdlib, sasl, fslib, gas最后我们自己的应用location_server。其中fslib和gas这两个应用为我们的发布提供了配置和日志翻转截断,关于他们的详细信息可以到erlware.org找到完整的文档。另外你还可以进入otp/lib/fslib或者otp/lib/gas目录中并运行“make docs”就可以在本地生成所有的文档了。我们运行了make之后,这个.rel.src文件就会被转换成实际的.rel文件,它包含了.rel.src文件中出现的所有应用名字结合了对应的版本号。关于.rel.src结构的更多信息,可以看一下fslib文件fs_boot_smithe的文档。

是时候该编译并运行我们的发布了。

生成了不少文件还有两个目录。我们看到了一个.rel文件,一个.script文件和一个.boot文件。每个文件的内容都看看,.boot文件就不用看了因为它只是.script文件的二进制版本。注意刚刚创建的两个目录:locallocation_server_rellocation_server_rel是一个用于安装和应用程序生产发布的一个场景区域,更多信息请参考erlware.org上的OTP Base中的文档。接下来将焦点放在这里的local目录。这个目录让我们可以交互式地运行我们的发布。如果我们要将其作为一个守护进程运行,只需要简单地调用local/location_server_rel.sh,并加上-detached标志就行了。现在转到local目录中并直接运行我们的应用程序。

err_log文件包含了所有通过saslerror_loggerinfo_msgerror_msg打印的日志。其他日志,如在sasl_log中你可以看到所有的sasl错误报告。如果发布的程序崩溃了,首先要查看sasl_log。默认情况下构建系统已经加入了对fs_elwrap_h(通过G.A.S应用,已作为这个发布的一部分)的应用以便截断和翻转日志文件。这可以防止日志文件无限制地增长。如果要配置或者移出这个功能,可以配置“local”目录下的location_server.config文件。

mod_specs告诉GAS应用对于某个发布,要启动哪些服务以及如何启动。后面的实际的配置元组,elwrap将根据它来工作。首先是err_log的位置,后面跟着wrap specs,指定了sasl日志和错误日志的单个文件进行翻转之前最大可以达到多少,以及指定的日志记录器保存多少个文件。配置的最后一位指定错误记录器是否要打印到屏幕。

第三部分:测试我们所构建的东西

若要测试我们的项目,首先要启动三个单独的Erlang命名节点。打开三个终端窗口并启动三个节点,其中第一个是位置服务器location server,由local/location_server.sh启动,第二个由erl -name a -pz <location_server/ebin的路径>启动,第三个erl -name b -pz <location_server/ebin的路径>。其中-pz参数告诉解释起将指定的路径添加到代码加载器的搜索路径中。这表示我们可以在节点a和b上使用location_server的接口了。(这种代码加载没有文件。)这样我们启动了三个节点a@machinename.comb@machinename.com以及<username>_location_server@machinename.com。要连接节点,从其他两个节点上执行nat_adm:ping('a@machinename.com')。完了后,在任意一个节点上运行nodes(),应该得到在我们的云中的其他节点的列表。在节点上运行以下命令。确保以下例子中的节点正确,因为这些命令不都是运行于同一个节点上的。

上面所发生的内容是:我们通过location_server_rel.sh启动脚本将位置服务器作为节点’martinjlogan_location_server_rel@core.martinjlogan.com’启动了。然后我们启动了两个节点a@core.martinjlogan.com和b@core.martinjlogan.com。然后执行了以下步骤:

  1. 在“a”上的第1个提示,我们存储了键“martin”和值“at work”
  2. 在“b”上的第1个提示,我们发出了要更改键“martin”的警告
  3. 在“b”上的第2个提示,我们等待接受信息并打印
  4. 在“a”上的第2个提示,我们又存储了个位置,将“martin”的位置改成了“at home”并让“b”退出了接受语句并打印了该消息:Msg: {location_changed, {martin, "at home"}}.订阅消息也成功了!
  5. 最后在“a”上的第3个地址,我们验证了fetch_location正常工作。

你可以从这里下载本教程的全部代码,祝你用Erlang愉快编程。

CN Erlounge III

12月20、21号,ECUG在上海美臣大酒店召开了CN Erlounge的第三次会议。此次会议云集了各路高手,高手多数都是在分布式计算、高并发的服务器编程上寻求更好的解决方案才进入Erlang的领域的。讲座非常精彩,气氛非常热烈,会议安排也非常不错。

没啥可特别说的,大家可以期待会务组放出讲座视频。Erlang配合诸如Comet开发高并发的Web应用是此次的焦点,有三个讲座与此有关。

PS: 会场上放眼望去,基本上都是IBM ThinkPad(或者说Lenovo ThinkPad)。

大家的 PPT 在[这里]有一些(有待补全)照片可以看[这里](有待补全),现场还有做 video camera 正在紧张制作中。

关于Erlang和SMP的一些说明

原文:http://groups.google.com/group/erlang-questions/browse_thread/thread/7827f5e32681ca8e

by.Kenneth Erlang/OTP team, Ericsson
译:ShiningRay

以下是一些Erlang SMP实现的细节和与性能与伸缩性相关一些简单介绍。

几周之内还有有一个关于多核如何运作以及未来如何发展的更详细的介绍。我打算将一些内容放在我的报告中,将于9月27日的ICFP2008,Erlang Workshop在Victoria BC展示给大家。

没有SMP支持的Erlang VM只有1个运行在主处理线程中的调度器。该调度器从运行队列(run-queue)中取出可以运行的Erlang进程以及IO任务,而且因为只有一个线程访问他们所以无须锁定任何数据。

而带有SMP支持的Erlang VM可以有一个或多个调度器,每个运行在一个线程中。调度器从同一个公共运行队列中取出可运行的Erlang进程和IO任务。在SMP VM中所有的共享数据结构都会由锁进行保护,运行队列就是这样一个由锁保护的数据结构。

从OTP R12B开始,如果操作系统报告有多于1个的CPU(或者核心)VM的SMP版本会自动启动,并且根据CPU或者核心的数量启动同样数量的调度器。

你可以从“erl”命令打印出来的第一行看到它选择了哪些参数。例如:

默认值可以用“-smp [enable|disable|auto]”来替换,auto是默认的。如果smp被启用了(-smp enable),要设置调度器的数量可以使用“+S Number”其中Number是调度器的数量(1到1024)

注意1:运行多于CPU或核心总数的调度器不会有任何提升。

注意2:在某些操作系统中一个进程可使用的CPU或者核心的数量可以被限制。例如,在Linux中,命令“taskset”就可以实现这个功能。Erlang VM目前还只能探测CPU或者核心的总数,不会考虑“taskset”所设置的掩码。正因如此,例如可能会出现(已经出现过了)即使Erlang VM运行了4个调度器,也只使用了2个核心。OS会进行限制因为它要考虑“taskset”所设置的掩码。

每个Erlang VM的调度器都运行于一个OS线程上,是OS来决定线程是否执行在不同的核心上。一般来说OS会很好地处理这个问题并且会保证线程在执行期间运行于同一个核心上。

Erlang进程会被不同的调度器运行,因为他们是从一个公共运行队列中被取出,由首先可用的调度器运行。

性能和伸缩性

只有一个调度器的SMP VM要比非SMP的VM稍微慢那么一点点。SMP VM内部需要用到各种锁,不过只要不存在锁的争用,那么由锁引起的开销不会非常大(就是锁争用上面需要花时间)。这也解释了为何在某些情况下,运行多个只有一个调度器的SMP VM要比包含多个调度器的单一SMP VM更加高效。当然运行多个VM要求应用可以按照多个并行任务的方式运行并且之间没有或者几乎不通讯。

一个程序是否能在多核上的SMP VM中良好地进行提升很大程度上取决于程序的性质,某些程序可以保持线性提升至8核甚至16核,同时其他某些程序基本不能提升,连2核都不行。实际应用中很多程序都能在主流市场的核心数上得到提升,见下文。

若并行的持续“通话”由每个核心一个或多个Erlang进程来表示,实际的支持大量通话的电信产品已经先现出在双核和四核处理器上不俗的伸缩性。注意,这些产品是在SMP VM和多核处理器出现很久以前按照普通的Erlang风格来写的,他们也能无须任何修改甚至不需重新编译代码就能从Erlang SMP VM中获益。

SMP性能得到持续改进

SMP实现正被不断改进以便能得到更好的性能和伸缩性。在每个服务发布版R12B-1,2,3,4,5…,R13B等等中,你都能发现新的优化。

一些已知的瓶颈

单一的常见运行队列随着CPU或核心的数量的增加会成为一个显著的瓶颈。

这从4核开始往上就会显现出来,不过4核仍然可以为多数应用程序提供不错的性能。我们正在从事一个每个调度器一个运行队列的解决方法作为目前最重要的改进。

Ets表格会引入锁。在R12B-4之前在每次对一个ets-table的访问中会用到两个锁,但是在R12B-4中meta-table的锁被优化过,可以显著减少争用(前面已经提到争用是有很大代价的)。如果很多Erlang进程访问同一个表格,就会有很多锁争用造成性能降低尤其当这些进程主要工作是访问ets-table。锁存在于表级而非记录级。注意!这也会影响到Mnesia因为Mnesia用到了很多ets-table。

我们关于SMP的策略

当我们开始实现SMP VM的最初,我们就确定了策略:“首先让它可以运行,然后测量,然后优化”。自从2006年五月我们发布了第一个稳定的SMP VM(R11B)以来,我们一直遵循着这个策略。

还有更多已知的东西可以改进,我们会按照性能的收益大小先后各个击破。

我们将主要的精力放在多核(大于4)上更好的连续伸缩性上。

卓越典范

即使SMP系统有还有一些已知的瓶颈不过已经有不错的整体性能和伸缩性,同时我相信在让程序员利用多核机器事半功倍方面,我们是一个卓越的典范。

并行的快速排序

快速排序是一种应用了“分治法”的算法,所谓分治法(divid & conquer),就是把大的问题分割为结构相同的小问题,然后进行解决的方法。因为都是相同的小问题,所以我们可以利用并行计算来并行处理这些问题,使用Erlang可以非常方便非常清晰地利用这种方法解决问题。
由于Erlang是一种声明式、函数式编程语言,所以要表述基本的快速排序算法非常方便:

其中F是一个用于比较的函数。这种描述方式非常精炼,不过效率不高,原因在于1. 不是尾递归, 2.  该代码中应用了两个列表领悟( list comprehension ),其结果是对同一个列表进行了两次遍历。不过这些问题我们稍后考虑。
在erlang中,创建和维护进程是非常方便的,可以很容易地将以上的顺序算法变成创建单独的进程解决对每个子问题:

解决父任务的进程通过接受子进程发来的消息来获得结果。然而,这个代码除了前面的问题之外,也有两个问题:1. 粒度太小,如果小到连一个空列表都要生成一个新的进程来解决问题,那么创建进程的开销就会大过计算本身。2. Programming Erlang中讲到,并发编程要注意“小消息,大计算”,这里在消息中频繁传递列表是开销较大的操作。

我通过生成1000个0-1000之间的随机数的列表来测试这两个算法的性能,在我的迅驰1.8G的本上,结果表示:

  • lists:sort 1.45ms
  • qsort  9.44ms
  • psort  30.75ms

看来我的这两个实现还是比较低效的。首先关于qsort的两个问题,1. 遍历了两次列表,可以通过单独使用一个分区函数来遍历一次完成对列表的划分;2. 不是尾递归,可以使用一个累加器来收集结果,此法可以将其中一个递归变成尾递归(我还无法做到两个都消除)。

此为改进版的快速排序。在此基础上,再解决并行快速排序的两个问题:1. 粒度太小:可以设置一个阈值,当列表长度小于多少时,调用快速排序解决问题;2. 消息太大——我还没想到解决方案。

测试结果为:

  • qsort3: 5.31ms
  • psort3: 7.76ms

另外,我发现,如果从命令行中传递比较函数,则性能下降非常大,不知何故。

Programming in Erlang

上上周搬了家,把宽带迁了,借着青黄不接的几天不能上网的光景,看完了”Programming in Erlang“。

Erlang是一门面向并发和分布式的编程语言。Programming in Erlang以非常浅显的语言,介绍了Erlang的各个方面的基础,是一本合适的入门教材。作为入门教材,能够提到OTP的一些常用内容和常用的分布式设计的思想,已经非常不错了,加上书后的参考。我更感兴趣的是如何使用erlang来构建高并发、分布式系统,包括它的设计模式、部署模式等等,并没有在本书中涉及,非常关键的OTP、Mnesia也仅仅只涉及了一些基础。当然它还是给出了深入学习的途径。

看完了本书,加上翻阅了一些资料,我对Erlang有了更深入的了解,也纠正了一些错误概念:

过去曾认为Erlang就代表高并发,高并发就代表高性能,同时认为Erlang就只是高并发的代名词。其实,Erlang的并发性能也并非最强。首先Erlang的虚拟机是C写的,其次诸如Haskell、OCaml + JoCaml,以及Python的PyPy,其实都能提供不弱于Erlang的并发能力。

同时Erlang在顺序计算上的性能也不佳,甚至不能超过Python、Lua这类脚本语言(也许是和其本身的特殊性质有关)。所以就更落后于Haskell和OCaml这类可编译的语言。然而即使并发和并行也需要顺序计算,所以单从并发性能上来说,Erlang还不是最强的。

Erlang写的Yaws这个Web服务器,虽然能承受高并发,但性能,尤其是IO性能依然无法与lighttpd、nginx这种C写的Web服务相比——Erlang的虚拟机本身也是C写的。

同时,Erlang本身设计为一个结构化编程语言而非面向对象编程语言。这就令其较难应用更有开发效率的面向对象的项目开发流程和方法。

那么为何Erlang在并发、分布式编程中如此受推崇?我认为因为它本身是并发和并行在语义、处理上的一致,所以它编写此类程序更加方便。同时,它提供的是一个关于并发、并行以及分布式的统一的解决方案,而不仅仅是高并发。它的目的是构建有容错能力、有伸缩性的可靠系统,同时又能最简化编程工作。这就是它的真正的优势。