百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

Elixir实战:13 运行系统 (2) OTP 发布

zhezhongyun 2025-01-05 00:37 152 浏览

OTP 发行版是一个独立的、编译的、可运行的系统,包含系统所需的最小 OTP 应用集。OTP 发行版可以选择性地包含最小的 Erlang 运行时二进制文件,这使得该发行版完全自给自足。发行版不包含工件,例如源代码、文档文件或测试。

这种方法提供了各种好处。首先,您可以在开发机器或构建服务器上构建系统,并仅交付二进制工件。主机机器不需要安装任何工具。如果您将最小的 Erlang 运行时嵌入到发布中,您甚至不需要在生产服务器上安装 Elixir 和 Erlang。运行系统所需的所有内容都将成为您的发布包的一部分。此外,发布简化了一些操作任务,例如连接到正在运行的系统并在系统上下文中执行自定义 Elixir 代码。最后,发布为系统的系统在线升级(和降级)铺平了道路,这在 Erlang 中称为发布处理。

13.2.1 构建发布

要构建一个发布版本,您需要编译您的主 OTP 应用程序及其所有依赖项。然后,您需要将所有二进制文件与 Erlang 运行时一起包含在发布版本中。这可以通过 mix release 命令完成(https://hexdocs.pm/mix/Mix.Tasks.Release.xhtml)。

让我们看看它的实际效果。前往待办事项文件夹,并运行发布命令:

$ 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 环境中构建发布。由于 release 旨在运行在生产环境中,您通常希望在生产环境中构建它。您可以通过在命令前加上 MIX_ENV=prod 来实现。或者,您可以在 mix.exs 中强制为 release 任务设置默认环境。

清单 13.1 强制执行 release 任务的生产环境 (todo_release/mix.exs)

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

cli 函数可用于为不同的 Mix 任务配置默认的 Mix 环境。该函数必须返回一个包含支持选项的关键字列表。 :preferred_envs 选项是一个关键字列表,其中每个键是任务名称(以原子形式提供),值是该任务所需的默认环境。

通过此更改,您可以调用 mix release ,这将编译您的项目在生产环境中,然后生成发布版本:

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

在 mix release 完成后,发布将位于_build/prod/rel/todo/子文件夹中。我们稍后会讨论发布的内容,但首先,让我们看看您如何使用它。

13.2.2 使用发布

与发布交互的主要工具是位于 _build/prod/rel/todo/bin/todo 的 shell 脚本。您可以使用它执行各种任务,例如:

  • 启动系统并在前台运行 iex shell。
  • 将系统作为后台进程启动。
  • 停止运行系统。
  • 将远程 shell 附加到正在运行的系统。

验证发布是否有效的最简单方法是将系统与 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 将根据主机名选择一个默认值。为了使示例在不同的机器上工作,选择了使用 localhost 作为主机部分的硬编码值。请注意,这是一个短节点名称。如果您想使用长名称,还需要将 RELEASE_DISTRIBUTION 操作系统环境变量设置为值 name 。有关如何配置发布的更多详细信息,请参阅 mix release 文档。

该版本不再依赖于您系统的 Erlang 和 Elixir。它是完全独立的;您可以将_build/prod/rel/todo 子文件夹的内容复制到另一台未安装 Elixir 和 Erlang 的机器上,它仍然可以正常工作。当然,由于该版本包含 Erlang 运行时二进制文件,目标机器必须使用相同的操作系统和架构。

要将系统作为后台进程启动,您可以使用 daemon 命令:

$ RELEASE_NODE="todo@localhost" _build/prod/rel/todo/bin/todo daemon

这与之前提到的分离进程不同。相反,系统是通过 run_erl 工具启动的 (https://erlang.org/doc/man/run_erl.xhtml)。该工具将标准输出重定向到位于 _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 节点仍将继续运行。

如果系统作为后台进程运行,并且您想停止它,可以使用 stop 命令:

$ RELEASE_NODE="todo@localhost" _build/prod/rel/todo/bin/todo stop

也可以直接附加到正在运行的进程的外壳上。附加提供了一个重要的好处:它捕获了正在运行的节点的标准输出。无论正在运行的节点打印什么——例如,通过 IO.puts ——都可以在附加的进程中看到(而远程外壳则不是这样)。

让我们看看它的实际操作。首先,我们将在后台启动发布,同时运行 iex 。这可以通过 daemon_iex 命令完成:

$ RELEASE_NODE="todo@localhost" _build/prod/rel/todo/bin/todo daemon_iex

现在,我们可以使用 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]    ?

捕获控制台的标准输出

在第 10 章中,您添加了一个定期打印内存使用情况和进程计数到标准输出的作业。当您连接到 shell 时,该作业的输出是可见的。相反,当运行远程 shell 时,这个输出将不可见。

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

todo 脚本可以执行各种其他命令。要获取帮助,只需调用 _build/prod/rel/todo/bin/todo 而不带任何参数。这将把帮助信息打印到标准输出。最后,关于构建发布的更多细节,请查看官方 Mix 文档,网址为 https://hexdocs.pm/mix/Mix.Tasks.Release.xhtml。

13.2.3 发布内容

让我们花一些时间讨论一下您发布的结构。一个完全独立的发布由以下部分组成:

  • 编译的 OTP 应用程序是运行您的系统所需的
  • 一个包含将传递给虚拟机的参数的文件
  • 一个描述需要启动哪些 OTP 应用程序的启动脚本
  • 一个包含 OTP 应用程序环境变量的配置文件
  • 一个帮助的 shell 脚本,用于启动、停止和与系统交互
  • Erlang 运行时二进制文件

在这种情况下,所有这些都位于 _build/prod/rel/todo 文件夹中的某个地方。让我们仔细看看发布的一些重要部分。

编译后的二进制文件

所有必需应用程序的编译版本位于 _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") 来查找该文件夹的绝对路径。

将所有所需的 OTP 应用程序捆绑在一起使得发布独立。因为系统包含所有所需的二进制文件(包括 Elixir 和 Erlang 标准库),在目标主机上不需要其他任何东西。

您可以通过查看负载路径来证明这一点:

$ 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 版本驱动的系统。

配置

配置文件位于 _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.xhtml#module-vm-args-and-env-sh-env-bat。

13.2.4 在 Docker 容器中打包

在生产中运行系统有多种方式。您可以将其部署到平台即服务(PaaS),例如 Heroku、Fly.io 或 Gigalixir,或者您可以在 Kubernetes 集群中运行它。另一个选项是通过服务管理器(如 systemd)将系统作为服务运行。

无论您选择哪种部署策略,都应努力将系统作为 OTP 发布运行。在大多数情况下,这意味着在前台启动发布。因此,有效的启动命令是 start_iex 或 start 。

前面的命令还会启动 iex 会话。这允许您附加到正在运行的 BEAM 节点的 iex shell,并在捕获节点的标准输出的同时与生产系统进行交互。另一方面,这种方法是有风险的,因为您可能会意外地停止节点(通过按两次 Ctrl-C)。

相反, start 命令将以前台模式启动系统,但不带 iex 会话。因此,您将无法附加到主 iex shell。您仍然可以通过建立远程 iex shell 会话与运行中的系统进行交互,但在这种情况下,节点的标准输出不会被捕获。

具体的部署步骤取决于所选择的策略。可供选择的选项太多,无法一一覆盖。关于一些流行选择的良好基本介绍可以在 Phoenix web 框架的部署指南中找到(https://hexdocs.pm/phoenix/deployment.xhtml)。

作为一个小例子,让我们看看如何在 Docker 容器中运行待办事项系统。Docker 是许多团队选择的热门选项,因为它有助于自动化部署,支持在本地运行类似生产的版本,并为各种部署选项铺平道路,特别是在云计算领域。本部分假设您对 Docker 有一定的了解。如果不是这样,您可以查看官方的入门指南,网址是 https://docs.docker.com/get-started/。

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

要构建 Docker 镜像,我们需要在项目根目录中创建一个名为 Dockerfile 的文件。以下列表展示了第一个构建阶段,该阶段生成 OTP 版本。

清单 13.2 构建阶段 (todo_release/Dockerfile)

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.xhtml#containers)。

接下来,让我们开始构建最终图像。

清单 13.3 构建最终镜像 (todo_release/Dockerfile)

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
ENV LANG="en_US.UTF-8"
ENV LANGUAGE="en_US:en"
ENV LC_ALL="en_US.UTF-8"
 
CMD ["/todo/bin/todo", "start_iex"]      ?

基础镜像

复制构建的发布版本

? 定义启动命令

首先要注意的是,基础镜像是 Debian,而不是 Elixir 或 Erlang。使用与构建镜像相同的基础操作系统非常重要。否则,您可能会因不兼容而遇到崩溃。

要构建最终镜像,您需要从构建阶段复制 OTP 版本,配置区域设置,并定义默认启动命令。在此示例中,选择了 start_iex 命令,这使得可以附加到正在运行的 shell。

此时,您可以构建映像:

$ docker build . -t elixir-in-action/todo

接下来,您可以启动容器:

$ docker run             \
    --rm -it             \
    --name todo_system   \
    -p "5454:5454"       \    ?
    elixir-in-action/todo

? 将 http 端口发布到主机

您现在可以在本地与系统进行交互:

$ curl -d "" \
  "http://localhost:5454/add_entry?list=bob&date=2023-12-19&title=Dentist"
OK
 
$ curl "http://localhost:5454/entries?list=bob&date=2023-12-19"
2023-12-19 Dentist

与构建阶段一样,生产镜像过于简单。特别是,它不支持通过分布式 Erlang 进行集群,也不支持建立远程 shell(通过 --remsh 开关)。这可以通过一些工作来解决,但为了简洁起见,这里不讨论。如果您想从多个容器建立 Erlang 集群,特别是当它们在 Kubernetes 集群中运行时,请查看 libcluster 库(https://hexdocs.pm/libcluster/)。

这结束了发布的话题。一旦您的系统启动并运行,了解如何分析其行为是很有用的。

相关推荐

Python入门学习记录之一:变量_python怎么用变量

写这个,主要是对自己学习python知识的一个总结,也是加深自己的印象。变量(英文:variable),也叫标识符。在python中,变量的命名规则有以下三点:>变量名只能包含字母、数字和下划线...

python变量命名规则——来自小白的总结

python是一个动态编译类编程语言,所以程序在运行前不需要如C语言的先行编译动作,因此也只有在程序运行过程中才能发现程序的问题。基于此,python的变量就有一定的命名规范。python作为当前热门...

Python入门学习教程:第 2 章 变量与数据类型

2.1什么是变量?在编程中,变量就像一个存放数据的容器,它可以存储各种信息,并且这些信息可以被读取和修改。想象一下,变量就如同我们生活中的盒子,你可以把东西放进去,也可以随时拿出来看看,甚至可以换成...

绘制学术论文中的“三线表”具体指导

在科研过程中,大家用到最多的可能就是“三线表”。“三线表”,一般主要由三条横线构成,当然在变量名栏里也可以拆分单元格,出现更多的线。更重要的是,“三线表”也是一种数据记录规范,以“三线表”形式记录的数...

Python基础语法知识--变量和数据类型

学习Python中的变量和数据类型至关重要,因为它们构成了Python编程的基石。以下是帮助您了解Python中的变量和数据类型的分步指南:1.变量:变量在Python中用于存储数据值。它们充...

一文搞懂 Python 中的所有标点符号

反引号`无任何作用。传说Python3中它被移除是因为和单引号字符'太相似。波浪号~(按位取反符号)~被称为取反或补码运算符。它放在我们想要取反的对象前面。如果放在一个整数n...

Python变量类型和运算符_python中变量的含义

别再被小名词坑哭了:Python新手常犯的那些隐蔽错误,我用同事的真实bug拆给你看我记得有一次和同事张姐一起追查一个看似随机崩溃的脚本,最后发现罪魁祸首竟然是她把变量命名成了list。说实话...

从零开始:深入剖析 Spring Boot3 中配置文件的加载顺序

在当今的互联网软件开发领域,SpringBoot无疑是最为热门和广泛应用的框架之一。它以其强大的功能、便捷的开发体验,极大地提升了开发效率,成为众多开发者构建Web应用程序的首选。而在Spr...

Python中下划线 ‘_’ 的用法,你知道几种

Python中下划线()是一个有特殊含义和用途的符号,它可以用来表示以下几种情况:1在解释器中,下划线(_)表示上一个表达式的值,可以用来进行快速计算或测试。例如:>>>2+...

解锁Shell编程:变量_shell $变量

引言:开启Shell编程大门Shell作为用户与Linux内核之间的桥梁,为我们提供了强大的命令行交互方式。它不仅能执行简单的文件操作、进程管理,还能通过编写脚本实现复杂的自动化任务。无论是...

一文学会Python的变量命名规则!_python的变量命名有哪些要求

目录1.变量的命名原则3.内置函数尽量不要做变量4.删除变量和垃圾回收机制5.结语1.变量的命名原则①由英文字母、_(下划线)、或中文开头②变量名称只能由英文字母、数字、下画线或中文字所组成。③英文字...

更可靠的Rust-语法篇-区分语句/表达式,略览if/loop/while/for

src/main.rs://函数定义fnadd(a:i32,b:i32)->i32{a+b//末尾表达式}fnmain(){leta:i3...

C++第五课:变量的命名规则_c++中变量的命名规则

变量的命名不是想怎么起就怎么起的,而是有一套固定的规则的。具体规则:1.名字要合法:变量名必须是由字母、数字或下划线组成。例如:a,a1,a_1。2.开头不能是数字。例如:可以a1,但不能起1a。3....

Rust编程-核心篇-不安全编程_rust安全性

Unsafe的必要性Rust的所有权系统和类型系统为我们提供了强大的安全保障,但在某些情况下,我们需要突破这些限制来:与C代码交互实现底层系统编程优化性能关键代码实现某些编译器无法验证的安全操作Rus...

探秘 Python 内存管理:背后的神奇机制

在编程的世界里,内存管理就如同幕后的精密操控者,确保程序的高效运行。Python作为一种广泛使用的编程语言,其内存管理机制既巧妙又复杂,为开发者们提供了便利的同时,也展现了强大的底层控制能力。一、P...