Elixir 编程Elixir技术分享

Phoenix with Distillery - 线上部署并定

2017-10-25  本文已影响151人  Shawn_xiaoyu

为什么使用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)
  1. 创建 phx 工程,名为my_app, 使用ecto,并加入Distillery支持,这个看Using Distillery With Phoenix 吧。
  2. 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
  1. 为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" $@
  1. 为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

上面代码里注意的点:

  1. 稍微修改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" $@
  1. 基本上搞定了,生成release, 有错误的话改改,我上面的代码都随便写的,有那么个样子,没测试过,自己调调吧。😅
    注意上面提到的 MyApp, my_app是你自己的工程名,改成自己的。
MIX_ENV=dev mix release
  1. 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,开发环境不做

上一篇 下一篇

猜你喜欢

热点阅读