版本发布

📘

版本和目标系统到底是什么?

版本是一组启动 Erlang 虚拟机并启动项目所需的应用程序。这通过版本资源文件(.rel)描述,该文件用于生成.script.boot。引导文件是脚本文件的二进制形式,Erlang 运行时系统 (ERTS) 使用它来启动 Erlang 节点,有点像启动操作系统。即使在命令行上运行erl也在使用引导脚本。

目标系统是在另一台机器(虚拟或其他)上能够启动的 Erlang 系统。通常 ERTS 与目标系统捆绑在一起。

有关更多信息,请查看Adopting Erlang中的关于版本的章节

入门

在项目的rebar.config中添加一个relx部分

{relx, [{release, {<release_name>, <release_vsn>},
         [<app>]},
        {release, {<release_name>, <release_vsn>},
         [<app>]},

        {dev_mode, true},
        {include_erts, false},

        {extended_start_script, true}]}.

运行rebar3 release将构建版本并提供一个脚本,用于在_build/<profile>/rel/<release name>/bin/<release name>下启动节点。

<release_name>必须是一个原子。<release_vsn>可以是以下之一

版本类型 结果
string() 使用字符串,按原样作为版本。例如:"0.1.0"
git | semver 使用存储库上的最新 Git 标签来构建版本。
{cmd, string()} 使用在 shell 中执行string()内容的结果。例如,使用文件VERSION{cmd, "cat VERSION | tr -d '[:space:]'"}
{git, short | long} 使用当前提交的简短(8 个字符)或完整 Git ref。
{file, File} 使用文件的内容。例如,使用VERSION文件比使用cmd更好的方法是:{file, "VERSION"}

每个<app>都是应用名称的原子(例如,myapp),或一个元组。有关<app>的元组语法,请参阅应用语法

您可以在项目rebar.config中的relx下添加多个release部分。

您可以只指定共享相同配置的不同版本

{relx, [{release, {<release name>, "0.0.1"},
         [<app>]},

        {release, {<other release name>, "0.1.0"},
         [<app>]},

         {dev_mode, false},
         {include_erts, true},
       ]}.

或者,您还可以通过使用 4 元组release定义来指定具有独立配置的版本

{relx, [{release, {<release name>, "0.0.1"},
         [<app>],
         [{dev_mode, false},
          {include_erts, true}]},
        {release, {<release name>, "0.1.0"},
         [<app>],
         [{dev_mode, true}]}
       ]}.

您可以使用rebar3 release -n <release_name>构建特定的版本

构建配置

应用语法

{release, {myrelease, "0.1.0"},
 [<app1>, <app2>]}

在版本中,每个<app>都是应用名称的原子,或者是一个包含应用名称和其他选项的元组

% No options specified
myapp

% All options specified
{myapp, "1.1.0", transient, [otherapp]}

% Some options specified
{myapp, "1.1.0"}
{myapp, "1.1.0", transient}
{myapp, "1.1.0", [otherapp]}
{myapp, transient}
{myapp, [otherapp]}
{myapp, transient, [otherapp]}

在这种情况下,myapp包含在版本中,其应用版本为"1.1.0",其启动类型为transient,其包含的应用列表为[otherapp]。这些选项直接对应于版本资源文件 (.rel)

 {<app_name>,       % atom()
  <app_vsn>,        % string()
  <start_type>,     % permanent | transient | temporary | load | none
  <included_apps>}  % [atom()]
  • 可以在保留顺序的情况下省略<app_name>之后的元素。
  • 如果包含<app_vsn>(不常见),则它必须与.app.src匹配。否则,relx会从.app.src中确定它。
  • 版本资源文件语法(为完整性起见,在下面重复)使得
    • <start_type>默认为permanent
    • <included_apps>默认为.app.src中的included_apps。如果指定,则必须是.app.srcincluded_apps的子集。

版本中包含源代码

默认情况下,版本将包含应用程序的源文件(如果存在)。

如果您不想包含源文件,请将include_src设置为false。

{include_src, false}

应用排除

以下允许您从输出版本中删除特定应用程序。

{exclude_apps, [app1, app2]}

应用程序将从版本中任何应用程序的.app文件中删除,这些应用程序在其applications下列出了这些应用程序。

模块排除

