ElixirDeBombe 2019-06-21
最近完成了一个 elixir 项目, 打包发布之后, 却遇到了一些问题. config
文件中的配置信息, 都在编译的时候固定了, 打包之后就无法更改配置了. 为了解决这个问题, 实现运行时的配置, 我查询了一些资料, 整理成本文.
Application.get_env
Application, 即应用, 是 elixir/erlang 世界里实现某些特定功能的单位. 比如在新建 Phoenix 项目的时候, 就会创建一个 lib/MyApp.ex
文件, 包含了这个应用的 start
等函数. elixir 提供了一个 Application.get_env(app, key, default \\ nil)
函数, 我们可以很方便地获取到某个应用的某个环境变量的值. 我在想, 这些变量是存储在哪里的, 是查询速度很快的ets里吗? 于是我查看了一下这个函数的实现:
@spec get_env(app, key, value) :: value def get_env(app, key, default \\ nil) do :application.get_env(app, key, default) end
-spec get_env(Application, Par, Def) -> Val when Application :: atom(), Par :: atom(), Def :: term(), Val :: term(). get_env(Application, Key, Def) -> case get_env(Application, Key) of {ok, Val} -> Val; undefined -> Def end.
到这里, 还只是在实现默认值的功能, 继续看看:
-spec get_env(Application, Par) -> 'undefined' | {'ok', Val} when Application :: atom(), Par :: atom(), Val :: term(). get_env(Application, Key) -> application_controller:get_env(Application, Key).
突然冒出了一个 application_controller
模块, 它大概是核心人物了吧, 继续看看:
get_env(AppName, Key) -> case ets:lookup(ac_tab, {env, AppName, Key}) of [{_, Val}] -> {ok, Val}; _ -> undefined end.
果然和我们预想的一样, 所有应用的环境变量都存储在一个名为 ac_tab
的 ets 表中, 以环境名, 应用名和变量的Key组成的三元组来进行查询. 这就意味着, 在运行时修改这些环境变量的值是有可能的.
Mix.Config
那么, elixir 是在什么时候往这个表里写入数据的呢. 在 Phoenix 项目中, 我们一般把配置文件写在 config
目录下, 每个配置文件都需要用到 use Mix.Config
. Mix 是elixir 内置的编译工具, 也是 elixir 世界里的管家, 项目的新建, 编译, 测试等它都要插一手. 让我来看看它都干了什么好事:
defmacro __using__(_) do quote do import Mix.Config, only: [config: 2, config: 3, import_config: 1] {:ok, agent} = Mix.Config.Agent.start_link var!(config_agent, Mix.Config) = agent end end
这里普及一点元编程的内容, var!(var, context)
函数的作用就是在宏内使用调用者上下文的变量. 在调用 use Mix.Config
之后, 首先导入了 Mix.Config
模块中的三个函数, 然后启动了一个 Agent, 并将它的 pid 绑定到变量 config_agent
上. 这个Agent是干嘛用的呢? 猜测一下, 应该是用来临时存储配置的, 最后再写入到 ets.
接下来就要看下 config(app, opts)
和 config(app, key, opts)
函数了. 看到这里, 我想它们的作用应该就是将配置信息写入到 config_agent
中:
defmacro config(app, opts) do quote do Mix.Config.Agent.merge var!(config_agent, Mix.Config), [{unquote(app), unquote(opts)}] end end defmacro config(app, key, opts) do quote do Mix.Config.Agent.merge var!(config_agent, Mix.Config), [{unquote(app), [{unquote(key), unquote(opts)}]}] end end
看一下, Mix.Config.Agent.merge(agent, new_config)
函数:
@spec merge(pid, config) :: config def merge(agent, new_config) do Agent.update(agent, &Mix.Config.merge(&1, new_config)) end
在 elixir 中, 如果一个进程的作用只是用来做简单的数据存取, 那么可以使用 Agent. 虽然很多人更偏向于只使用 GenServer~. 这里就不看 Mix.Config.merge/2
函数了, 它的作用就是将相同 app 的配置合并到一个列表.
那么, 调用完 config
函数, 配置信息都写入了 agent, 那么什么时候写入 ets 呢? 是在编译时吗, 还是运行时? 首先, 编译时肯定是有写入的, 因为我们经常会像这样 @something Application.get_env(:my_app, :something)
将环境变量获取到模块属性中, 而模块属性的值是在编译时确定的.
有很重要的一点我们还没有确定, 那就是 myapp/config
目录下的这些.exs
文件到底是什么时候执行的, elixir又是怎么获得 config_agent 的pid 的. 我在 Mix.Config
模块中发现了这个函数:
def read!(file, loaded_paths \\ []) do try do if file in loaded_paths do raise ArgumentError, message: "recursive load of #{file} detected" end {config, binding} = Code.eval_string File.read!(file), [{{:loaded_paths, Mix.Config}, [file | loaded_paths]}], [file: file, line: 1] config = case List.keyfind(binding, {:config_agent, Mix.Config}, 0) do {_, agent} -> get_config_and_stop_agent(agent) nil -> config end validate!(config) config rescue e in [LoadError] -> reraise(e, System.stacktrace) e -> reraise(LoadError, [file: file, error: e], System.stacktrace) end end defp get_config_and_stop_agent(agent) do config = Mix.Config.Agent.get(agent) Mix.Config.Agent.stop(agent) config end
我们看到, 这里使用 Code.eval_string
函数, 执行 .exs配置文件, 并且粗暴地提取了其中的 config_agent
变量, 也就是用于存放环境变量列表的 Agent 的pid. 然后从该 Agent 里获取 config , 并将其终结.
在Mix.Tasks.Loadconfig
模块里, 我们看到这个函数:
defp load(file) do apps = Mix.Config.persist Mix.Config.read!(file) Mix.ProjectStack.configured_applications(apps) :ok end
其它的代码这里就不展示了, 简而言之, 每次运行 mix ...
命令时, 都会先执行 mix loadconfig
来完成配置文件的载入工作.
sys.config
现在, 我们已经搞清楚了 elixir 是如何读取和保存环境变量的. 问题在于, 当我们使用 distillery 等发布工具将项目打包之后, 就无法使用 Mix 了, 那要如何修改配置呢.
事实上, 打包后的 my_app.tar.gz 文件解压缩后, 会附带一个 var/sys.config
文件, 里面有我们在 config.exs
中的所有配置. 修改它, 并将其保存到上层目录, 再运行项目, 就大功告成啦.
至于erlang是如何读取 sys.config
文件, 以及如何以更简单的方式修改该文件, 我们下回分解.