2016-07-31 6 views
10

私はエリクシールとフェニックスフレームワークにはかなり新しいので、私の質問はちょっとばかげているかもしれません。エリクシル+フェニックスチャネルメモリ消費量

私はElixir + Phoenix Frameworkをバックエンドとして、Angular 2をフロントエンドとして使用しています。私はフェニックスチャンネルをフロントエンド/バックエンドインターチェンジのチャンネルとして使用しています。そして私は奇妙な状況を発見しました。バックエンドからフロントエンドまで大量のデータを送信すると、特定のチャネルプロセスのメモリ消費量が数百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 

私は、このようなデータの量に対処するためのフロントエンドの能力をテストするために60000件のレコードを持つセットを作成しますが、副作用を持っています - 私は特定のチャネルプロセスのメモリ消費量が167Mbであることを発見しました。だから私はいくつかの新しいブラウザウィンドウを開き、 "playlists:list"リクエストの後に新しいチャンネルプロセスのメモリ消費量がこの量まで増えました。

通常の動作ですか?私は、データベースクエリとデータオフロード時に高いメモリ消費が予想されますが、依頼が完了しても依然として同じです。

更新1。ですから、@Dogbertと@michalmuskalaの大きな助けを借りて、手作業によるガベージコレクションメモリを解放した後にメモリが解放されることがわかりました。

私はrecon_exライブラリと少し掘るしようとした次の例を見つけた:

iex([email protected])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}}]}] 

#PID<0.4410.6>はElixir.Phoenix.Channel.Serverあると#PID<0.4405.6>はcowboy_protocolです。

次の私は一緒に行きました:

iex([email protected])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}}]}] 

と:

iex([email protected])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}}]}] 

そして最後に、問題の状態はもちろん、実際にガベージコレクションの後に(recon.bin_leak以降の処理 - 私が実行した場合:これらのプロセスのpidを含む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}]}, 

私が手動でガベージコレクションを実行しない場合、メモリは "決して"(少なくとも、16時間待っています)空きになります。

覚えておいて欲しいのは、Postgresから70,000レコードを取り出して、バックエンドからフロントエンドにメッセージを送った後、私はそのようなメモリ消費をしています。モデルは非常に単純です:

schema "playlists" do 
    field :title, :string 
    field :description, :string  
    belongs_to :user, MyApp.User 
    timestamps() 
    end 

レコードが自動生成され、次のようになります。

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) 

私は本当にここにどんなアドバイスをいただければ幸いです。私はこのような膨大な量のデータを送信するつもりはないと信じていますが、より小さなデータセットでさえ、多くのクライアント接続の場合に大量のメモリを消費する可能性があります。おそらくこの状況は、もっと一般的な問題を隠してしまうでしょう(しかし、それはちょっと仮定です)。

+0

明らかに正常ではありません。切断してプロセスが終了すると、メモリ使用量が低下しますか?また、 ':observer.start'を実行して、どのプロセスがメモリを使用しているのか、何が何であるのかを確認してください。 – Dogbert

+0

jsonで送信している生データにはどのくらいのメモリが必要ですか? 60_000レコード?プロセスメモリは、それを処理するために少なくともそのサイズ(おそらく途方もなくゴミのために)以上に成長する必要があります。 – michalmuskala

+0

@Dogbert私はobserverでこのメモリ消費を見出しました。この量はElixir.Phoenix.Channel.Server:init/1プロセスによって消費されたので、私はPhoenixチャネルのメモリ消費量について質問しています。そして、はい、切断プロセスが終了し、メモリが解放された後。 ところで、私はcowboy_protocol:init/4が接続ごとに100Mbを食べることを発見しました。 – heathen

答えて

13

これはバイナリメモリリークの古典的な例です。私は何が起こっているのかを説明しましょう:

本当に大量のデータを処理します。これにより、プロセスヒープが増大し、そのプロセスがすべてのデータを処理できるようになります。そのデータの処理が終わったら、ほとんどのメモリは解放されますが、ヒープは大きくなり、データを処理する最後のステップとして作成された大きなバイナリへの参照が保持される可能性があります。 これで、プロセスによって参照される大きなバイナリと、そこに要素が少ない大きなヒープがあります。この時点で、プロセスは、少量のデータのみを処理する低速期間に入り、データをまったく処理しません。これは、次のガベージコレクションが非常に遅くなることを意味します(ヒープが大きいことを覚えておいてください)。ガベージコレクションが実際に実行され、メモリが再利用されるまでにはかなり時間がかかることがあります。

メモリが2つのプロセスで成長しているのはなぜですか?チャネルプロセスは、データベースにすべてのデータを照会してデコードするために増加します。結果が構造体/マップにデコードされると、トランスポートプロセス(カウボーイハンドラ)に送られます。プロセス間でメッセージを送信すると、コピーが行われるため、そのデータはすべてコピーされます。つまり、受信したデータに合わせて転送プロセスを拡張する必要があります。転送プロセスでは、データはjsonにエンコードされます。どちらのプロセスも成長しなければならず、後で大きなヒープと何もする必要がありません。

解決策になりました。 1つの方法は、たくさんのデータを処理したばかりのことを知っているときに、明示的に:erlang.garbage_collect/0を実行することです。別の方法としては、最初にヒープを増やすことを避けることができます。別のプロセス(おそらくTask)でデータを処理し、最終的にエンコードされた結果のみに気を付けることができます。仲介プロセスがデータを処理して完了すると、そのすべてのメモリが停止して解放されます。その時点で、ヒープを成長させることなくプロセス間でのみrefcバイナリを渡します。最後に、一度に必要とされない多くのデータを処理する通常の方法があります。ページネーション。

+0

このような詳細な説明に感謝します!私はあなたが提供した方法について考えていましたが、ここでいくつかの注意点があります。(具体的には) erlang.garbage_collect()を使用している場合、DBからデータを取り出してから実行することができます。ソケットと前のcowboy_protocolプロセスが終了したので、アプリケーションはcowboy_protocolプロセスの大きなヒープにとどまります。 私はTask.asyncの動作を実装しようとしましたが、おそらくメモリ消費量がさらに高いために何か問題があります。 [こちら](https://gist.github.com/vheathen/a21be4cd275e10679e553c5d3de54931)は、コードを要点としています。 – heathen

+0

正直言って、私はフェニックスフレームワーク自体がガベージコレクションを気にする必要があると思います。なぜなら、カスタマイズされたコードが終了した後に始まるいくつかの操作の後にメモリを解放する方法が見えないからです。 – heathen

関連する問題