以下指令允许您从输出版本中删除应用程序模块。

{exclude_modules, [
    {app1, [app1_mod1, app1_mod2]},
    {app2, [app2_mod1, app2_mod2]}
]}.

模块将从应用程序的.app文件的module列表中删除。

模式

其他模式包括prodminimal

{relx, [...
        {mode, <mode>},
        ...
       ]
}.
模式 扩展选项
dev [{dev_mode, true}, {include_src, true}, {debug_info, keep}, {include_erts, false}]
prod [{include_src, false}, {debug_info, strip}, {include_erts, true}, {dev_mode, false}]
minimal [{include_src, false}, {debug_info, strip}, {include_erts, false}, {dev_mode, false}]

在开发过程中,您可能希望所有对应用程序的更改都立即在版本中可用。relx提供多种模式,包括用于此特定用例的dev模式。它不会将构成版本的应用程序复制到其创建的版本结构中,而是创建符号链接,因此只需编译和重新启动或加载更改的模块即可。

prod模式扩展为通常用于生产版本的选项:include_srcfalse,因此源代码不包含在版本中;debug_info设置为strip,这使得 BEAM 文件更小,并且仅删除工具使用的与您的版本中不太可能包含的数据;并且include_erts设置为true,以捆绑当前的 Erlang 运行时,使您可以将版本复制到兼容的目标并运行,而无需首先安装 Erlang。

📘

Rebar3 Prod 配置文件

rebar3 prod配置文件中构建时,例如使用rebar3 as prod release,则relx prod模式会自动启用。

minimal模式与prod相同,只是它不包含 Erlang 运行时。

您可以通过包含显式设置来覆盖模式扩展的选项。例如,如果您想在 BEAM 模块中保留调试信息,则可以使用以下配置

[
  {mode, prod},
  {debug_info, keep}
]

验证检查

缺失函数

默认情况下,relx将检查版本中包含的项目应用程序的模块中使用的外部函数是否存在。这意味着如果调用了版本中未包含的函数,即使它是rebar.config中的依赖项或 OTP 中包含的应用程序,也会发出警告。

这有助于防止我们所有人都在某个时候遇到的常见错误,即忘记将应用程序添加到依赖它的应用程序的.app.src中。

如果出于某种原因您希望禁用此检查,则可以在relx配置中将其设置为false

{check_for_undefined_functions, false}

过时模块

选项src_tests将在模块的源代码丢失或比对象代码更新时发出警告

{src_tests, true}

这有助于捕获对依赖项源文件的任何修改。由于rebar3 release会自动编译项目中应用程序的所有更改,因此依赖项应该是唯一可能过时的模块。

运行时配置

虚拟机配置

默认情况下,relx将提供一个基本vm.args文件,该文件设置节点名称和 cookie。有关选项及其用法的完整列表,请查看Erlang 文档

## Name of the node
-name {{release_name}}@127.0.0.1

## Cookie for distributed Erlang
-setcookie {{release_name}}

要提供自定义vm.argsvm.args.src,只需在项目根目录的顶级config/目录中创建该文件即可。如果您将其命名为除vm.argsvm.args.src以外的其他名称,则必须添加到relx配置中

{vm_args, "config/vm_prod.args"}

{vm_args_src, "config/vm_prod.args.src"}

应用配置

对于在版本运行时传递应用配置,有sys.configsys.config.src

[
  {<app_name>, [{<key>, <val>}, ...]}
].

如果项目中存在文件config/sys.config.srcconfig/sys.config,则relx将自动将其中一个(如果两者都存在,则.src优先)包含在版本中。

要设置特定文件以用作应用配置文件,可以使用sys_configsys_config_src进行设置

{sys_config, "config/sys_prod.config"}
{sys_config_src, "config/sys_prod.config.src"}

这些文件在包含到版本中时将重命名为sys.configsys.config.src

如果两者都不存在,则使用包含空列表的文件。

config 文档systools 文档中阅读有关 Erlang 配置的更多信息。

环境变量替换

使用 OTP-21+ 和 Rebar3 3.6+

