38、OTP Releases:构建与部署Elixir系统的最佳实践

OTP Releases:构建与部署Elixir系统的最佳实践

1. 生产环境启动系统

在生产环境中启动系统时,可以通过设置 MIX_ENV 环境变量来实现。具体操作如下:

$ MIX_ENV=prod elixir -S mix run --no-halt

此命令会重新编译代码及其所有依赖项,所有 .beam 文件会存储在 _build/prod 文件夹中,并且 Mix 会确保 BEAM 实例从该文件夹加载文件。

需要注意的是,开发环境下编译的代码并未经过优化,虽然开发环境便于开发,但代码执行效率较低。因此,在评估系统在高负载下的性能时,应始终在生产环境中进行编译。若在开发环境中进行测量,可能会对瓶颈产生误判,从而浪费时间和精力去优化在生产环境中并无问题的代码。

不过,使用 mix elixir 启动系统存在一些缺点。首先,需要在主机上编译项目,这意味着系统源代码必须存在于主机上,并且需要获取并编译所有依赖项。因此,需要在目标主机上安装编译所需的所有工具,包括 Erlang Elixir Hex Mix 以及在 Mix 工作流中集成的任何第三方工具。此外,如果在同一台机器上运行多个系统,协调不同系统所需的支持工具的不同版本会变得越来越困难。而 OTP 发布则是解决这些问题的有效方法。

2. OTP 发布概述

OTP 发布是一个独立的、已编译的、可运行的系统,它由系统所需的最少 OTP 应用程序集组成。 OTP 发布还可以选择包含最少的 Erlang 运行时二进制文件,从而使发布完全自给自足。发布不包含源代码、文档文件或测试等工件。

这种方法具有诸多优点。首先,可以在开发机器或构建服务器上构建系统,只传输二进制工件,主机无需安装任何工具。如果将最少的 Erlang 运行时嵌入到发布中,甚至无需在生产服务器上安装 Elixir Erlang ,运行系统所需的一切都将包含在发布包中。此外,发布简化了一些操作任务,如连接到正在运行的系统并在系统上下文中执行自定义 Elixir 代码。最后,发布为系统的在线升级(和降级)铺平了道路,在 Erlang 中称为发布处理。

3. 构建发布

要构建发布,需要编译主 OTP 应用程序及其所有依赖项,然后将所有二进制文件与 Erlang 运行时一起包含在发布中。可以使用 mix release 命令来完成这一操作,具体步骤如下:
1. 进入项目目录:

$ cd to-do
  1. 构建开发环境下的发布:
$ mix release
* assembling todo-0.1.0 on MIX_ENV=dev
* using config/runtime.exs to configure the release at runtime
Release created at _build/dev/rel/todo
...

由于发布通常用于生产环境,因此建议在生产环境中进行构建。可以通过在命令前加上 MIX_ENV=prod 来实现,或者在 mix.exs 中强制设置发布任务的默认环境。以下是在 mix.exs 中设置默认环境的示例:

defmodule Todo.MixProject do
  ...
  def cli do
    [
      preferred_envs: [release: :prod]
    ]
  end
  ...
end

通过上述更改,执行 mix release 命令时,项目将在生产环境中编译并生成发布:

$ mix release
* assembling todo-0.1.0 on MIX_ENV=prod
...

mix release 完成后,发布将位于 _build/prod/rel/todo/ 子文件夹中。

4. 使用发布

与发布进行交互的主要工具是位于 _build/prod/rel/todo/bin/todo 的 shell 脚本,它可以执行多种任务,如下所示:
| 任务 | 命令 |
| ---- | ---- |
| 前台启动系统和 iex shell | $ RELEASE_NODE="todo@localhost" _build/prod/rel/todo/bin/todo start_iex |
| 后台启动系统 | $ RELEASE_NODE="todo@localhost" _build/prod/rel/todo/bin/todo daemon |
| 停止正在运行的系统 | $ RELEASE_NODE="todo@localhost" _build/prod/rel/todo/bin/todo stop |
| 连接到正在运行的系统的远程 shell | $ RELEASE_NODE="todo@localhost" _build/prod/rel/todo/bin/todo remote |

