使用rebar生成releases和upgrades

在我对 rebar 的实验中,我制作了一个简单的示例应用程序来测试upgrades和releases。本文将引导您使用 rebar 创建应用程序、正确布局、打包和部署它,以及创建和安装新版本而无需停机。

本文随附的代码位于github.com/RJ/erlang_rebar_example_project的各个分支中。

注意:如果您想了解 Erlang 应用程序和版本的 OTP 方法概述,OTP 设计原则文档是一个很好的起点。但是,rebar(还)不是 OTP 的一部分,因此请考虑背景阅读。rebar使事情变得更容易。

创建项目

构建rebar:

$ cd ~/src
$ git clone https://github.com/basho/rebar.git
Initialized empty Git repository in /tmp/rebar/.git/
remote: Counting objects: 2651, done.
remote: Compressing objects: 100% (1344/1344), done.
remote: Total 2651 (delta 1540), reused 2227 (delta 1174)
Receiving objects: 100% (2651/2651), 622.99 KiB | 495 KiB/s, done.
Resolving deltas: 100% (1540/1540), done.
$ cd rebar && make
...snip....
==> rebar (compile)
Congratulations! You now have a self-contained script called "rebar" in
your current working directory. Place this script anywhere in your path
and you can use rebar to build OTP-compliant apps.

现在我们将创建一个名为“dummy_proj”的项目目录,将 rebar 复制到其中,并使用 rebar 生成一个骨架应用程序:

$ mkdir -p ~/src/dummy_proj/apps
$ cd ~/src/dummy_proj/
$ cp ../rebar/rebar .
$ cd apps
$ ../rebar create-app appid=dummy_proj
==> dummy_proj (create-app)
Writing src/dummy_proj.app.src
Writing src/dummy_proj_app.erl
Writing src/dummy_proj_sup.erl

在骨架中,我添加了一个名为 dummy_proj_server 的基本 gen_server,它只是跟踪它被推动的次数,即它保持一些状态,用于演示目的。

我还将 dummy_proj_app.erl 重命名为 dummy_proj.erl,并添加了一个 start/0 函数,该函数在开发期间启动应用程序时很有用,而不是从生成的版本运行。

用rebar编译

你需要一个 rebar.conf,把它放在顶层项目目录中:

{sub_dirs, [
            "apps/dummy_proj",
            "rel"
           ]}.
{erl_opts, [debug_info, fail_on_warning]}.

{require_otp_vsn, "R14"}.

现在你这样做,进行编译:

$ ./rebar compile
==> dummy_proj (compile)
Compiled src/dummy_proj_sup.erl
Compiled src/dummy_proj.erl
Compiled src/dummy_proj_server.erl
==> rel (compile)
==> dummy_proj (compile)

请注意,您现在在 apps/dummy_proj/ebin/ 中有 .beam 文件,并且 .app.src 为您生成了 apps/dummy_proj/ebin/dummy_proj.app 以及完整的模块列表。

注意:我做了一个简单的 Makefile 调用’rebar compile’,因为我太习惯于输入 make。可以在 git repo 中找到它。

运行您的应用程序(development)

以下是启动应用程序的方法(包含sasl,以便提供良好的错误报告):