从 Erlang/OTP 21 和 Rebar3 3.6.0 开始,配置选项 sys_config_srcvm_args_src 可用于显式包含将在运行时渲染的模板,将定义为 ${VARIABLE} 的变量替换为 shell 环境中对应的值。从 Rebar3 3.14.0 开始,在使用变量时可以通过将其定义为 ${VARIABLE:-DEFAULT} 来可选地设置默认值。

从 Rebar3 3.14.0 开始,如果配置文件存在,则会将其包含进来,因此,只有在文件未命名为 config/sys.config.srcconfig/vm.args.src 时,您才需要在 relx 配置中包含 {sys_config_src, <filename>}{vm_args_src, <filename>}

%% sys.config.src
[
  {appname, 
   [
    {port, ${PORT:-8080}},
    {log_level, ${LOG_LEVEL:-info}},
    {log_root, "${LOG_ROOT:-/var/log/appname}"}
   ]}
].
# vm.args.src
-name ${NODE_NAME}
%% rebar.config
{relx, [{release, {<release name>, "0.0.1"},
         [<app>]},

        {mode, dev}]}.

使用 .src 文件进行配置时,无需设置 RELX_REPLACE_OS_VARS=true。在下一节中,我们将看到运行时配置的旧形式。

OTP-21 和 Rebar3 3.6 之前

通过设置 RELX_REPLACE_OS_VARS=truevm.argssys.config 文件都可以包含 OS 环境变量,这些变量将被替换为节点启动时环境中的当前值。这意味着用于启动 Web 服务器并在端口上监听的版本的 vm.argssys.config 可能如下所示

# vm.args
-name ${NODE_NAME}
%% sys.config
[
 {appname, [{port, "${PORT}"}]}
].

然后可以用来启动同一个版本的多个节点,并且节点名称不同。

#!/bin/bash

export RELX_REPLACE_OS_VARS=true

for i in `seq 1 10`;
do
    NODE_NAME=node_$i PORT=808$i _build/default/rel/<release>/bin/<release> foreground &
    sleep 1
done

覆盖:构建时配置

覆盖提供了定义文件和模板以包含在目标系统中的能力。例如,用于管理节点的自定义脚本或在 Heroku 上运行所需的 Procfile。

{relx, [
    ...
    {overlay_vars, "vars.config"},
    {overlay, [{mkdir, "log/sasl"},
               {template, "priv/app.config", "etc/app.config"},
               {copy, "Procfile", "Procfile"}]}
]}.

支持的操作有

  • mkdir 用于在发布版本中创建目录
  • copy 用于将文件从本地目录复制到发布版本中的某个位置
  • template 的行为方式与 copy 相同,但其中包含变量扩展。

Relx 的模板化功能公开变量以及完整的 Mustache 模板系统功能(参见 mustache)。您可以查看那里的文档以了解支持的完整语法。

默认情况下提供了一组可用的变量,这些变量将在下一节中描述,否则可以在 {overlay_vars, "vars.config"} 中指定的文件中声明自定义变量,该文件应具有以下格式

%% some variables
{key, value}.
{other_key, other_val}.
%% includes variables from another file
"./some_file.config".

下面定义了默认变量。

预定义的覆盖变量

名称 描述
log 当前日志级别,格式为 (<logname>:<loglevel>)
output_dir 构建版本的当前输出目录
target_dir output_dir 相同;出于向后兼容性而存在
overridden 当前被覆盖的应用列表(应用名称列表)
goals 系统中用户指定的目标列表
lib_dirs 库目录列表,包括用户指定的和派生的目录
config_file 系统中使用的配置文件列表
providers 本次 relx 运行中使用的提供程序名称列表
sys_config sys.config 文件的位置
root_dir 当前项目的根目录
default_release_name 本次 relx 运行的当前默认版本名称
default_release_version 本次 relx 运行的当前默认版本号
default_release 本次 relx 运行的当前默认版本
release_erts_version 正在使用的 Erlang 运行时系统的版本
erts_vsn release_erts_version 相同(出于向后兼容性)
release_name 当前正在执行的版本
release_version 当前正在执行的版本号
rel_vsn release_version 相同。出于向后兼容性而存在
release_applications 版本中包含的应用列表
拆分配置

可以拆分覆盖文件以处理更复杂的情况。为了解释这一点,让我们看一个简化的例子。