以下是具体操作示例:
- 前台启动系统和 iex shell

$ RELEASE_NODE="todo@localhost" _build/prod/rel/todo/bin/todo start_iex
Starting database worker.
Starting database worker.
Starting database worker.
Starting to-do cache.
iex(todo@localhost)1>

这里设置了 RELEASE_NODE 环境变量为所需的节点名称,若不设置, Elixir 将根据主机名选择默认值。若要使用长节点名,还需设置 RELEASE_DISTRIBUTION 环境变量。

  • 后台启动系统
$ RELEASE_NODE="todo@localhost" _build/prod/rel/todo/bin/todo daemon

此命令通过 run_erl 工具启动系统,该工具会将标准输出重定向到 _build/prod/rel/todo/tmp/log 文件夹中的日志文件,方便分析系统的控制台输出。

  • 连接到正在运行的系统的远程 shell
$ RELEASE_NODE="todo@localhost" _build/prod/rel/todo/bin/todo remote
iex(todo@localhost)1>

此时,将在生产节点的上下文中运行一个 iex shell 会话。按两次 Ctrl-C 退出 shell 只会停止远程 shell, todo 节点仍将继续运行。

  • 停止正在运行的系统
$ RELEASE_NODE="todo@localhost" _build/prod/rel/todo/bin/todo stop

此外,还可以直接连接到正在运行的进程的 shell,这可以捕获正在运行的节点的标准输出。操作步骤如下:
1. 后台启动带有 iex 运行的发布:

$ RELEASE_NODE="todo@localhost" _build/prod/rel/todo/bin/todo daemon_iex
  1. 使用 to_erl 工具连接到 shell:
$ _build/prod/rel/todo/erts-13.0/bin/to_erl _build/prod/rel/todo/tmp/pipe/
iex(todo@localhost)1>
[memory_usage: 70117728, process_count: 230]

需要注意的是,连接到 shell 时要小心。与远程 shell 不同,连接的 shell 在正在运行的节点的上下文中运行,通过操作系统管道连接到正在运行的节点,因此一次只能有一个连接会话。此外,按 Ctrl-\ 可能会意外停止正在运行的节点,应按 Ctrl-D 从正在运行的节点分离而不停止它。

若要获取帮助,只需在不提供任何参数的情况下调用 _build/prod/rel/todo/bin/todo ,这将在标准输出中打印帮助信息。更多关于构建发布的详细信息,请参考官方 Mix 文档:https://hexdocs.pm/mix/Mix.Tasks.Release.html。

5. 发布内容

一个完全独立的发布包含以下内容:
- 运行系统所需的已编译 OTP 应用程序
- 包含将传递给虚拟机的参数的文件
- 描述需要启动哪些 OTP 应用程序的启动脚本
- 包含 OTP 应用程序环境变量的配置文件
- 用于启动、停止和与系统交互的辅助 shell 脚本
- Erlang 运行时二进制文件

这些内容都位于 _build/prod/rel/todo 文件夹中,下面详细介绍其中一些重要部分。

5.1 编译后的二进制文件

所有必需应用程序的编译版本位于 _build/prod/rel/todo/lib 文件夹中,例如:

$ ls -1 _build/prod/rel/todo/lib
asn1-5.1
compiler-8.3
cowboy-2.10.0
cowboy_telemetry-0.4.0
cowlib-2.12.1
crypto-5.2
eex-1.15.0
elixir-1.15.0
iex-1.15.0
kernel-9.0
logger-1.15.0
mime-2.0.3
plug-1.14.2
plug_cowboy-2.6.1
plug_crypto-1.2.5
poolboy-1.5.2
public_key-1.14
ranch-1.8.0
runtime_tools-2.0
sasl-4.2.1
ssl-11.0
stdlib-5.0
telemetry-1.2.1
todo-0.1.0

