2017-04-25 47 views
12

Tôi có một ứng dụng elixir/OTP bị treo trong quá trình sản xuất do sự cố hết bộ nhớ. Các chức năng gây ra vụ tai nạn được gọi là mỗi 6 giờ, trong một quá trình chuyên dụng. Phải mất vài phút (~ 30) để chạy và trông như thế này:Giải quyết các tệp nhị phân lớn bị rò rỉ

def entry_point do 
    get_jobs_to_scrape() 
    |> Task.async_stream(&scrape/1) 
    |> Stream.map(&persist/1) 
    |> Stream.run() 
end 

Trên máy tính địa phương của tôi, tôi thấy một sự tăng trưởng liên tục trong tiêu thụ bộ nhớ mã nhị phân lớn khi các chức năng chạy:

observer memory usage shows constant memory growth of large binaries

Lưu ý rằng khi tôi kích hoạt bộ sưu tập rác theo cách thủ công trong quá trình chạy chức năng, mức tiêu thụ bộ nhớ giảm đáng kể, vì vậy chắc chắn không phải là vấn đề với một số quy trình khác nhau không thể GC, nhưng chỉ có một quy trình không đúng GC. Ngoài ra, điều quan trọng là phải nói rằng cứ sau vài phút, quy trình thực hiện quản lý GC, nhưng đôi khi không đủ. Máy chủ sản xuất chỉ có RAM 1 GB và nó bị lỗi trước khi GC khởi động.

Cố gắng giải quyết vấn đề tôi gặp phải Erlang in Anger (xem trang 66-67). Một gợi ý là đặt tất cả các thao tác nhị phân lớn trong các quy trình một lần. Giá trị trả về của hàm scrape là một bản đồ chứa các tệp nhị phân lớn. Do đó, chúng được chia sẻ giữa Task.async_stream "công nhân" và quá trình chạy chức năng. Vì vậy, về lý thuyết, tôi có thể đặt persist cùng với scrape bên trong Task.async_stream. Tôi không muốn làm như vậy, và giữ cho các cuộc gọi đến persist được đồng bộ hóa thông qua quy trình.

Một đề xuất khác là gọi :erlang.garbage_collect theo định kỳ. Dường như nó giải quyết vấn đề nhưng cảm thấy quá khó khăn. Tác giả cũng không khuyến cáo điều đó. Đây là giải pháp của tôi hiện tại:

def entry_point do 
    my_pid = self() 
    Task.async(fn -> periodically_gc(my_pid) end) 
    # The rest of the function as before... 
end 

defp periodically_gc(pid) do 
    Process.sleep(30_000) 
    if Process.alive?(pid) do 
    :erlang.garbage_collect(pid) 
    periodically_gc(pid) 
    end 
end 

Và tải bộ nhớ kết quả:

observer memory usage after GC hack

Tôi hoàn toàn không hiểu làm thế nào những gợi ý khác trong cuốn sách phù hợp với vấn đề.

Bạn sẽ đề xuất điều gì trong trường hợp đó? Giữ các giải pháp hacky hoặc có những lựa chọn tốt hơn.

+0

Bạn có cân nhắc việc giữ các tệp nhị phân trong ETS không? Nếu bạn có thể giải phóng chúng một cách đáng tin cậy, bạn có thể chuyển sang phân bổ thủ công một cách hiệu quả và phá vỡ bất kỳ sự nastiness nào trong GC của BEAM. OTOH, nếu đây là một bông tuyết thích hợp, có thể giải pháp GC-cuộc gọi thủ công là đủ tốt? – cdegroot

+0

Ý tưởng thú vị! Hãy xem nếu tôi hiểu: Có chức năng scraper đặt dữ liệu trong ETS và sau đó ánh xạ các 'persist' chức năng trên dữ liệu trong bảng, là chính xác? Tuy nhiên, công nhân 'Task.async_stream' sẽ có tham chiếu đến các tệp nhị phân lớn và quy trình chính sẽ có cùng tham chiếu để tìm nạp từ ETS bên trong hàm' persist' và vấn đề sẽ vẫn còn. – Nagasaki45

+0

Vâng, "Mỗi mục có thể đếm được được chuyển làm đối số cho hàm và được xử lý bởi nhiệm vụ của chính nó" theo tài liệu 'Task.async_stream', vì vậy nếu mỗi lệnh' scrape' của bạn chỉ xây dựng nhị phân, hãy chặn nó trong ETS và trả lại một tham chiếu, sau đó bạn có thể đang kinh doanh. – cdegroot

Trả lời

6

Máy ảo erlang có cơ chế thu thập rác, theo mặc định, được tối ưu hóa cho dữ liệu sống ngắn. Một quá trình sống ngắn có thể không phải là rác thu thập được cho đến khi nó chết, và hầu hết các bộ sưu tập rác chỉ chạy kiểm tra các mục mới được thêm vào. Các mục đã tồn tại sau lần chạy GC sẽ không được kiểm tra lại cho đến khi quá trình quét hoàn tất.

Tôi khuyên bạn nên thử điều chỉnh cờ fullsweep_after. Nó có thể được thiết lập toàn cầu thông qua :erlang.system_flag(:fullsweep_after, value) hoặc cho quá trình cụ thể của bạn bằng cách sử dụng :erlang.spawn_opt/4.

Từ các tài liệu:

Hệ thống runtime Erlang sử dụng một chương trình thu gom rác thải thế hệ, sử dụng một "đống cũ" cho dữ liệu đã tồn tại ít nhất một thu gom rác thải. Khi không còn chỗ trống trên heap cũ, một bộ sưu tập rác đầy đủ được thực hiện.

Tùy chọn fullsweep_after giúp bạn có thể chỉ định số lượng tối đa các bộ sưu tập thế hệ trước khi bắt buộc toàn cảnh, ngay cả khi có chỗ trên heap cũ. Việc đặt số thành 0 sẽ vô hiệu hóa thuật toán thu thập chung, tức là tất cả dữ liệu trực tiếp được sao chép ở mọi bộ sưu tập rác.

Một vài trường hợp khi nó có thể hữu ích để thay đổi fullsweep_after:

  • Nếu mã nhị phân mà không còn được sử dụng là được vứt bỏ càng sớm càng tốt. (Đặt số thành số không.)
  • Quá trình chủ yếu có dữ liệu ngắn ngủi được ẩn hoàn toàn hiếm khi hoặc không bao giờ, đó là đống cũ chứa phần lớn rác. Để đảm bảo một chế độ fullsweep thỉnh thoảng, hãy đặt Số thành giá trị phù hợp, chẳng hạn như 10 hoặc 20.
  • Trong các hệ thống nhúng với số lượng RAM giới hạn và không có bộ nhớ ảo, bạn có thể muốn giữ lại bộ nhớ bằng cách đặt Số thành 0. (Giá trị có thể được thiết lập trên toàn cầu, xem erlang: system_flag/2.)

Giá trị mặc định là 65535 (trừ khi bạn đã thay đổi nó thông qua các biến môi trường ERL_FULLSWEEP_AFTER), do đó giá trị bất kỳ thấp sẽ làm cho thùng rác bộ sưu tập tích cực hơn.

Đây là thông tin tốt về chủ đề: https://www.erlang-solutions.com/blog/erlang-19-0-garbage-collector.html

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