Erlang及其堆内存消耗问题

5
我一直在我的惠普Proliant服务器上运行高并发应用程序。这个应用程序是我用Erlang编写的文件系统索引器。它为它在文件系统中找到的每个文件夹生成一个进程,并记录所有文件路径在一个碎片化的Mnesia数据库中。(数据库由disc_only_copies类型的表组成,可以在这里查看其文件系统的屏幕截图here。)
下面显示了执行浏览文件系统的高强度工作的代码片段:
%%% -------- COPYRIGHT NOTICE --------------------------------------------------------------------
%% 作者:Muzaaya Joshua,<joshmuza@gmail.com> [http://joshanderlang.blogspot.com]
%% 版本:1.0 免费软件,但禁止修改
%% 版权:Muzaaya Joshua(file_scavenger-1.0)2011 - 2012。版权所有。
%% 参考:<a href="http://www.erlang.org">OpenSource Erlang WebSite</a>
%% 
%%% ---------------- EDOC INTRODUCTION TO THE MODULE ----------------------------------------------
%% @doc 该模块提供了读取、写入、搜索、连接和移动目录的低级API。模块实现于@date @time。
%% @end
-module(file_scavenger_utilities).
%%% ------- EXPORTS ------------------------------------------------------------------------------- -compile(export_all).
%%% ------- INCLUDES -----------------------------------------------------------------------------
%%% -------- MACROS ------------------------------------------------------------------------------ -define(IS_FOLDER(X),filelib:is_dir(X)). -define(IS_FILE(X),filelib:is_file(X)). -define(FAILED_TO_LIST_DIR(X),error_logger:error_report(["*** File Scavenger Utilities Error ***** ",{error,"Failed to List Directory"},{directory,X}])). -define(NOT_DIR(X),error_logger:error_report(["*** File Scavenger Utilities Error ***** ",{error,"Not a Directory"},{alleged,X}])). -define(NOT_FILE(X),error_logger:error_report(["*** File Scavenger Utilities Error ***** ",{error,"Not a File"},{alleged,X}])). %%%--------- TYPES -------------------------------------------------------------------------------
%% @type dir() = string(). %% 必须包含正斜杠,而不是反斜杠。不能以斜杠结尾 %% 准确的目录,例如: "C:/Program Files/SomeDirectory/" %% 但这是正确的:"C:/Program Files/SomeDirectory" %% @type file_path() = string(). %% 必须包含正斜杠,而不是反斜杠。 %% 应该包括文件扩展名,例如 "C:/Program Files/SomeFile.pdf"
%% ----------------------------------------------------------------------------------------------- %% @doc 进入一个目录并为找到的每个文件执行fun ForEachFileFound/2。 %% 如果它发现一个目录,它会执行fun ForEachDirFound/2。 %% 上述两个函数都将父目录作为第一个参数。然后,它将生成一个erlang进程, %% 该进程也将以与父目录相同的方式传播找到的目录。传播过程一直持续到每个文件(无论它是否在嵌套目录中)都被其完整路径注册。 %% @end %% %% @spec spread_directory(dir(),dir(),funtion(),function())-> ok.
spread_directory(Dir,Top_Directory,ForEachFileFound,ForEachDirFound) when is_function(ForEachFileFound),is_function(ForEachDirFound) -> case ?IS_FOLDER(Dir) of false -> ?NOT_DIR(Dir); true -> F = fun(X)-> FileOrDir = filename:absname_join(Dir,X), case ?IS_FOLDER(FileOrDir) of true -> (catch ForEachDirFound(Top_Directory,FileOrDir)), spawn(fun() -> ?MODULE:spread_directory(FileOrDir,Top_Directory,ForEachFileFound,ForEachDirFound) end); false -> case ?IS_FILE(FileOrDir) of false -> {error,not_a_file,FileOrDir}; true -> (catch ForEachFileFound(Top_Directory,FileOrDir)) end end end, case file:list_dir(Dir) of {error,_} -> ?FAILED_TO_LIST_DIR(Dir); {ok,List} -> lists:foreach(F,List) end end.
函数spread_directory/4是一种通用的方式,它需要两个funs。其中一个fun:ForEachFileFound/2会将最上层目录和找到的文件一起处理,并对其进行任何操作;另一个fun:ForEachDirFound/2会将最上层目录和找到的文件夹一起处理,并以任何方式使用它。
我用的启动脚本确保erlang能够尽可能地生成多个进程。一旦一个进程完成了对文件夹的索引,它就会退出。
#!/usr/bin/env sh
echo "Starting File Scavenger System. Layer 1 on the P2P File Sharing System....."
erl \
    -name file_scavenger@127.0.0.1 \
    +P 13421779 \
    -pa ./ebin ./lib/*/ebin ./include \
    -mnesia dir '"./database"' \
    -mnesia dump_log_write_threshold 10000 \
    -eval "application:load(file_scavenger)" \
    -eval "application:start(file_scavenger)"
有一个gen_server,它将密集模块与数据库接口,我在其中记录所有路径。以下是一个片段,显示它从哪里开始执行spread_directory的工作:
handle_cast(index_dirs,#scavenger{directory_paths = Dirs} = State) ->
    {File,Folder} = case {State#scavenger.verbose,State#scavenger.verbose_to} of
                        {true,tty} -> 
                            {
                            fun(TopDir,Fl) -> 
                                io:format(" 文件: ~p~n",[Fl]),
                                file_scavenger_database:insert_file(filename:basename(Fl),file,Fl,TopDir,filename:extension(Fl))
                            end,
                            fun(TopDir,Fd) -> 
                                io:format(" 文件夹: ~p~n",[Fd]),
                                file_scavenger_database:insert_file(Fd,folder,Fd,TopDir,undefined)
                            end
                            };
                        {true,SomeFile} -> 
                            {
                            fun(TopDir,Fl) -> 
                                os:cmd("echo 文件: " ++ Fl ++ " >> " ++ SomeFile),
                                file_scavenger_database:insert_file(filename:basename(Fl),file,Fl,TopDir,filename:extension(Fl))
                            end,
                            fun(TopDir,Fd) -> 
                                os:cmd("echo 文件夹: " ++ Fd ++ " >> " ++ SomeFile),
                                file_scavenger_database:insert_file(Fd,folder,Fd,TopDir,undefined)
                            end
                            }                       
                    end,
    Main = fun(Dir) -> 
                error_logger:info_msg("*** 文件清理服务器正在索引目录: ~p~n",[Dir]),
                spawn(fun() -> file_scavenger_utilities:spread_directory(Dir,Dir,File,Folder) end)
            end,
    lists:foreach(Main,Dirs),
    {noreply,State};    
handle_cast(stop, State) -> {stop, normal, State}.

整个应用程序中可以找到更多源代码细节。应用程序的全部源代码和构建可以在这里找到:File_scavenger-1.0.zip

现在,我在服务器上启动应用程序(HP Proliant G6,包含Intel处理器(2个处理器,每个4个核心,每个核心2.4 GHz速度,8 MB缓存大小),20 GB RAM大小,1.5 Terabytes磁盘空间)。现在,我们拥有两台这样的高性能机器。系统数据库应在这两台机器之间复制。每个服务器运行Solaris 10,64位),其终端现在看起来像下面这样:

开始文件搜寻系统。在P2P文件共享系统上进行第一层搜索...... Erlang R14B03(erts-5.8.4)[源] [smp:8:8] [rq:8] [async-threads:0] [hipe] [kernel-poll:false] Eshell V5.8.4(使用^ G中止) (file_scavenger@127.0.0.1)1> =INFO报告==== 18-Aug-2011 :: 09:36:04 === 启动文件搜寻数据库...... =INFO报告==== 18-Aug-2011 :: 09:36:04 === 数据库成功启动.... =INFO报告==== 18-Aug-2011 :: 09:36:04 === 启动文件搜寻数据库...... =INFO报告==== 18-Aug-2011 :: 09:36:04 === 数据库成功启动.... =INFO报告==== 18-Aug-2011 :: 09:36:04 === 默认详细设置下启动文件搜寻服务器.... (file_scavenger@127.0.0.1)1> file_scavenger_server:index_dirs()。
服务器开始运行并向终端输出它发现的所有文件和文件夹。服务器配备了过多的RAM(20 GB)和Swap空间(Swap为16 GB)。然而,它运行了大约18个小时,最终,erlang虚拟机报告了这个问题:
文件路径: "/proc/4324/root/opt/csw/gcc4/share/locale/ja/LC_MESSAGES/gcc.mo"
 文件夹路径: "/proc/4324/root/opt/csw/gcc4/share/locale/da"
 文件夹路径: "/proc/4324/root/opt/csw/gcc4/share/locale/es/LC_MESSAGES"
 文件路径: "/proc/4324/root/proc/4984/root/.thumbnails/normal/dc259e3897e8af4b379c6d956b6c1393.png"
 文件路径: "/proc/4324/root/proc/4984/root/.thumbnails/fail/gnome-thumbnail-factory/223c19786421b7101d14075bdec46f61.png"
 文件路径: "/proc/4324/root/opt/csw/gcc4/libexec/gcc/i386-pc-solaris2.10/4.5.1/install-tools/mkheaders"
 文件路径: "/proc/4324/root/opt/csw/gcc4/libexec/gcc/i386-pc-solaris2.10/4.5.1/cc1plus"
 文件路径: "/proc/4324/root/opt/csw/gcc4/lib/libsupc++.la"
崩溃转储写入到:erl_crash.dump eheap_alloc:无法分配153052320字节的内存(类型为“heap”)。 中止 - 核心已转储 bash-3.00#
问题1: 有这么强大的服务器,为什么操作系统不能将这样的内存提供给应用程序(它是唯一运行的应用程序)? 问题2: 我启动的 Erlang Emulator 被指示能够生成所需的许多进程。值为 +P 13421779。Erlang VM 是无法访问此内存还是无法将其分配给其进程? 问题3: 对于 Solaris,它只看到一个进程:epmd,可能包含并启动了数千个微线程。我可以对 Solaris 进行哪些配置,以便无论应用程序需要多少内存都不会停止?可用的交换空间为16 GB,RAM为20 GB,老实说,肯定有问题。 问题4: 我可以对 Erlang Emulator 进行哪些配置,以避免这些堆内存崩溃转储,尤其是当服务器上可用所有内存时?如果 Erlang 仍然无法将此类内存分配给简单的文件系统索引器(它是高度并发的),那我该如何在此服务器上运行更消耗内存的应用程序?
最后,欢迎提出所有其他调整以避免在这样一台功能强大的硬件上出现堆内存问题。提前致谢。

该应用程序没有副作用。您可以下载源代码并指出我代码中可能浪费内存的任何地方。但是,我能够浪费20 GB的RAM和16 GB的交换空间吗? - Muzaaya Joshua
2
简短回答:你的 Erlang 应用程序存在内存泄漏。如果无法分配更多内存,Erlang VM 除了终止之外无能为力。 - Adam Lindberg
2
作为一个猜测,如果您有一个目录链接到其父目录或更高级祖先,会发生什么? - keymone
2
我建议保留erl_crash.dump文件并使用崩溃转储查看器查看。您应该能够看到是否有进程分配了过多内存,或者ETS表比预期更大。 - legoscia
1个回答

6
我还没有时间查看源代码,但是我有一些评论:
问题1. 为什么这么强大的服务器会失败并未将内存提供给应用程序(它是唯一运行的应用程序)?
因为Erlang VM尝试消耗超过可用空闲内存的内存。
问题2. 我启动的Erlang Emulator被指示能够生成它所需的许多进程。 值+P 13421779。 Erlang VM是否无法访问此内存或无法将其分配给其进程?
不是的。如果您已经用完了进程,Erlang VM会说出来的(而且VM仍然可以正常运行)。
=ERROR REPORT==== 18-Aug-2011::10:04:04 ===
Error in process <0.31775.138> with exit value: {system_limit,[{erlang,spawn_link,    [erlang,apply,[#Fun<shell.3.130303173>,[]]]},{erlang,spawn_link,1},{shell,get_command,5},    {shell,server_loop,7}]}

问题3. 对于Solaris系统,它只看到一个进程:epmd,可能包含并启动了数千个微线程。我可以对Solaris进行哪些配置,以便无论我的应用程序有多“内存饥饿”,都能保持运行?可用交换空间为16 GB,RAM为20 GB,老实说,肯定出了些问题。
epmd是Erlang端口映射守护程序。它负责管理分布式Erlang,并与您的单个Erlang应用程序无关。您应该寻找的进程将是名称为beam.smp的进程。这些将显示Erlang VM等的操作系统内存消耗。
问题4. 我可以对Erlang仿真器进行哪些配置,以避免堆内存崩溃转储,特别是当服务器上所有可能需要的内存都可用时?如果Erlang仍然无法将此类内存分配给简单的文件系统索引器(它具有很高的并发性),我将如何在此服务器上运行更多的内存消耗应用程序?
Erlang VM应该能够使用您计算机中的所有可用内存。但是,这取决于您的应用程序编写方式。可能会有许多原因导致内存泄漏:
- 原子表填满(创建了太多唯一的原子) - ETS或Mnesia表没有进行垃圾回收(您没有删除旧的未使用元素) - 进程内存不足(您生成了太多进程) - 创建了太多二进制文件(可能会保留对旧二进制文件的未使用引用)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接