$ erl -pa apps/*/ebin -boot start_sasl -s dummy_proj
...snip...
=INFO REPORT==== 16-Mar-2011::14:17:04 ===
Starting dummy_proj application...

=PROGRESS REPORT==== 16-Mar-2011::14:17:04 ===
supervisor: {local,dummy_proj_sup}
started: [{pid,<0.45.0>},
{name,dummy_proj_server},
{mfargs,{dummy_proj_server,start_link,[]} },
{restart_type,permanent},
{shutdown,5000},
{child_type,worker}]

=PROGRESS REPORT==== 16-Mar-2011::14:17:04 ===
application: dummy_proj
started_at: nonode@nohost
Eshell V5.8.1  (abort with ^G)
1> dummy_proj_server:num_pokes().
0
2> dummy_proj_server:poke().     
{ok,1}
3> dummy_proj_server:poke().
{ok,2}
4> dummy_proj_server:num_pokes().
2
5>  

现在你有一个很好的结构合理的 Erlang 项目,你可以用 rebar 编译它。使用 q() 退出 VM。让我们使用 rebar 将其打包,这样您就可以将其部署到生产环境上。

生成您的第一个版本

当您使用 rebar 生成版本时,实际上如果您手动使用 erlang 工具(不推荐,期望使用 rebar工具),您最终会将整个 Erlang VM 和所需的库打包在一个目录下。

这意味着您拥有一个包含 Erlang、您需要的 OTP 库以及所有应用程序代码和依赖项的自包含环境。您可以将其打包,将其传送到另一台机器(具有相同架构,例如 GNU/Linux 64 位),然后在那里运行它

创建节点配置

使用 rebar 在 rel 子目录中创建默认节点配置:

$ mkdir rel
$ cd rel/
$ ../rebar create-node nodeid=dummynode
==> rel (create-node)
Writing reltool.config
Writing files/erl
Writing files/nodetool
Writing files/dummynode
Writing files/app.config
Writing files/vm.args

您需要稍微编辑一下 reltool.config;指向您的应用程序目录,并确保版本号与您的 .app.src 文件匹配。您还应该将 dummy_app 添加到作为发布的一部分启动的应用程序列表中。这是我的 v1 标签中的 reltool.conf

生成发布

回到顶层目录,只需运行:

$ ./rebar generate
==> rel (generate)

现在看看 rel/dummynode。这是包含运行应用程序所需的所有内容的发布目录。

稍后我们将创建更多版本,因此将 rel/dummynode 重命名为 rel/dummynode_first,然后使用 rebar 为我们创建的便捷脚本启动它:

$ cd rel/dummynode_first
$ ./bin/dummynode console
...snip...
Erlang R14B (erts-5.8.1) [source] [64-bit] [smp:8:8] [rq:8] [async-threads:5] [hipe] [kernel-poll:true
=INFO REPORT==== 16-Mar-2011::13:29:59 ===
Starting dummy_proj application...
Eshell V5.8.1  (abort with ^G)
(dummynode@127.0.0.1)1> 
(dummynode@127.0.0.1)1> dummy_proj_server:num_pokes().
0
(dummynode@127.0.0.1)2> dummy_proj_server:poke().     
{ok,1}
(dummynode@127.0.0.1)3> dummy_proj_server:poke().
{ok,2}
(dummynode@127.0.0.1)4> dummy_proj_server:num_pokes().
2
(dummynode@127.0.0.1)5>

当前的版本正在运行,我们再也不想重新启动它,所以打开另一个控制台,因为我们希望在处理版本 2 时保持它运行。

在生产环境中,您可以从“./bin/dummynode start”开始,以便它在后台运行,然后使用“dummynode attach”来获取控制台。

check github 上的“v1 分支”以获取到目前为止的代码。

升级到版本 2

将 poke_twice() 函数添加到 dummy_proj_server。

在 apps/dummy_proj.app.src 和 rel/reltool.conf 中将版本从“1”更改为“2”。

这是 v1…v2 之间的 github差异

Erlang 应用程序版本号可以是任何字符串——我倾向于使用带有字母的日期格式:“20110316a”,但你可以使用任何你想要的方案。我在 git 中使用与 erlang 应用程序相同的版本标记发布。为简单起见,我们在这里只使用“1”、“2”、“3”。

注意:如果您在 .app.src 中通过{vsn, git}使用版本,rebar 将从最近的 git 标签中获取版本字符串。

现在构建新版本:

$ ./rebar compile
$ ./rebar generate

所以现在你有了 rel/dummy_proj,其中包含版本 2 的完整版本(包括 VM)。如果你不关心在线升级,你可以杀死你的版本 1 VM,并从这个新的发布目录启动版本 2。

编写 .appup 升级说明

为了进行升级,您必须拥有一个有效的 .appup 文件。这告诉 erlang release_handler 如何在应用程序的特定版本之间升级和降级。

Rebar 有一个(相对较新的)命令,称为“generate-appups”。我将展示它是如何工作的,但最终我们将手动编写我们的 .appup,并将其保存在我们的项目目录中(在 git 中)。

$ ./rebar generate-appups previous_release=dummynode_first
==> rel (generate-appups)
Generated appup for dummy_proj
Appup generation complete
$ cat ./rel/dummynode/lib/dummy_proj-2/ebin/dummy_proj.appup
%% appup generated for dummy_proj by rebar ("2011/03/16 13:37:43"                                   
{"2", [{"1", [{update,dummy_proj_server,{advanced,[]}}]}], [{"1", []}]}.¬

摆脱自动生成的文件,并在apps/dummy_proj/ebin/dummy_proj.appup 中手动创建appup 文件:

{"2", 
    %% Upgrade instructions from 1 to 2
    [{"1", [
        {load_module, dummy_proj_server}    
    ]}], 
    %% Downgrade instructions from 2 to 1
    [{"1",[
        {load_module, dummy_proj_server}    
    ]}]
}.

此 .appup 包含在版本“2”和“1”之间升级和降级的说明。通常,降级说明与升级说明相反。由于我们只是在我们的服务器进程中添加了一个函数,没有改变任何内部状态,我们可以使用 load_module 指令。Appup Cookbook 深入解释了各种升级说明。

现在再次生成,覆盖之前的版本 2。这将确保 .appup 是发布目录的一部分:

$ ./rebar generate -f

现在,创建升级包:

$ ./rebar generate-upgrade previous_release=dummynode_first
==> rel (generate-upgrade)
dummynode_2 upgrade package created

generate-upgrade 命令将查找 rel/dummynode 作为当前版本,并查找 rel/dummynode_first 作为以前的版本。它应该在 rel 中创建了升级 .tar.gz:

$ ls -lh rel/
total 15M
drwxr-xr-x 8 rj rj 4.0K 2011-03-16 13:42 dummynode
drwxr-xr-x 8 rj rj 4.0K 2011-03-16 13:29 dummynode_first
-rw-r--r-- 1 rj rj  14M 2011-03-16 13:45 dummynode_2.tar.gz
drwxr-xr-x 2 rj rj 4.0K 2011-03-16 13:11 files
-rw-r--r-- 1 rj rj  922 2011-03-16 13:36 reltool.config

安装升级包

您仍然应该让虚拟机从 dummynode_first 运行。确保调用了 poke(),因此内部状态不是默认值。这将有助于说明升级工作无缝。

将升级包复制到正在运行的版本的releases目录下:

$ cp rel/dummynode_2.tar.gz rel/dummynode_first/releases

现在,在运行版本 1 的 Erlang 控制台中,我们使用 release_handler 检查当前可用的版本,并安装我们的新版本:

(dummynode@127.0.0.1)5> release_handler:which_releases().
[{"dummynode","1",[],permanent}]
(dummynode@127.0.0.1)6> release_handler:unpack_release("dummynode_2").
{ok,"2"}
(dummynode@127.0.0.1)7> release_handler:install_release("2").
{ok,"1",[]}   
(dummynode@127.0.0.1)8> dummy_proj_server:num_pokes().
2
(dummynode@127.0.0.1)9> dummy_proj_server:poke_twice().
{ok,4}
(dummynode@127.0.0.1)10> dummy_proj_server:num_pokes(). 
4
(dummynode@127.0.0.1)11> release_handler:which_releases().
[{"dummynode","2",
  ["kernel-2.14.1","stdlib-1.17.1","dummy_proj-2",
   "sasl-2.1.9.2","compiler-4.7.1","crypto-2.0.1",
   "syntax_tools-1.6.6","edoc-0.7.6.7","et-1.4.1","gs-1.5.13",
   "hipe-3.7.7","inets-5.5","mnesia-4.4.15","observer-0.9.8.3",
   "public_key-0.8","runtime_tools-1.8.4.1","ssl-4.0.1",
   "tools-2.6.6.1","webtool-0.8.7","wx-0.98.7","xmerl-1.2.6"],
  current},
 {"dummynode","1",[],permanent}]

升级成功;您可以看到 num_pokes() 被保留,并且新的 poke_twice() 函数可用。

release_handler 将我们的版本 2 显示为“当前”,将原始版本 1 显示为“永久”。这意味着虽然版本 2 现在正在运行,但如果您重新启动 VM,版本“1”将启动。

如果您对升级感到满意,请将其永久化,这意味着如果您重新启动 VM,它将启动而不是版本 1:

(dummynode@127.0.0.1)12> release_handler:make_permanent("2").

到目前为止,请check github 上的“v2 分支”以获取代码。

版本 3 及更高版本

从 v1 升级到 v2 很简单:我们只是在不更改内部 #state 记录的情况下添加了一个乐趣。

Erlang .appup 文件可以做各种聪明的事情,允许您在升级过程中重新连接正在运行的应用程序。

Appup Cookbook详细介绍了您可以在 .appup 中放入的各种命令。

让我们使用更复杂的应用程序进行升级 – 我们将更改 dummy_proj_server 进程中的#state 记录。

对于版本 3,我们将跟踪 prods 和 pokes,这将需要状态记录中的另一个字段。

这是 v2…v3 之间的 github差异

查看此版本对 .appup 的补充:

{"3", 
    %% Upgrade instructions
    [{"2", [
        {update,dummy_app_server,{advanced,[from2to3]}}
    ]}], 
    %% Downgrade instructions
    [{"2",[
        {update,dummy_app_server,{advanced,[from3to2]}}
    ]}]
}.

此 {update..} 指令将导致在 dummy_app_server 上调用 code_change 函数。code_change 的目的是将状态从旧的 (v2) 格式更改为新的 (v3) 格式。

虽然这不是绝对必要的,但我在 code_change 调用中将“from2to3”作为“Extra”字段传递。这可以进行模式匹配,并在您的 code_change 代码中明确说明预期的版本升级。

打包并升级到 v3

移动为 v2 生成的发布目录:

$ mv rel/dummynode rel/dummynode_2

为 v3 编译生成,然后创建升级包:

$ ./rebar compile
$ ./rebar generate
$ ./rebar generate-upgrade previous_release=dummynode_2

和之前一样,将升级包复制到正在运行的版本的releases目录下:

$ cp rel/dummynode_3.tar.gz rel/dummynode_first/releases

现在,在您将 v1 升级到 v2 的 Erlang 控制台中:

(dummynode@127.0.0.1)12> release_handler:unpack_release("dummynode_3").
{ok,"3"}
(dummynode@127.0.0.1)13> release_handler:install_release("3").
{ok,"2",[]}

恭喜

现在您可以以正确的 OTP 方式部署热代码升级。非常适合更改内部状态或确实需要特殊升级的复杂、大型升级。阅读 Appup Cookbook 几次,并在部署之前在测试环境中测试您的升级包。您可以将实时环境 tar 打包并复制到您的测试环境箱,以获得生产系统的精确克隆以测试升级。

Related Posts

2021 年你需要知道的关于 Erlang 的一切

今天,我们将看一个相当古老且有些古怪的东西。 你们大多数人可能没有注意到的语言。 虽然 Erlang 不像某些现代编程语言那样流行,但它安静地运行着 WhatsApp 和微信等每天为大量用户提供服务的应用程序。 在这篇文章中,我将告诉你关于这门语言的更多事情、它的历史,以及你是否应该考虑自己学习它。 ## 什么是 Erlang,它在哪里使用? Erl

Read More

Erlang JIT中基于类型的优化

这篇文章探讨了 Erlang/OTP 25 中基于类型的新优化,其中编译器将类型信息嵌入到 BEAM 文件中,以帮助JIT(即时编译器)生成更好的代码。 ## 两全其美 OTP 22 中引入的基于SSA的编译器处理步骤进行了复杂的类型分析,允许进行更多优化和更好的生成代码。然而,Erlang 编译器可以做什么样的优化是有限制的,因为 BEAM 文件必须

Read More

Erlang JIT之路

自从Erlang 存在,就一直有让它更快的需求和野心。这篇博文是一堂历史课,概述了主要的 Erlang 实现以及如何尝试提高 Erlang 的性能。 ## Prolog 解释器 Erlang 的第一个版本是在 1986 年在 Prolog 中实现的。那个版本的 Erlang 对于创建真正的应用程序来说太慢了,但它对于找出Erlang语言的哪些功能有用,哪

Read More