我们构建应用程序,并希望通过让覆盖变量 build 拼写出 "prod""dev" 来区分生产和开发构建,以便 app.config 文件可以在其配置中包含它,并且我们可以启用或禁用功能。

为此,我们构建三个覆盖文件

  • dev.config
  • prod.config
  • base.config

对于开发构建,我们将使用 dev.config 作为 overlay_vars,对于生产构建,我们将使用 prod.config

%% base.config
{data_dir, "/data/yolo_app"}.
{version, "1.0.0"}.
{run_user, "root"}.
%% dev.config
%% Include the base config
"./base.config".
%% The build we have
{build, "dev"}.
%% prod.config
%% Include the base config
"./base.config".
%% The build we have
{build, "prod"}.

可部署的压缩包

包含 ERTS

目标系统不能有像使用 dev_mode 时创建的符号链接,并且我们通常希望将 ERTS 与系统一起包含进来,以便它不需要事先安装在目标系统上。

如果使用 prod 配置文件构建版本,Rebar3 将自动将 {mode, prod} 添加到 Relx 配置中。例如

$ rebar3 as prod tar
===> Verifying dependencies...
===> Analyzing applications...
===> Compiling relx_overlays
===> Assembling release myrel-0.1.0...
===> Release successfully assembled: _build/prod/rel/myrel
===> Building release tarball myrel-0.1.0.tar.gz...
===> Tarball successfully created: _build/prod/rel/myrel/myrel-0.1.0.tar.gz

现在可以将 tarball myrel-0.1.0.tar.gz 复制到另一个兼容的系统并启动。

$ mkdir myrel
$ mv myrel-0.1.0.tar.gz myrel/
$ cd myrel
$ tar -zxvf myrel-0.1.0.tar.gz
$ bin/myrel console

不包含 ERTS

当需要将 ERTS 从版本中排除时,可以在 rebar.configprofiles 下设置 prod 配置文件配置。例如,要在目标上使用 ERTS 和基本应用程序(如 kernelstdlib),请在 relx 配置元组中将 mode 设置为 minimal,并将 system_libs 设置为 false

{profiles, [{prod, [{relx, [{mode, minimal},
                            {system_libs, false}]}]}]}.

或手动将 include_erts 设置为 false

{profiles, [{prod, [{relx, [{include_erts, false},
                            {system_libs, false}]}]}]}

现在,当运行 rebar3 as prod tar 时,生成的 tarball 将不包含 ERTS 或 kernelstdlib 等应用程序。

包含为其他系统构建的 ERTS

如果您希望包含一个与运行 rebar3 的版本不同的 Erlang 运行时系统,例如您在 MacOS 上构建,但希望包含为 GNU/Linux 版本构建的 ERTS,则可以为 include_erts 提供路径而不是布尔值,并为 system_libs 提供路径,仍然在 relx 配置元组中

{include_erts, "/path/to/erlang"},
{system_libs, "/path/to/erlang"},

将这些路径与配置文件一起使用可以产生更简单的方法来设置交叉编译。

扩展启动脚本

命令

relx 附带的扩展启动脚本提供了启动和连接到版本的几种方法。

对于本地开发,您可能会使用 console。在生产环境中,您将希望使用 foreground,无论您是在 tmux 等工具中手动启动,还是使用 systemd 等初始化系统,或者在 Docker 容器中运行版本。

要打开使用 foreground 启动的节点上的控制台,请使用 remote_console

完整的命令列表如下。

命令 描述
foreground 将输出发送到标准输出的启动版本
remote 连接到正在运行的节点的远程 shell
console 使用交互式 shell 启动版本
console_clean 在没有版本的应用程序的情况下启动交互式 shell
rpc [Mod [Fun [Args]]]] 在正在运行的节点上运行 apply(Mod, Fun, Args)
eval [Exprs] 在正在运行的节点上运行表达式
status 验证节点是否正在运行,然后运行状态挂钩脚本
restart 重新启动应用程序,但不重新启动虚拟机
reboot 重新启动整个虚拟机
stop 停止正在运行的节点
pid 打印 OS 进程的 PID
ping 如果节点处于活动状态,则打印 pong
daemon 使用 run_erl(命名管道)在后台启动版本
daemon_attach 连接到使用 to_erl(命名管道)作为守护程序启动的节点