这个列表包括所有运行时依赖项,包括直接依赖项(在 mix.exs 中指定)和间接依赖项(依赖项的依赖项)。此外,一些 OTP 应用程序,如 kernel stdlib elixir ,会自动包含在发布中,它们是任何基于 Elixir 的系统所需的核心 OTP 应用程序。最后, iex 应用程序也包含在内,这使得可以运行远程 iex shell。

每个文件夹中都有一个 ebin 子文件夹,编译后的二进制文件和 .app 文件位于其中。每个 OTP 应用程序文件夹还可能包含 priv 文件夹,其中包含特定于应用程序的附加文件。

如果需要在发布中包含额外的文件,最好的方法是在项目根目录下创建一个 priv 文件夹,该文件夹(如果存在)会自动出现在发布的应用程序文件夹下。当需要访问 priv 文件夹中的文件时,可以使用 Application.app_dir(:an_app_name, "priv") 来查找该文件夹的绝对路径。

通过查看加载路径可以证明发布是独立的:

$ RELEASE_NODE="todo@localhost" _build/prod/rel/todo/bin/todo start_iex
iex(todo@localhost)1> :code.get_path()
[~c"ch13/todo_release/_build/prod/rel/todo/lib/../releases/0.1.0/consolidated",
~c"ch13/todo_release/_build/prod/rel/todo/lib/kernel-9.0/ebin",
~c"ch13/todo_release/_build/prod/rel/todo/lib/stdlib-5.0/ebin",
~c"ch13/todo_release/_build/prod/rel/todo/lib/compiler-8.3/ebin",
~c"ch13/todo_release/_build/prod/rel/todo/lib/elixir-1.15.0/ebin",
~c"ch13/todo_release/_build/prod/rel/todo/lib/sasl-4.2.1/ebin",
~c"ch13/todo_release/_build/prod/rel/todo/lib/logger-1.15.0/ebin",
~c"ch13/todo_release/_build/prod/rel/todo/lib/crypto-5.2/ebin",
~c"ch13/todo_release/_build/prod/rel/todo/lib/cowlib-2.12.1/ebin",
~c"ch13/todo_release/_build/prod/rel/todo/lib/asn1-5.1/ebin",
~c"ch13/todo_release/_build/prod/rel/todo/lib/public_key-1.14/ebin",
~c"ch13/todo_release/_build/prod/rel/todo/lib/ssl-11.0/ebin",
~c"ch13/todo_release/_build/prod/rel/todo/lib/ranch-1.8.0/ebin",
~c"ch13/todo_release/_build/prod/rel/todo/lib/cowboy-2.10.0/ebin",
~c"ch13/todo_release/_build/prod/rel/todo/lib/telemetry-1.2.1/ebin",
~c"ch13/todo_release/_build/prod/rel/todo/lib/cowboy_telemetry-0.4.0/ebin",
~c"ch13/todo_release/_build/prod/rel/todo/lib/eex-1.15.0/ebin",
~c"ch13/todo_release/_build/prod/rel/todo/lib/mime-2.0.3/ebin",
~c"ch13/todo_release/_build/prod/rel/todo/lib/plug_crypto-1.2.5/ebin",
~c"ch13/todo_release/_build/prod/rel/todo/lib/plug-1.14.2/ebin",
~c"ch13/todo_release/_build/prod/rel/todo/lib/plug_cowboy-2.6.1/ebin",
~c"ch13/todo_release/_build/prod/rel/todo/lib/poolboy-1.5.2/ebin",
~c"ch13/todo_release/_build/prod/rel/todo/lib/runtime_tools-2.0/ebin",
~c"ch13/todo_release/_build/prod/rel/todo/lib/todo-0.1.0/ebin",
~c"ch13/todo_release/_build/prod/rel/todo/lib/iex-1.15.0/ebin"]

