Elixir + Phoenix Channels内存消耗

hea*_*hen 10 elixir phoenix-framework phoenix-channels

我对Elixir和Phoenix Framework很新,所以可能我的问题有点愚蠢.

我有一个应用程序,Elixir + Phoenix Framework作为后端,Angular 2作为前端.我使用Phoenix Channels作为前端/后端交换的渠道.我发现了一个奇怪的情况:如果我从后端向前端发送大量数据,那么特定的通道进程内存消耗就会达到数百MB.即使在传输结束后,每个连接(每个通道进程)也会占用大量内存.

以下是来自后端频道说明的代码段:

defmodule MyApp.PlaylistsUserChannel do
  use MyApp.Web, :channel

  import Ecto.Query

  alias MyApp.Repo
  alias MyApp.Playlist

  # skipped ... #

  # Content list request handler
  def handle_in("playlists:list", _payload, socket) do 
    opid = socket.assigns.opid + 1
    socket = assign(socket, :opid, opid)

    send(self, :list)
    {:reply, :ok, socket}
  end

  # skipped ... #        

  def handle_info(:list, socket) do

    payload = %{opid: socket.assigns.opid}

    result =
    try do
      user = socket.assigns.current_user
      playlists = user
                  |> Playlist.get_by_user
                  |> order_by([desc: :updated_at])
                  |> Repo.all

      %{data: playlists}
    catch
      _ ->
        %{error: "No playlists"}
    end

    payload = payload |> Map.merge(result)

    push socket, "playlists:list", payload

    {:noreply, socket}
  end
Run Code Online (Sandbox Code Playgroud)

我创建了一个包含60000条记录的集合来测试前端处理这么多数据的能力,但是产生了副作用 - 我发现特定的通道进程内存消耗是167 Mb.所以我打开一些新的浏览器窗口,每个新的频道进程内存消耗在"播放列表:列表"请求之后增长到这个数量.

这是正常的行为吗?我期望在数据库查询和数据卸载期间消耗大量内存,但即使在请求完成后它仍然相同.

更新1.所以在@Dogbert和@michalmuskala的大力帮助下,我发现在手动垃圾收集后,内存将被释放.

我试着用recon_ex库挖掘一下,发现了以下例子:

iex(n1@192.168.10.111)19> :recon.proc_count(:memory, 3)
[{#PID<0.4410.6>, 212908688,
  [current_function: {:gen_server, :loop, 6},
   initial_call: {:proc_lib, :init_p, 5}]},
 {#PID<0.4405.6>, 123211576,
  [current_function: {:cowboy_websocket, :handler_loop, 4},
   initial_call: {:cowboy_protocol, :init, 4}]},
 {#PID<0.12.0>, 689512,
  [:code_server, {:current_function, {:code_server, :loop, 1}},
   {:initial_call, {:erlang, :apply, 2}}]}]
Run Code Online (Sandbox Code Playgroud)

#PID<0.4410.6>是Elixir.Phoenix.Channel.Server并且#PID<0.4405.6>是cowboy_protocol.

接下来我去了:

iex(n1@192.168.10.111)20> :recon.proc_count(:binary_memory, 3)
[{#PID<0.4410.6>, 31539642,
  [current_function: {:gen_server, :loop, 6},
   initial_call: {:proc_lib, :init_p, 5}]},
 {#PID<0.4405.6>, 19178914,
  [current_function: {:cowboy_websocket, :handler_loop, 4},
   initial_call: {:cowboy_protocol, :init, 4}]},
 {#PID<0.75.0>, 24180,
  [Mix.ProjectStack, {:current_function, {:gen_server, :loop, 6}},
   {:initial_call, {:proc_lib, :init_p, 5}}]}]
Run Code Online (Sandbox Code Playgroud)

和:

iex(n1@192.168.10.111)22> :recon.bin_leak(3)                  
[{#PID<0.4410.6>, -368766,
  [current_function: {:gen_server, :loop, 6},
   initial_call: {:proc_lib, :init_p, 5}]},
 {#PID<0.4405.6>, -210112,
  [current_function: {:cowboy_websocket, :handler_loop, 4},
   initial_call: {:cowboy_protocol, :init, 4}]},
 {#PID<0.775.0>, -133,
  [MyApp.Endpoint.CodeReloader,
   {:current_function, {:gen_server, :loop, 6}},
   {:initial_call, {:proc_lib, :init_p, 5}}]}]
Run Code Online (Sandbox Code Playgroud)

最后问题的状态在recon.bin_leak之后进行处理(实际上在垃圾收集之后,当然 - 如果我运行:带有这些进程的pids的erlang.garbage_collection(),结果是相同的):

 {#PID<0.4405.6>, 34608,
  [current_function: {:cowboy_websocket, :handler_loop, 4},
   initial_call: {:cowboy_protocol, :init, 4}]},
...
 {#PID<0.4410.6>, 5936,
  [current_function: {:gen_server, :loop, 6},
   initial_call: {:proc_lib, :init_p, 5}]},
Run Code Online (Sandbox Code Playgroud)

如果我不手动运行垃圾收集 - 内存"永不"(至少,我等了16个小时)变得自由.

只是为了记住:在从后端向前端发送消息后,我有这样的内存消耗,其中有7万条记录来自Postgres.该模型非常简单:

  schema "playlists" do
    field :title, :string
    field :description, :string    
    belongs_to :user, MyApp.User
    timestamps()
  end
Run Code Online (Sandbox Code Playgroud)

记录是自动生成的,如下所示:

description: null
id: "da9a8cae-57f6-11e6-a1ff-bf911db31539"
inserted_at: Mon Aug 01 2016 19:47:22 GMT+0500 (YEKT)
title: "Playlist at 2016-08-01 14:47:22"
updated_at: Mon Aug 01 2016 19:47:22 GMT+0500 (YEKT)
Run Code Online (Sandbox Code Playgroud)

我真的很感激这里的任何建议.我相信我不会发送如此大量的数据,但即使是较小的数据集也可能导致大量内存消耗,以防许多客户端连接.由于我没有编写任何棘手的事情,可能这种情况隐藏了一些更普遍的问题(当然,这只是一个假设).

mic*_*ala 16

这是二进制内存泄漏的经典示例.让我解释一下发生了什么:

您在此过程中处理了大量数据.这会增加进程堆,以便进程能够处理所有数据.完成处理该数据后,大部分内存都被释放,但堆仍然很大,并且可能保存对作为处理数据的最后一步创建的大二进制文件的引用.所以现在我们有一个由进程引用的大型二进制文件和一个包含很少元素的大堆.此时,进程进入缓慢的时段,仅处理少量数据,甚至根本不处理任何数据.这意味着下一个垃圾收集将会非常延迟(记住 - 堆很大),并且可能需要很长时间,直到垃圾收集实际运行并回收内存.

为什么内存在两个进程中增长?由于查询数据库中的所有数据并对其进行解码,通道进程增长.一旦结果被解码成结构/地图,它就被发送到传输过程(牛仔处理程序).在进程之间发送消息意味着复制,因此将复制所有数据.这意味着传输过程必须增长以适应它接收的数据.在传输过程中,数据被编码为json.这两个过程都必须增长,然后在那里保持大堆而无所事事.

现在来解决方案.一种方法是:erlang.garbage_collect/0在您知道自己刚刚处理了大量数据时明确运行,并且在一段时间内不再这样做.另一个可能是避免首先增加堆 - 您可以在单独的进程(可能是a Task)中处理数据,并且只关注最终的编码结果.在完成处理数据的中间过程之后,它将停止并释放其所有内存.那时你将只在进程之间传递refc二进制文件而不增加堆.最后,总是采用通常的方法来处理大量不需要的数据 - 分页.

  • 老实说,我认为Phoenix Framework本身应该关心垃圾回收,因为在自定义代码完成之后开始的一些操作之后,我看不到释放内存的方法。 (2认同)