2016-07-31 21 views
10

Tôi khá mới với Elixir và Phoenix Framework, vì vậy có thể là câu hỏi của tôi là một chút câm.Elixir + Phoenix Kênh tiêu thụ bộ nhớ

Tôi có một ứng dụng với Elixir + Phoenix Framework làm phụ trợ và Angular 2 làm giao diện người dùng. Tôi đang sử dụng Kênh Phoenix làm kênh cho giao diện đầu cuối/đầu cuối. Và tôi đã tìm thấy một tình huống lạ lùng: nếu tôi gửi một khối dữ liệu lớn từ backend đến frontend thì tiêu thụ bộ nhớ kênh cụ thể sẽ tăng lên tới hàng trăm MB. Và mỗi kết nối (mỗi quá trình kênh) ăn một lượng bộ nhớ như vậy, ngay cả sau khi kết thúc truyền.

Dưới đây là một đoạn mã từ mô tả kênh phụ trợ:

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 

tôi đã tạo ra một bộ với 60000 hồ sơ chỉ để kiểm tra khả năng frontend để đối phó với số tiền đó dữ liệu, nhưng có một tác dụng phụ - Tôi thấy rằng tiêu thụ bộ nhớ kênh cụ thể là 167 Mb. Vì vậy, tôi mở một vài cửa sổ trình duyệt mới và mỗi tiêu thụ bộ nhớ kênh mới tăng lên đến số tiền này sau yêu cầu "danh sách phát: danh sách phát".

Hành vi bình thường có phải không? Tôi sẽ mong đợi tiêu thụ bộ nhớ cao trong quá trình truy vấn cơ sở dữ liệu và tải dữ liệu, nhưng nó vẫn giống nhau ngay cả sau khi yêu cầu hoàn tất.

CẬP NHẬT 1. Vì vậy, với sự trợ giúp lớn của @Dogbert và @michalmuskala tôi thấy rằng sau khi bộ nhớ thu gom rác thủ công sẽ miễn phí.

Tôi đã cố gắng để đào một chút với thư viện recon_ex và tìm thấy các ví dụ sau:

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> là Elixir.Phoenix.Channel.Server và #PID<0.4405.6> là cowboy_protocol.

Tiếp theo, tôi đã đi với:

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}}]}] 

và:

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}}]}] 

Và cuối cùng tình trạng của vấn đề xử lý sau recon.bin_leak (trên thực tế sau khi thu gom rác thải, tất nhiên - nếu tôi chạy: erlang.garbage_collection() với pids của các quá trình này kết quả là như nhau):

{#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}]}, 

Nếu tôi không chạy bộ sưu tập rác thủ công - bộ nhớ "không bao giờ" (ít nhất, tôi đã đợi 16 giờ) trở nên miễn phí.

Chỉ cần nhớ: Tôi có mức tiêu thụ bộ nhớ như vậy sau khi gửi tin nhắn từ phần phụ trợ đến giao diện người dùng với 70 000 bản ghi được tải xuống từ Postgres. Mô hình này là khá đơn giản:

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

ghi là autogenerated và trông như thế này:

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) 

Tôi thực sự sẽ đánh giá cao bất kỳ lời khuyên ở đây. Tôi tin rằng tôi sẽ không gửi một số lượng lớn dữ liệu nhưng ngay cả các tập dữ liệu nhỏ hơn cũng có thể dẫn đến mức tiêu thụ bộ nhớ lớn trong trường hợp có nhiều kết nối máy khách. Và kể từ khi tôi đã không mã hóa bất kỳ điều khó khăn có lẽ tình hình này ẩn một số vấn đề chung chung hơn (nhưng nó chỉ là một giả định, tất nhiên).

+0

Chắc chắn không bình thường. Việc sử dụng bộ nhớ giảm xuống sau khi bạn ngắt kết nối và quá trình chết? Ngoài ra, hãy thử ': observer.start' để xem quy trình nào đang sử dụng bộ nhớ và cho cái gì. – Dogbert

+0

Dữ liệu thô bạn đang gửi trong json mất bao nhiêu bộ nhớ? 60_000 bản ghi? Bộ nhớ quá trình sẽ cần phải phát triển đến ít nhất là kích thước đó (thậm chí có thể nhiều hơn vì rác trên đường đi) để xử lý nó. – michalmuskala

+0