可以看到,所有加载路径都指向发布文件夹。相比之下,当启动一个普通的 iex -S mix shell 并运行 :code.get_path/0 时,会看到一个长得多的加载路径列表,其中一些指向构建文件夹,另一些指向系统 Elixir Erlang 的安装路径。这表明发布是自给自足的,运行时只会在发布文件夹中查找模块。

此外,发布中还包含最少的 Erlang 二进制文件,它们位于 _build/prod/rel/todo/erts-X.Y 中,其中 X.Y 对应于运行时版本号(与 Erlang 版本号无关)。包含 Erlang 运行时使发布完全独立,并且允许在同一台机器上运行由不同 Elixir Erlang 版本驱动的多个系统。

5.2 配置文件

配置文件位于 _build/prod/rel/todo/releases/0.1.0 文件夹中,其中 0.1.0 对应于 todo 应用程序的版本(在 mix.exs 中提供)。该文件夹中最相关的两个文件是 vm.args env.sh

  • vm.args 文件可用于向 Erlang 运行时提供标志,例如 +P 标志用于设置最大运行进程数。
  • env.sh 文件可用于设置环境变量,如前面提到的 RELEASE_NODE RELEASE_DISTRIBUTION

更多关于提供这些文件的自定义版本的详细信息,请参考:https://hexdocs.pm/mix/Mix.Tasks.Release.html#module-vm-args-and-env-sh-env-bat。

6. 在 Docker 容器中打包

在生产环境中运行系统有多种方式,如部署到平台即服务(PaaS),如 Heroku Fly.io Gigalixir ,或者在 Kubernetes 集群中运行,还可以在服务管理器(如 systemd )下将系统作为服务运行。无论选择哪种部署策略,都应尽量将系统作为 OTP 发布来运行,在大多数情况下,这意味着在前台启动发布。有效的启动命令可以是 start_iex start

  • start_iex 命令会同时启动 iex 会话,允许连接到正在运行的 BEAM 节点的 iex shell 并与生产系统进行交互,同时捕获节点的标准输出。但这种方法有风险,因为可能会意外停止节点(按两次 Ctrl-C )。
  • start 命令会在前台启动系统,但不启动 iex 会话,因此无法连接到主 iex shell。不过,仍可以通过建立远程 iex shell 会话与正在运行的系统进行交互,但此时节点的标准输出不会被捕获。