版本处理:安装和升级

此外,扩展启动脚本还包含用于使用 release_handler 的命令

命令 描述
unpack [Version] 解压缩版本 tarball
install [Version] 安装版本
uninstall [Version] 卸载版本
upgrade [Version] 将正在运行的版本升级到新版本
downgrade [Version] 将正在运行的版本降级到新版本
versions 打印可用的版本号

要了解这些命令的工作原理,请参阅 OTP 设计原则章节 版本处理

有关包括版本增量和 appup 生成在内的详细工作流程,请查看 Richard Jones 构建在 rebar3 之上的工具 relflow

对于安装版本后的基本版本升级,假设我们有一个名为 myrel 的版本,版本号为 0.0.10.0.2

  • 安装:在正在运行的系统上安装版本将解压缩并升级版本:bin/myrel install 0.0.1
  • 列出:您可以检查当前可用的版本:bin/myrel versions
  • 升级:如果版本已经解压缩,则可以简单地调用 upgrade 命令升级到该版本:bin/myrel upgrade 0.0.2
  • 降级:要降级到先前版本,请使用 downgrade 命令:bin/myrel downgrade 0.0.1

钩子

可以在扩展启动脚本的特定操作上定义挂钩,这些操作为 startstopinstall_upgrade;每个操作的 prepost 挂钩都可用。

挂钩可以是内置的(即它们已包含在版本中)或自定义的(用户为自定义功能编写的脚本)。提供预打包功能的内置脚本为

  • pid:将 BEAM pid 写入可配置的文件位置(默认情况下为 /var/run/<rel_name>.pid)。
  • wait_for_vm_start:等待虚拟机启动(即,当它可以被 ping 时)。
  • wait_for_process:等待可配置名称出现在 Erlang 进程注册表中。
{extended_start_script_hooks, [
  {pre_start, [{custom, "hooks/pre_start"}]},
  {post_start, [
    {pid, "/tmp/foo.pid"},
    {wait_for_process, some_process},
    {custom, "hooks/post_start"}
  ]},
  {pre_stop, [
    {custom, "hooks/pre_stop"}]},
    {post_stop, [{custom, "hooks/post_stop"}]},
  ]},
  {post_stop, [{custom, "hooks/post_stop"}]}
]}.

扩展

生成的扩展启动脚本带有一组内置命令,允许您管理版本:foregroundstoprestart 等。

有时需要公开一些特定于应用程序的自定义命令。例如,如果您正在运行游戏服务器,则只需调用 bin/gameserver games 输出有用的信息会很方便。

扩展启动脚本扩展允许您创建自定义 shell 脚本,该脚本将附加到启动脚本可用的命令列表中。扩展 shell 脚本可以接受参数,并且可以访问启动脚本本身中定义的所有 shell 变量。您可以通过在 rebar.config 中定义扩展来开始,例如

%% start script extensions
{extended_start_script_extensions, [
   {status, "extensions/status"}
]}.

这里您添加了 status 脚本扩展,它将调用 extensions/status shell 脚本。

此路径相对于生成的版本上启动脚本的位置,因此您可能希望使用 overlay 将其放置在正确的位置

{copy, "scripts/extensions/status", "bin/extensions/status"},

扩展脚本本身是标准的 shell 脚本,描述的游戏服务器示例可以用以下方式实现

#!/bin/bash

case $1 in
    help)
        echo "bin/gameserver status"
        ;;
    *)
        ;;
esac

# get the status tuple from gameserver
Status=$(relx_nodetool eval "pool_debug:status(json).")

# now print it out
code="Json = binary_to_list($Status),
      io:format(\"~p~n\", [Json]),
      halt()."
echo $(erl -boot no_dot_erlang -sasl errlog_type error -noshell -eval "$code")

其他配置

可以在运行版本的 target 系统上设置 RELX_RPC_TIMEOUT 环境值,以选择脚本在放弃联系正在运行的 Erlang 系统之前可以等待多长时间。如果未指定值,则默认为 NODETOOL_TIMEOUT 值(从毫秒转换为秒)。如果 NODETOOL_TIMEOUT 本身未设置,则默认为 60 秒。

参考

最后修改于 2022 年 5 月 27 日:修复支持默认值的 rebar3 版本 (6181f59)