Phoenix with Distillery - 线上部署并定
为什么使用Release
Elixir 和 Phoenix 本身没有提供 Erlang 的 rel 工具,但是我们又不想把源码放在生产环境中,然后用mix 工具编译并执行。
这里有一篇很好的文章,讲述了怎样使用 Distillery 来部署Phoenix应用。
Runtime configuration, migrations and deployment for Elixir applications
作者 Andrew Dryga 通过引用一篇文章 Always use Releases,解释了他拒绝在线上使用mix的原因:
Always use Releases 的作者认为,mix 提供的能力不多,像 rel 的 --heart,remote console 这种的功能是很必要的,当然还有很多其他的功能,在rel 配置文件里都有, mix 没提供。
Always use Releases 的作者认为,这一点尤其重要:就是mix加载modules不会预先加载,等到第一次请求到某module的时候才会加载,这样会出现延迟,以及延迟带来的各种奇怪问题。
当然还看到有人说,Erlang支持线上代码热更新的,如果打了rel包的话,修改完bug,用rsync直接同步那些ebin文件,会相当方便。我不大清楚使用mix能不能做到热更新。那人还说不打包erts,使用线上环境里装的erlang可能会遇到erts版本不一致导致的问题,意思就说一定要把erts打包上线,当然erl就很必要了。
我不想在线上使用mix只是因为,我是一个有点洁癖的程序员,让我copy代码上线简直不能忍。一是觉着在线上搞开发环境太臃肿,开发环境就是开发环境,生产环境就是生产环境,要分开;二是我觉着直接把代码搞上生产环境,本身就怂恿程序员线上改动代码和编译代码,这样跟某些习惯于直接在线上改代码的PHP程序员还有什么区别呢 😂
怎样使用 Distillery 为 Phoenix 生成 release 包,可以参看这里 Using Distillery With Phoenix, 应该不用翻墙。我这里就不详述了。
这里分享我怎样使用 Distillery的 command 工具做自己的 CLI (Command Line Interface)。 这方面谷歌找不到详细资料,所以我来分享我的经验。
如何添加 CLI
我们将为 Phoenix 工程加个 "users" 命令,来实现对 users 资源的 CURD 操作, 效果像这样:
./_build/dev/rel/my_app/bin/my_app users list
User(userId=123, userName=Shawn, password=xxxxx)
User(userId=124, userName=Terry, password=xxxxx)
- 创建 phx 工程,名为my_app, 使用ecto,并加入Distillery支持,这个看Using Distillery With Phoenix 吧。
-
Custom Commands 这个文档里教你怎样创建自己的command:
这里我们只为开发环境创建一个 CLI:
修改文件: rel/config.exs
environment :dev do
set dev_mode: true
set include_erts: true
set cookie: :"xxxxxxxxxxxx"
set commands: [
users: "rel/cli/users"
]
end
- 为CLI创建一个脚本:
新建bash文件: rel/cli/users
#!/usr/bin/env bash
set -o posix
set -e
require_cookie
require_live_node
## Execute an MFA on the running node via `:rpc`
nodetool rpc "Elixir.MyApp.CLI.Users" "run" $@
- 为CLI创建一个Module:
新建Elixir文件:lib/myapp/user_cli.ex
defmodule MyApp.CLI.User do
alias MyApp.Accounts
alias MyApp.Accounts.User
def args do
IO.puts """
Users management CLI
=======================
## List all Users:
list
## Create a new User:
## - userId: type:string, length: 8 characters
## - userName: type:string
## - password: type:string, length: 32 characters
new <userId> <userName> <password>
## Delete a User:
del <appId>
"""
end
def run(['list']), do: list()
def run(['new', userId, userName, password]), do: new(userId, userName, password)
def run(['del', userId]), do: del(userId)
def run(['help']), do: args()
def run(_), do: args()
defp new(userId, userName, password) do
t = %{userId: to_string(userId), userName: to_string(userName), password: to_string(password)}
with {:ok, %User{}=user} <- Accounts.create_user(t) do
IO.puts "Created User successfully. userId: #{user.userId}"
else
{:error, changeset} ->
IO.puts "Failed! errors: #{Kernel.inspect changeset.errors}"
end
end
defp list do
case Accounts.list_users() do
[] ->
IO.puts "empty list"
users ->
Enum.each(users, &format_user/1)
end
end
defp del(userId) do
with user when user != nil <- Accounts.get_user(to_string(userId)),
{:ok, %User{}=user} <- Accounts.delete_user(user)
do
IO.puts "Deleted User successfully. userId: #{user.userId}"
else
nil -> IO.puts "not exists"
{:error, changeset} ->
IO.puts "Failed! errors: #{Kernel.inspect changeset.errors}"
end
end
defp format_user(%User{userId: userId, userName: userName, password: password}) do
IO.puts "User(userId=#{userId}, userName=#{userName}, password=#{password})"
end
end
上面代码里注意的点:
- Shell 脚本用rpc调用我们代码里的run函数,run的参数要设计为list。这样的好处是,我们可以利用函数的模式匹配,匹配不上的时候,就调用args() 返回命令的 help 信息。
- 注意run函数列表里的值都是 character list,是单引号而不是双引号。e.g. 'list' (character list) 而不是 "list" (binary)。这是因为,第3步用 shell 脚本做rpc调用的时候,只能传string类型给 nodetool, 这样子最终到我们的run函数的时候,传进来的参数都是 character list, 而不是 binary.
Shell 里面好像没法儿传binary,谁知道怎么传告诉我一声 🙂
- 稍微修改nodetool
现在看一下第三部里的nodetool命令:
nodetool rpc arg1 arg2 arg3 内部会调用 rpc:call(M, F, [arg1 arg2 arg3]),这样最终调用的就是 Mod:Fun(arg1 arg2 arg3).
然而我们想要这样调用: rpc:call(M, F, [[arg1 arg2 arg3]]),这样最终调用的才是我们想要的 Mod:Fun([arg1 arg2 arg3]).
换句话说,我们想要nodetool rpc "Elixir.MyApp.CLI.Users" "run" new 123 Shawn xxx
调用Elixir.MyApp.CLI.User.run(['new', '123', 'Shawn', 'xxx'])
, 而不是Elixir.MyApp.CLI.User.run('new', '123', 'Shawn', 'xxx')
所以我们需要加一个新的功能,把参数全部打包到一个list里,只传递这个list到对方函数:
修改文件:deps/distillery/priv/templates/nodetool.eex ,55行左右, 加个 rpc_with_a_list_arg 分支,让 RpcArgs 变成一个列表:
["rpc", Module, Function | RpcArgs] ->
do_rpc(TargetNode, Module, Function, RpcArgs);
+ ["rpc_with_a_list_arg", Module, Function | RpcArgs] ->
+ do_rpc(TargetNode, Module, Function, [RpcArgs]);
["rpcterms", Module, Function | ArgsAsString] ->
do_rpc(TargetNode, Module, Function,
consult(lists:flatten(ArgsAsString)));
然后修改第3部里的文件:rel/cli/users
- nodetool rpc "Elixir.MyApp.CLI.Users" "run" $@
+ nodetool rpc_with_a_list_arg "Elixir.MyApp.CLI.Users" "run" $@
- 基本上搞定了,生成release, 有错误的话改改,我上面的代码都随便写的,有那么个样子,没测试过,自己调调吧。😅
注意上面提到的 MyApp, my_app是你自己的工程名,改成自己的。
MIX_ENV=dev mix release
- Bug都解决了?试试吧!
./_build/dev/rel/my_app/bin/my_app start
./_build/dev/rel/my_app/bin/my_app users new 123 Shawn xxx
./_build/dev/rel/my_app/bin/my_app users list
./_build/dev/rel/my_app/bin/my_app users help
**NOTE:** 做release 包,一定要去掉code自动加载功能: code_reloader: false. 不然phoenix 无法运行。所以尽量只给生产环境做release,开发环境不做