具体的部署步骤取决于所选的策略,由于选项众多,无法一一涵盖。 Phoenix Web 框架的部署指南(https://hexdocs.pm/phoenix/deployment.html)提供了一些流行选择的基本介绍。

下面以在 Docker 容器中运行待办事项系统为例进行说明。Docker 是许多团队选择的流行选项,因为它有助于自动化部署,支持在本地运行类似生产环境的版本,并为各种部署选项(尤其是在云环境中)铺平了道路。这里假设你对 Docker 有一定的了解,若不熟悉,可以参考官方入门指南:https://docs.docker.com/get-started/。

Elixir 项目的 Docker 镜像通常分两个阶段构建。第一阶段(通常称为构建阶段)需要编译代码并组装 OTP 发布;第二阶段将发布复制到最终镜像中,该镜像将部署到目标主机。最终镜像不包含构建工具,如 Erlang Elixir ,因为 OTP 发布本身包含了所需的最少 Erlang Elixir 二进制文件。

构建 Docker 镜像需要在项目根目录下创建名为 Dockerfile 的文件,以下是第一阶段构建 OTP 发布的代码:

ARG ELIXIR="1.15.4"
ARG ERLANG="26.0.2"
ARG DEBIAN="bookworm-20230612-slim"
ARG OS="debian-${DEBIAN}"
FROM "hexpm/elixir:${ELIXIR}-erlang-${ERLANG}-${OS}" as builder
WORKDIR /todo
ENV MIX_ENV="prod"
RUN mix local.hex --force && mix local.rebar --force
COPY mix.exs mix.lock ./
COPY config config
COPY lib lib
RUN mix deps.get --only prod
RUN mix release

此示例中使用的基础 Docker 镜像由 Hex 包管理器团队维护(https://hub.docker.com/r/hexpm/elixir)。需要注意的是,为了简洁起见,这个 Docker 文件比较简单,没有利用 Docker 层缓存。因此,任何源文件的更改都将需要重新编译整个项目,包括所有依赖项。若要更精细地构建镜像,可以参考 Phoenix Web 框架生成的 Dockerfile (https://hexdocs.pm/phoenix/releases.html#containers)。

接下来是构建最终镜像的代码:

ARG DEBIAN="bookworm-20230612-slim"
FROM debian:${DEBIAN}
WORKDIR "/todo"
RUN apt-get update -y && apt-get install -y openssl locales
COPY \
  --from=builder \
  --chown=nobody:root \
  /todo/_build/prod/rel/todo ./
RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen

综上所述, OTP 发布为 Elixir 系统的构建、部署和管理提供了一种高效、独立的解决方案。通过合理利用 OTP 发布和 Docker 容器,可以简化系统的部署流程,提高系统的可维护性和可扩展性。

OTP Releases:构建与部署Elixir系统的最佳实践

7. 总结与建议

为了更好地理解和运用 OTP 发布,下面通过一个流程图来展示整个构建和使用发布的主要流程:

graph LR
    classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
    classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px

    A([开始]):::startend --> B(编译主OTP应用及依赖):::process
    B --> C{选择环境}:::decision
    C -->|开发环境| D(执行 mix release):::process
    C -->|生产环境| E(设置 preferred_envs 或前缀 MIX_ENV=prod):::process
    E --> F(执行 mix release):::process
    D --> G(发布位于 _build/dev/rel/todo):::process
    F --> H(发布位于 _build/prod/rel/todo):::process
    G --> I{选择使用方式}:::decision
    H --> I
    I -->|前台启动带 iex| J(执行 start_iex 命令):::process
    I -->|后台启动| K(执行 daemon 命令):::process
    I -->|停止系统| L(执行 stop 命令):::process
    I -->|连接远程 shell| M(执行 remote 命令):::process
    K --> N(可连接本地 shell):::process
    N --> O(使用 to_erl 工具):::process
    J --> P([结束]):::startend
    K --> P
    L --> P
    M --> P
    O --> P

通过这个流程图,可以清晰地看到从开始构建到使用发布的整个过程。在实际操作中,我们还可以根据不同的需求和场景进行灵活调整。

另外,以下是一些使用 OTP 发布和 Docker 容器的建议:
- 环境管理
- 始终明确区分开发环境和生产环境,确保在生产环境中构建发布时使用正确的配置。
- 合理使用 vm.args env.sh 文件来管理系统的运行参数和环境变量。
- 文件管理
- 利用项目根目录下的 priv 文件夹来包含额外的文件,方便在发布中使用。
- 定期清理不必要的文件,避免发布体积过大。
- Docker 优化
- 学习并使用 Docker 层缓存技术,减少构建时间和资源消耗。
- 参考优秀的 Dockerfile 示例,如 Phoenix Web 框架生成的 Dockerfile,优化自己的构建流程。

8. 常见问题及解决方案

在使用 OTP 发布和 Docker 容器的过程中,可能会遇到一些常见问题,下面通过表格的形式列出并给出相应的解决方案:
| 问题描述 | 可能原因 | 解决方案 |
| ---- | ---- | ---- |
| 发布构建失败 | 依赖项未正确安装、网络问题、配置错误 | 检查网络连接,确保所有依赖项都正确安装,仔细检查 mix.exs 和配置文件 |
| 无法启动发布 | 环境变量设置错误、Erlang 运行时问题 | 检查 RELEASE_NODE RELEASE_DISTRIBUTION 等环境变量是否正确设置,确保 Erlang 运行时版本兼容 |
| Docker 构建缓慢 | 未使用层缓存、依赖项下载慢 | 优化 Dockerfile 以利用层缓存,使用国内镜像源加速依赖项下载 |
| 本地 shell 连接异常 | 管道文件损坏、节点已停止 | 检查管道文件是否存在或损坏,确保节点正在运行 |

9. 未来趋势与展望

随着 Elixir 和 Erlang 生态系统的不断发展,OTP 发布和 Docker 容器的应用也将更加广泛和深入。未来可能会出现以下趋势:
- 自动化程度提高 :更多的工具和框架将支持自动化构建、部署和更新 OTP 发布,进一步简化开发和运维流程。
- 与云服务的集成加强 :OTP 发布和 Docker 容器将更好地与各种云服务集成,如 AWS、Azure 等,提供更强大的云计算能力。
- 安全性增强 :在构建和部署过程中,将更加注重安全性,包括依赖项的安全检查、容器的安全配置等。

为了跟上这些趋势,开发者需要不断学习和掌握新的技术和工具,提高自己的技能水平。同时,要积极参与社区讨论,分享经验和见解,共同推动 Elixir 生态系统的发展。

10. 实战案例分析

为了更直观地展示 OTP 发布和 Docker 容器的应用效果,下面通过一个简单的实战案例进行分析。

假设我们有一个简单的 Elixir Web 应用,用于处理用户的待办事项。我们希望将这个应用部署到生产环境中,并且使用 OTP 发布和 Docker 容器来实现。

10.1 构建 OTP 发布

首先,按照前面介绍的步骤,在生产环境中构建 OTP 发布:

# 设置 preferred_envs
# 在 mix.exs 中添加以下代码
defmodule Todo.MixProject do
  ...
  def cli do
    [
      preferred_envs: [release: :prod]
    ]
  end
  ...
end

# 执行 mix release
$ mix release
* assembling todo-0.1.0 on MIX_ENV=prod
...

发布完成后,发布文件将位于 _build/prod/rel/todo 文件夹中。

10.2 创建 Docker 镜像

接下来,创建 Dockerfile 来构建 Docker 镜像:

# 第一阶段:构建 OTP 发布
ARG ELIXIR="1.15.4"
ARG ERLANG="26.0.2"
ARG DEBIAN="bookworm-20230612-slim"
ARG OS="debian-${DEBIAN}"
FROM "hexpm/elixir:${ELIXIR}-erlang-${ERLANG}-${OS}" as builder
WORKDIR /todo
ENV MIX_ENV="prod"
RUN mix local.hex --force && mix local.rebar --force
COPY mix.exs mix.lock ./
COPY config config
COPY lib lib
RUN mix deps.get --only prod
RUN mix release

# 第二阶段:构建最终镜像
ARG DEBIAN="bookworm-20230612-slim"
FROM debian:${DEBIAN}
WORKDIR "/todo"
RUN apt-get update -y && apt-get install -y openssl locales
COPY \
  --from=builder \
  --chown=nobody:root \
  /todo/_build/prod/rel/todo ./
RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen

然后,使用以下命令构建 Docker 镜像:

$ docker build -t todo-app .
10.3 运行 Docker 容器

最后,使用以下命令运行 Docker 容器:

$ docker run -p 4000:4000 todo-app

现在,我们的 Elixir Web 应用就可以通过 http://localhost:4000 访问了。

通过这个实战案例,我们可以看到使用 OTP 发布和 Docker 容器可以轻松地将 Elixir 应用部署到生产环境中,并且具有很好的独立性和可移植性。

总之,OTP 发布和 Docker 容器为 Elixir 系统的构建和部署提供了强大的支持。通过深入学习和实践,我们可以更好地利用这些技术,提高系统的质量和效率。希望本文对大家有所帮助,让我们一起在 Elixir 的世界中不断探索和进步!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值