@ Dogbert Tôi tìm thấy mức tiêu thụ bộ nhớ này với: người quan sát. Số tiền này được sử dụng bởi Elixir.Phoenix.Channel.Server: init/1 process, đó là lý do tại sao tôi hỏi về mức tiêu thụ bộ nhớ kênh Phoenix. Và có, sau khi ngắt kết nối quá trình chết và bộ nhớ miễn phí. Nhân tiện, tôi thấy rằng cowboy_protocol: init/4 cũng ăn 100Mb cho mỗi kết nối. – heathen

Trả lời

13

Đây là ví dụ điển hình về rò rỉ bộ nhớ nhị phân. Hãy để tôi giải thích những gì đang xảy ra:

Bạn xử lý một lượng dữ liệu thực sự lớn trong quy trình. Điều này phát triển quá trình đống, để quá trình có thể xử lý tất cả dữ liệu đó. Sau khi bạn đã xử lý xong dữ liệu đó, phần lớn bộ nhớ được giải phóng, nhưng vùng heap vẫn còn lớn và có thể giữ tham chiếu đến nhị phân lớn đã được tạo ra làm bước cuối cùng để xử lý dữ liệu. Vì vậy, bây giờ chúng tôi có một nhị phân lớn được tham chiếu bởi quá trình và một đống lớn với vài yếu tố trong đó. Tại thời điểm này, quá trình đi vào một giai đoạn chậm chỉ xử lý một lượng nhỏ dữ liệu, hoặc thậm chí không có dữ liệu nào cả. Điều này có nghĩa là bộ sưu tập rác tiếp theo sẽ rất chậm (nhớ - đống lớn), và có thể mất một thời gian rất lâu cho đến khi bộ sưu tập rác thực sự chạy và thu hồi bộ nhớ.

Tại sao bộ nhớ lại tăng lên trong hai quy trình? Quá trình kênh tăng lên do truy vấn cơ sở dữ liệu cho tất cả dữ liệu đó và giải mã nó. Một khi kết quả được giải mã thành các bản vẽ/bản đồ, nó sẽ được gửi đến quá trình vận chuyển (trình xử lý cao bồi). Gửi thư giữa các quá trình có nghĩa là sao chép, vì vậy tất cả dữ liệu đó được sao chép. Điều này có nghĩa là quá trình vận chuyển phải phát triển để phù hợp với dữ liệu mà nó nhận được. Trong quá trình vận chuyển, dữ liệu được mã hóa thành json. Cả hai quy trình phải phát triển, và sau đó ở lại đó với đống lớn và không có gì để làm.

Bây giờ đến các giải pháp. Một cách sẽ là chạy một cách rõ ràng :erlang.garbage_collect/0 khi bạn biết bạn vừa xử lý nhiều dữ liệu và sẽ không làm điều đó một lần nữa trong một thời gian. Khác có thể là để tránh phát triển heap ở nơi đầu tiên - bạn có thể xử lý dữ liệu trong một quá trình riêng biệt (có thể là Task) và chỉ quan tâm đến bản thân với kết quả được mã hóa cuối cùng. Sau khi quá trình trung gian được thực hiện với xử lý dữ liệu, nó sẽ dừng và giải phóng tất cả bộ nhớ của nó. Tại thời điểm đó, bạn sẽ chỉ đi qua một nhị phân refc giữa các quá trình mà không phát triển heaps. Cuối cùng, luôn có cách tiếp cận thông thường để xử lý nhiều dữ liệu không cần thiết cùng một lúc - phân trang.

+0

Cảm ơn rất nhiều vì lời giải thích chi tiết như vậy! Tôi đã suy nghĩ về những cách bạn cung cấp, nhưng tôi thấy một số cảnh báo ở đây (trong trường hợp cụ thể của tôi): Nếu tôi đang sử dụng: erlang.garbage_collect(), tôi có thể chạy nó chỉ sau khi lấy dữ liệu từ DB, đẩy nó vào các socket và trước khi quá trình cowboy_protocol kết thúc, do đó, ứng dụng ở lại với quá trình cowboy_protocol 'đống lớn. Tôi đã cố gắng triển khai hành vi Task.async, nhưng có lẽ tôi làm điều gì đó sai vì mức tiêu thụ bộ nhớ thậm chí còn cao hơn trong trường hợp đó. [Ở đây] (https://gist.github.com/vheathen/a21be4cd275e10679e553c5d3de54931) là một gist với một mã. – heathen

+0

Thành thật mà nói, tôi nghĩ rằng Phoenix Framework chính nó nên quan tâm đến việc thu thập rác, bởi vì tôi không thể nhìn thấy những cách để giải phóng bộ nhớ sau khi một số hoạt động được bắt đầu sau khi mã tùy chỉnh kết thúc. – heathen

Các vấn đề liên quan