OTP应用开发:从基础到发布
1. 监督者:可靠性的核心
在开发可靠的应用程序时,监督者起着至关重要的作用。即使序列工作进程崩溃,它也能重新启动并保留状态。这表明,若要编写可靠的应用,仔细的监督是关键。
监督者的作用体现在构建代码的信任环上。外层代码与外界交互,应尽可能可靠;而内层嵌套的代码可以不那么完美,但每层代码都需知道如何处理下一层代码的失败。监督者有多种策略来处理子进程的终止、重启等情况。使用监督者管理工作进程,能促使开发者在设计应用时考虑可靠性和状态,从而打造出高可用性的应用。例如,OTP曾被用于构建可靠性达99.9999999%的系统。
以下是一个练习:
-
练习OTP - 监督者 - 2
:重新设计栈服务器,使用监督树和单独的存储进程来保存状态。验证其功能,并确保服务器崩溃重启后状态得以保留。
2. OTP应用:与传统应用不同
OTP中的应用与我们通常理解的应用有所不同。在OTP世界里,应用是带有描述符的代码包,描述符会告知运行时代码的依赖关系、注册的全局名称等信息。它更像动态链接库或共享对象,而非传统意义上的独立应用。可以将“应用”理解为“组件”或“服务”。
例如,使用HTTPoison库获取GitHub问题时,实际上安装的是包含HTTPoison的独立应用。Mix会自动加载该应用,HTTPoison又会启动其依赖的其他应用,而这一切对开发者来说是透明的。
3. 应用规范文件
Mix会提及名为
name.app
的文件,这就是应用规范文件,用于向运行时环境定义应用。Mix会根据
mix.exs
中的信息以及编译应用时收集的信息自动创建该文件。运行应用时会参考此文件来加载相关内容。即使应用不使用所有OTP功能,该文件也会被创建和引用。当开始使用OTP监督树时,
mix.exs
中添加的内容会被复制到
.app
文件中。
4. 将序列程序转换为OTP应用
之前的序列应用已经是一个完整的OTP应用。Mix创建初始项目树时,添加了监督者并在
mix.exs
文件中提供了启动应用所需的信息。具体来说,填充了
application
函数:
def application do
[mod: { Sequence, [] }]
end
这表明应用的顶层模块是
Sequence
,OTP会假设该模块实现了
start
函数,并将空列表作为参数传递给它。
为了从
mix.exs
中获取初始值,我们进行如下修改:
- 修改
mix.exs
传递初始值(这里使用456):
def application do
[mod: { Sequence, 456 }]
end
-
修改
sequence.ex代码以使用传递的值:
defmodule Sequence do
use Application
def start(_type, initial_number) do
Sequence.Supervisor.start_link(initial_number)
end
end
可以通过以下命令验证其功能:
$ iex -S mix
Compiling 5 files (.ex)
Generated sequence app
iex> Sequence.Server.next_number
456
mod
选项指定了应用的主入口模块,OTP启动应用时会调用该模块的
start
函数。还可以添加
registered
选项,列出应用将注册的名称,以确保在节点或集群中所有加载的应用中名称唯一。更新后的
mix.exs
配置如下:
# Configuration for the OTP application
def application do
[
mod: { Sequence, 456 },
registered: [ Sequence.Server ]
]
end
运行
mix compile
会编译应用并更新
sequence.app
应用规范文件。生成的
sequence.app
文件位于
_build/dev/lib/sequence/ebin
目录下,其内容如下:
{application,sequence,
[{description,"sequence"},
{vsn,"0.0.1"},
{modules,['Elixir.Sequence','Elixir.Sequence.Server',
'Elixir.Sequence.Stash',
'Elixir.Sequence.SubSupervisor',
'Elixir.Sequence.Supervisor']},
{applications,[kernel,stdlib,elixir]},
{mod,{'Elixir.Sequence',456}},
{registered,['Elixir.Sequence.Server']}]}.
5. 关于应用参数
之前的示例中传递了整数456作为初始参数,更推荐传递关键字列表,因为Elixir提供了
Application.get_env
函数来从代码的任何位置检索这些值。可以这样设置
mix.exs
:
def application do
[
mod: { Sequence, [] },
env: [initial_number: 456],
registered: [ Sequence.Server ]
]
end
然后在
Sequence
模块中使用
get_env
获取值:
defmodule Sequence do
use Application
def start(_type, _args) do
Sequence.Supervisor.start_link(Application.get_env(:sequence, :initial_number))
end
end
6. 监督是可靠性的基础
通过运行OTP序列应用可以看到,启动了两个监督进程和两个工作进程,它们相互协作,即使与我们交互的工作进程崩溃,系统也能继续运行且不丢失状态。其他Erlang进程也能与序列应用交互。
start
函数有两个参数,第二个参数对应
mix.exs
文件中
mod
选项指定的值,第一个参数指定重启状态,这里暂不深入探讨。
以下是两个练习:
-
练习OTP - 应用 - 1
:将栈服务器转换为OTP应用。
-
练习OTP - 应用 - 2
:目前尚未为应用编写测试,思考可以测试的内容并进行尝试。
7. 代码发布
Erlang通过强大的发布管理系统实现了高可用性的应用。Elixir简化了这个系统的使用。
7.1 术语解释
- 发布 :包含应用特定版本、依赖项、配置以及运行和持续运行所需元数据的包。
- 部署 :将发布包放入可使用的环境中的方式。
- 热升级 :一种部署方式,允许在应用继续运行的情况下更改其发布版本,升级过程对用户无感知。
7.2 EXRM - Elixir发布管理器
EXRM是一个Elixir包,基于Erlang的relx包,利用了Erlang虚拟机的特殊功能,使大多数发布任务变得简单。
7.3 准备工作
在Elixir中,应用代码和其操作的数据都需要进行版本控制,二者相互独立。代码版本存储在
mix.exs
的项目字典中,而数据版本可以在每个服务器模块中进行管理。例如,服务器的状态最初以二元组形式存储,可定义为版本0;后来改为三元组存储,可定义为版本1。可以使用
@vsn
指令设置服务器状态数据的版本:
defmodule Sequence.Server do
use GenServer
@vsn "0"
7.4 首次发布
首先,需要将
exrm
添加为项目依赖。打开
sequence
项目的
mix.exs
文件,更新
deps
函数:
defp deps do
[
{:exrm, "~> 1.0.6"}
]
end
然后安装依赖:
$ mix do deps.get, deps.compile
现在可以创建第一个发布版本:
$ mix release
EXRM会从
mix.exs
文件中获取应用名称和版本号,并将应用打包到
rel/
目录下。最重要的文件是
rel/sequence/releases/0.0.1/sequence.tar.gz
,它包含了运行该版本所需的所有内容,我们将使用这个文件进行服务器部署。
7.5 模拟部署环境
为了简化部署过程,我们将应用部署到本地机器,但模拟远程部署的方式,使用SSH进行操作。将发布包存储在
deploy
目录中:
$ ssh localhost mkdir ~/deploy
7.6 部署和运行应用
将
sequence.tar.gz
文件复制到
deploy
目录并解压:
$ scp rel/sequence/releases/0.0.1/sequence.tar.gz localhost:deploy
$ ssh localhost tar -x -f ~/deploy/sequence.tar.gz -C ~/deploy
启动
iex
控制台:
$ ssh -t localhost ~/deploy/bin/sequence console
在控制台中可以与序列应用交互:
iex(sequence@127.0.0.1)2> Sequence.Server.next_number
456
iex(sequence@127.0.0.1)3> Sequence.Server.next_number
457
保持该会话运行,用于后续的热代码加载演示。
7.7 第二个发布版本
根据市场调研,客户希望
next_number
函数返回类似“下一个数字是458”的消息。我们进行如下修改:
- 修改
server.ex
文件:
def next_number do
with number = GenServer.call(__MODULE__, :next_number),
do: "The next number is #{number}"
end
-
提升
mix.exs文件中应用的版本号:
def project do
[
app: :sequence,
version: "0.0.2",
deps: deps()
]
end
创建新的发布版本:
$ mix release
在服务器上创建新的发布目录并复制发布包:
$ ssh localhost mkdir deploy/releases/0.0.2
$ scp rel/sequence/releases/0.0.2/sequence.tar.gz localhost:deploy/releases/0.0.2
升级正在运行的代码:
$ ssh localhost ~/deploy/bin/sequence upgrade 0.0.2
回到之前的控制台会话,再次调用
next_number
函数:
iex(sequence@127.0.0.1)4> Sequence.Server.next_number
"The next number is 458"
iex(sequence@127.0.0.1)5> Sequence.Server.next_number
"The next number is 459"
如果新发布版本出现问题,可以降级到之前的版本:
$ ssh localhost ~/deploy/bin/sequence downgrade 0.0.1
之后可以再升级回当前版本:
$ ssh localhost ~/deploy/bin/sequence upgrade 0.0.2
7.8 迁移服务器状态
在实际使用中发现
increment_number
函数的实现有问题,它应该设置连续数字之间的差值,而不是一次性增加一个值。
我们对代码进行修改,在状态中添加一个
delta
值。更新后的服务器代码如下:
defmodule Sequence.Server do
use GenServer
require Logger
@vsn "1"
defmodule State do
defstruct current_number: 0, stash_pid: nil, delta: 1
end
# 外部API
def start_link(stash_pid) do
GenServer.start_link(__MODULE__, stash_pid, name: __MODULE__)
end
def next_number do
with number = GenServer.call(__MODULE__, :next_number),
do: "The next number is #{number}"
end
def increment_number(delta) do
GenServer.cast __MODULE__, {:increment_number, delta}
end
# GenServer实现
def init(stash_pid) do
current_number = Sequence.Stash.get_value stash_pid
{ :ok, %State{current_number: current_number, stash_pid: stash_pid} }
end
def handle_call(:next_number, _from, state) do
{ :reply, state.current_number, %{ state | current_number: state.current_number + state.delta} }
end
def handle_cast({:increment_number, delta}, state) do
{:noreply, %{ state | current_number: state.current_number + delta, delta: delta} }
end
def terminate(_reason, state) do
Sequence.Stash.save_value state.stash_pid, state.current_number
end
end
由于状态格式发生了变化,我们将版本号更新为1,并实现
code_change
函数来处理状态迁移:
def code_change("0", old_state = { current_number, stash_pid }, _extra) do
new_state = %State{current_number: current_number,
stash_pid: stash_pid,
delta: 1
}
Logger.info "Changing code from 0 to 1"
Logger.info inspect(old_state)
Logger.info inspect(new_state)
{ :ok, new_state }
end
提升
mix.exs
文件中应用的版本号:
def project do
[
app: :sequence,
version: "0.0.3",
deps: deps()
]
end
创建新的发布版本:
$ mix release
在服务器上创建新的发布目录并复制发布包:
$ ssh localhost mkdir ~/deploy/releases/0.0.3/
$ scp rel/sequence/releases/0.0.3/sequence.tar.gz localhost:deploy/releases/0.0.3/
升级应用:
$ ssh localhost ~/deploy/bin/sequence upgrade 0.0.3
通过以上步骤,我们完成了从序列程序到OTP应用的转换,以及应用的发布和升级过程,充分展示了OTP在构建可靠、可维护应用方面的强大能力。
OTP应用开发:从基础到发布
8. 热升级原理与优势
在OTP应用开发中,热升级是一项非常重要的特性。Erlang允许同时运行一个模块的两个版本,当前正在执行的代码会继续使用旧版本,直到代码显式引用了发生更改的模块名称,此时该特定进程的执行会切换到新版本。
以之前的序列应用为例,当我们调用
Sequence.Server.next_number
时,对
Sequence.Server
的引用会触发代码重新加载,从而让0.0.2版本的代码处理后续请求。这种方式确保了正在运行的代码不会被中断,同时新发布的版本也能在合适的时候生效。热升级的优势在于可以在不影响用户使用的情况下更新应用,大大提高了应用的可用性和用户体验。
9. 代码版本与状态管理总结
在Elixir的OTP应用开发中,代码版本和状态管理是确保应用可靠性和可维护性的关键。
| 管理对象 | 管理方式 | 示例 |
|---|---|---|
| 代码版本 |
存储在
mix.exs
的项目字典中,每次有重大代码变更时更新版本号
|
def project do [ app: :sequence, version: "0.0.3", deps: deps() ] end
|
| 状态版本 |
在每个服务器模块中使用
@vsn
指令进行管理,状态格式变化时更新版本号
|
@vsn "1"
|
当状态格式发生变化时,需要实现
code_change
函数来处理状态迁移,确保旧状态能正确转换为新状态。例如:
def code_change("0", old_state = { current_number, stash_pid }, _extra) do
new_state = %State{current_number: current_number,
stash_pid: stash_pid,
delta: 1
}
Logger.info "Changing code from 0 to 1"
Logger.info inspect(old_state)
Logger.info inspect(new_state)
{ :ok, new_state }
end
10. 开发流程总结
整个OTP应用的开发和发布流程可以用以下mermaid流程图表示:
graph LR
A[设计应用架构] --> B[编写代码]
B --> C[配置mix.exs]
C --> D[编译应用]
D --> E[创建应用规范文件]
E --> F[测试应用]
F --> G[添加EXRM依赖]
G --> H[创建发布版本]
H --> I[部署应用]
I --> J[热升级或降级]
具体步骤如下:
1.
设计应用架构
:确定应用的功能需求,设计监督树和工作进程的结构。
2.
编写代码
:实现应用的业务逻辑,包括服务器模块、监督者模块等。
3.
配置mix.exs
:设置应用的初始参数、依赖项、版本号等信息。
4.
编译应用
:运行
mix compile
命令编译应用代码。
5.
创建应用规范文件
:Mix会根据
mix.exs
和编译信息自动创建
.app
文件。
6.
测试应用
:编写测试用例,确保应用的功能正常。
7.
添加EXRM依赖
:在
mix.exs
中添加
exrm
依赖,并安装。
8.
创建发布版本
:运行
mix release
命令创建发布包。
9.
部署应用
:将发布包复制到目标服务器并解压,启动应用。
10.
热升级或降级
:根据需要对应用进行热升级或降级操作。
11. 开发技巧与注意事项
-
参数传递
:使用关键字列表传递应用参数,方便在代码中使用
Application.get_env函数获取参数值。 -
状态管理
:当状态格式发生变化时,及时更新状态版本号,并实现
code_change函数处理状态迁移。 -
版本控制
:每次代码有重大变更时,及时更新
mix.exs中的版本号,确保发布版本的准确性。 - 测试 :虽然OTP应用具有较高的可靠性,但仍需要编写测试用例来验证应用的功能和稳定性。
12. 未来展望
随着技术的不断发展,OTP应用开发也将不断演进。未来可能会有更强大的工具和框架来简化开发流程,提高应用的性能和可靠性。例如,可能会出现更智能的热升级机制,能够自动处理更多复杂的状态迁移和代码变更。同时,与云计算、容器化等技术的结合也将为OTP应用的部署和管理带来更多便利。开发者可以持续关注这些技术趋势,不断提升自己的开发能力,打造出更优秀的OTP应用。
通过对OTP应用开发的深入学习,我们掌握了从基础概念到实际开发、发布的全过程。希望这些知识能帮助开发者在实际项目中构建出可靠、高效的应用。
超级会员免费看
1396

被折叠的 条评论
为什么被折叠?



