Skip to content

Instantly share code, notes, and snippets.

@redink
Created June 3, 2018 15:50
Show Gist options
  • Save redink/c4572e28e7c3ddb884fadf690c7621c9 to your computer and use it in GitHub Desktop.
Save redink/c4572e28e7c3ddb884fadf690c7621c9 to your computer and use it in GitHub Desktop.

对于beam 来说,一般有几种存储数据的方式:

  • process dictionary
  • ETS table

如果把gen server 的state 也算是一种方式的话,那么还有:

  • gen server state

在Elixir 的Macro 中,提供了很方便的方式,可以在编译期将数据编译成beam,最简单的方式就是使用 @expr,关于@ 的详细文档,可以参见 here.

defmodule CompileDataBeam_1 do
  @beam_data %{a: "a", b: "b"}
  def test_beam_data do
    @beam_data
  end
end

在这个例子中,将一个map 在编译期compile 到了beam,当调用 test_beam_data/0 这个函数时,就可以得到 %{a: "a", b: "b"} 这个map.

@expr

编译一个.ex 是比较基本的操作,

$ elixirc compile_data_beam_1.ex

编译之后,会得到.beam 文件,大概是这个样子:

$ ls Elixir.CompileDataBeam_1.beam
Elixir.CompileDataBeam_1.beam

接着,可以尝试去反编译它,将它反编译成 .erl,接着再有 .erl 文件生成 .S 文件。需要反编译的话,这里有个 library.

$ iex -pa erlware_commons/_build/default/lib/erlware_commons/ebin/
iex(1)> :ec_compile.beam_to_erl_source(Elixir.CompileDataBeam_1, "compile_data_beam_1.erl")
:ok

至此,.erl 文件,很简单的得到了,其中,最主要的部分应该是:

test_beam_data() -> #{a => <<"a">>, b => <<"b">>}.

接着,再将 erl 文件编译成 S 文件,

$ erlc -S compile_data_beam_1.erl

将会得到 compile_data_beam_1.S,其中,最重要的部分是:

{function, test_beam_data, 0, 10}.
  {label,9}.
    {line,[{location,"compile_data_beam_1.erl",26}]}.
    {func_info,{atom,'Elixir.CompileDataBeam_1'},{atom,test_beam_data},0}.
  {label,10}.
    {move,{literal,#{a => <<"a">>,b => <<"b">>}},{x,0}}.
    return.

到这里,基本上可以看得出来,%{a: "a", b: "b"} 是作为一个字面量(literal variable)编译到beam 的。

define functions

%{a: "a", b: "b"} 作为一个整块编译到beam 足够简单粗暴,如果想得到特定key 对应的value, 可以使用 Map.get/2,3 函数。

但是,如果尝试将一个很大的变量compile 到beam,那么将会编译期触发memory 方面的问题:

defmodule CompileDataBeamBigVar do
  @big_big_map Map.new(for i <- 1..100_000, do: {i, Enum.take_random(?a..?z, 10)})
  def get(k) do
    Map.get(@big_big_map, k)
  end
end

在编译期间,系统内存将会出现很大的spike (这个和Elixir 代码编译成beam 的过程有关,也和Erlang AST 相关,这一部分,将在后续的writing 详细分析)。

多说几句背景,在 tubitv 有几个地方重度使用到了『编译期将database 数据compile 到beam』这个特性,其中有两个地方,非常典型:

  • content policy
  • content discovery

content policy 主要是将content 的policy 进行compile,然后利用简单表达式计算,加速某个content policy 的check,这就要求将所有的policy 进行compile,以及content 到policy 映射关系进行编译,随着content 数量的增多,如果还仅仅作为一个变量来编译的话,service 时不时就会memory crash.

content discovery 是将content 分类编排,比如说,动作片放哪些content id, 喜剧片放哪些,在tubi 对这里的预期是,content ops 修改了分类编排之后,可以尽快的set live,同样考虑到policy 的影响,对content discovery 这部分的内容也做了编译期处理,将其compile 到beam,提高interface performance.

implementation

在这个例子中,map 的key 和 value 都固定,所以可以试用函数:

def get(1), do: 'value_1'
def get(2), do: 'value_2'
...

完整的module:

defmodule CompileDataBeamDefineFunctions do
  @big_big_map Map.new(for i <- 1..100_000, do: {i, Enum.take_random(?a..?z, 10)})
  @big_big_map
  |> Enum.each(fn {k, v} ->
    def get(unquote(k)), do: unquote(v)
  end)
end

define modules

不出意外的话,编译完成的beam 文件,将会达到3.7M 的大小,这对于 EVM 来说,是非常大的文件了,非常容易导致各种问题,compile 以及load binary. 既然可以将一个大的变量编译成functions, 为何不能将其编译成多个module呢?

implementation through hash keys

将一个大的map 的key 通过hash 拆分为多个块,可以分为三步:

  • split data

    在这个例子中,data 是一个大的map,所以,可以根据key 的hash 值将其分为若干小的map

  • define modules

    对于每个小的map,需要将其compile 成不同的module

  • define external function

    由于将大的map 编译成了多个module,就需要维护key 到module name 的映射关系

split data

拆分最简单的办法就是hash:

  def split_map(map, n) do
    hash_map = map
    |> Enum.map(fn {k, _} = item ->
      {:erlang.phash2(k, n), item}
    end)
    for i <- 0..(n - 1) do
      {i, :proplists.get_all_values(i, hash_map)}
    end
  end

可以使用 Erlang 自带的函数对key 进行hash, :erlang.phash2/2.

define modules

  big_big_map
  |> CompileDataBeamDefineModules.Help.split_map(@slice_size)
  |> Enum.map(fn {i, map_list} ->
    defmodule Module.safe_concat([CompileDataBeamDefineModules, "#{i}"]) do
      map_list
      |> Enum.map(fn {k, v} ->
        def get(unquote(k)), do: unquote(v)
      end)
    end
  end)

可以像define function 那样define module,非常简单。

define get function

最简单定义function 的方式:

def get(k) do
   apply(Module.safe_concat([CompileDataBeamDefineModules, "#{:erang.phash2(k, n)}"]), :get, [k])
end

n 是拆分成块的数量。

但是这样有一个问题,在runtime concat module 是一个非常耗时的操作,可以将这一步放在编译期:

  for i <- 0..(n - 1) do
    defp module_name(unquote(i)) do
      unquote([CompileDataBeamDefineModules, "#{i}"] |> Module.safe_concat())
    end
  end

  def get(k) do
    apply(module_name(:erlang.phash2(k, n)), :get, [k])
  end

define modules using multi-files

define 多个 module ,可以在编译期间将内存spike 的问题尽可能修复,系统内存尽可能平稳,但是并没有降低编译时间。虽然是多个module, 但是由于放在同一个文件中,在Elixir 编译期间,是将这多个module 看做一个文件,只能够利用一个process 来编译这多个module.

大概是这段 code

    result =
      spawn_workers(files, [], [], [], [], %{
        dest: Keyword.get(options, :dest),
        each_cycle: Keyword.get(options, :each_cycle, fn -> [] end),
        each_file: Keyword.get(options, :each_file, fn _file -> :ok end),
        each_long_compilation: Keyword.get(options, :each_long_compilation, fn _file -> :ok end),
        each_module: Keyword.get(options, :each_module, fn _file, _module, _binary -> :ok end),
        output: output,
        long_compilation_threshold: Keyword.get(options, :long_compilation_threshold, 15),
        schedulers: schedulers
      })

一个文件,spawn 一个worker 去compile,所以,如果将多个module 放置在多个文件中,是否就可以利用多核去编译,加快编译速度?

Elixir 提供了 EEx.

define eex file

defmodule CompileDataBeamSplitFiles.N<%= number %> do
  <%= total%>
  |> CompileDataBeamSplitFiles.Help.split_map()
  |> Map.get(<%= number %>)
  |> Enum.map(fn {k, v} -> def get(unquote(k)), do: unquote(v) end)
end

numbertotal 是输入参数。

the generator

defmodule CompileDataBeamSplitFiles.Generator do

  def generate_multi_files(n) do
    for i <- 0..(n - 1) do
      content = EEx.eval_file("def_modules_split_files.eex", number: i, total: n)
      filename = "def_modules_split_files_#{i}.ex"
      File.write!(filename, content)
    end
  end
end

如果不使用mix 来组织代码的话,就需要这个一个 generator 来生成文件,如果使用mix 的话,应该在project 的 compilers scope 中定义 before compile .

总结下

在tubitv,这种将一个大的变量(来自database)编译成多个文件多个module,极大的稳定了系统内存,避免出现因内存spike 而node crash 的情况。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment