小消息,大计算与分布式索引技术实践
1. 并行计算拓展与SMP Erlang启动
pmap的一个扩展版本不仅可以在多核CPU的进程间进行计算映射,还能在分布式网络的节点间进行映射。基本的spawn、send和receive原语可以构建大量的抽象,利用这些原语能创建自己的并行控制抽象,以提高程序的并发度,且无副作用是提高并发的关键。
为了进行性能测量,我们将进行两个实验,分别对两个函数在包含100个元素的列表上进行映射操作,并比较顺序映射和并行映射的时间。
-
实验一
:对包含100个列表的列表L进行排序操作,每个子列表包含1000个随机整数。
L = [L1, L2, ..., L100],
map(fun lists:sort/1, L)
- 实验二 :对包含100个27的列表L计算斐波那契数列。
L = [27,27,..., 27],
map(fun ptests:fib/1, L)
在多核CPU上,由于计算斐波那契数列时进程间数据复制少且计算量大,我们预计第二个问题的性能会优于第一个。
在进行实验前,需要了解如何启动SMP Erlang。SMP Erlang可在多种架构和操作系统上运行,自R11B - 0起,在已知支持的平台上默认启用。若要在其他平台上强制构建SMP Erlang,需在configure命令中添加
--enable-smp-support
标志。
SMP Erlang有两个命令行标志决定其在多核CPU上的运行方式:
| 标志 | 说明 |
| ---- | ---- |
|
-smp
| 启动SMP Erlang |
|
+S N
| 使用N个调度器运行Erlang,若省略该参数,默认使用SMP机器中的逻辑处理器数量 |
我们可能需要调整调度器数量的原因如下:
- 进行性能测量时,调整调度器数量以观察不同CPU数量对运行效果的影响。
- 在单核CPU上,通过调整N模拟多核CPU的运行。
- 有时使用比物理CPU更多的调度器可以提高吞吐量并改善系统性能,但这些效果尚未完全明确,仍在积极研究中。
2. 实验脚本与测试程序
为了进行测试,我们需要一个脚本
runtests
来启动不同调度器数量的Erlang并运行计时测试,将所有计时结果收集到一个名为
results
的文件中。
#!/bin/sh
echo "" >results
for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16\
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
do
echo $i
erl -boot start_clean -noshell -smp +S $i \
-s ptests tests $i >> results
done
同时,还需要一个测试程序
ptests.erl
:
-module(ptests).
-export([tests/1, fib/1]).
-import(lists, [map/2]).
-import(lib_misc, [pmap/2]).
tests([N]) ->
Nsched = list_to_integer(atom_to_list(N)),
run_tests(1, Nsched).
run_tests(N, Nsched) ->
case test(N) of
stop ->
init:stop();
Val ->
io:format("~p.~n",[{Nsched, Val}]),
run_tests(N+1, Nsched)
end.
test(1) ->
%% Make 100 lists
%% Each list contains 1000 random integers
seed(),
S = lists:seq(1,100),
L = map(fun(_) -> mkList(1000) end, S),
{Time1, S1} = timer:tc(lists,
map,
[fun lists:sort/1, L]),
{Time2, S2} = timer:tc(lib_misc, pmap, [fun lists:sort/1, L]),
{sort, Time1, Time2, equal(S1, S2)};
test(2) ->
%% L = [27,27,27,..] 100 times
L = lists:duplicate(100, 27),
{Time1, S1} = timer:tc(lists,
map,
[fun ptests:fib/1, L]),
{Time2, S2} = timer:tc(lib_misc, pmap, [fun ptests:fib/1, L]),
{fib, Time1, Time2, equal(S1, S2)};
test(3) ->
stop.
%% Equal is used to test that map and pmap compute the same thing
equal(S,S) -> true;
equal(S1,S2) ->
{differ, S1, S2}.
%% recursive (inefficent) fibonacci
fib(0) -> 1;
fib(1) -> 1;
fib(N) -> fib(N-1) + fib(N-2).
%% Reset the random number generator. This is so we
%% get the same sequence of random numbers each time we run
%% the program
seed() -> random:seed(44,55,66).
%% Make a list of K random numbers
%% Each random number in the range 1..1000000
mkList(K) -> mkList(K, []).
mkList(0, L) -> L;
mkList(N, L) -> mkList(N-1, [random:uniform(1000000)|L]).
通过运行这些脚本和程序,我们可以得到顺序映射和并行映射的时间对比结果。从结果中可以看出,CPU密集型且消息传递少的计算具有线性加速效果,而轻量级且消息传递多的计算扩展性较差。
3. MapReduce原理与实现
接下来,我们将理论应用于实践,介绍高阶函数mapreduce并使用它构建一个简单的索引引擎。
mapreduce的基本思想是:有多个映射进程生成
{Key, Value}
对的流,这些映射进程将这些对发送给一个归约进程,归约进程合并这些对,将具有相同键的对组合在一起。
mapreduce的定义如下:
@spec mapreduce(F1, F2, Acc0, L) -> Acc
F1 = fun(Pid, X) -> void,
F2 = fun(Key, [Value], Acc0) -> Acc
L = [X]
Acc = X = term()
-
F1(Pid, X)是映射函数,其任务是向进程Pid发送{Key, Value}消息流,然后终止。mapreduce会为列表L中的每个X值生成一个新进程。 -
F2(Key, [Value], Acc0) -> Acc是归约函数,当所有映射进程终止后,归约进程会将特定键的所有值合并,然后为每个收集到的{Key, [Value]}对调用F2,Acc是初始值为Acc0的累加器。
mapreduce在模块
phofs
中定义:
-module(phofs).
-export([mapreduce/4]).
-import(lists, [foreach/2]).
%% F1(Pid, X) -> sends {Key,Val} messages to Pid
%% F2(Key, [Val], AccIn) -> AccOut
mapreduce(F1, F2, Acc0, L) ->
S = self(),
Pid = spawn(fun() -> reduce(S, F1, F2, Acc0, L) end),
receive
{Pid, Result} ->
Result
end.
reduce(Parent, F1, F2, Acc0, L) ->
process_flag(trap_exit, true),
ReducePid = self(),
%% Create the Map processes
%% One for each element X in L
foreach(fun(X) ->
spawn_link(fun() -> do_job(ReducePid, F1, X) end)
end, L),
N = length(L),
%% make a dictionary to store the Keys
Dict0 = dict:new(),
%% Wait for N Map processes to terminate
Dict1 = collect_replies(N, Dict0),
Acc = dict:fold(F2, Acc0, Dict1),
Parent ! {self(), Acc}.
%% collect_replies(N, Dict)
%% collect and merge {Key, Value} messages from N processes.
%% When N processes have terminated return a dictionary
%% of {Key, [Value]} pairs
collect_replies(0, Dict) ->
Dict;
collect_replies(N, Dict) ->
receive
{Key, Val} ->
case dict:is_key(Key, Dict) of
true ->
Dict1 = dict:append(Key, Val, Dict),
collect_replies(N, Dict1);
false ->
Dict1 = dict:store(Key,[Val], Dict),
collect_replies(N, Dict1)
end;
{'EXIT', _, Why} ->
collect_replies(N-1, Dict)
end.
%% Call F(Pid, X)
%% F must send {Key, Value} messsages to Pid
%% and then terminate
do_job(ReducePid, F, X) ->
F(ReducePid, X).
为了测试mapreduce,我们编写一个程序来统计代码目录中所有单词的频率:
-module(test_mapreduce).
-compile(export_all).
-import(lists, [reverse/1, sort/1]).
test() ->
wc_dir(".").
wc_dir(Dir) ->
F1 = fun generate_words/2,
F2 = fun count_words/3,
Files = lib_find:files(Dir, "*.erl", false),
L1 = phofs:mapreduce(F1, F2, [], Files),
reverse(sort(L1)).
generate_words(Pid, File) ->
F = fun(Word) -> Pid ! {Word, 1} end,
lib_misc:foreachWordInFile(File, F).
count_words(Key, Vals, A) ->
[{length(Vals), Key}|A].
运行
test_mapreduce:test()
可以得到单词频率统计结果。
4. 全文索引与倒排索引
在构建索引时,我们需要找到文件中的所有单词,这将用于mapreduce操作的“映射”阶段。
倒排索引是实现全文索引的一种方式。下面通过一个简单的例子来说明倒排索引的概念:
假设我们有一个文件系统,包含三个文件:
| 文件名 | 内容 |
| ---- | ---- |
| /home/dogs | rover jack buster winston |
| /home/animals/cats | zorro daisy jaguar |
| /home/cars | rover jaguar ford |
首先,为每个文件编号:
| 索引 | 文件名 |
| ---- | ---- |
| 1 | /home/dogs |
| 2 | /home/animals/cats |
| 3 | /home/cars |
然后,创建一个单词与文件索引的表格:
| 单词 | 文件索引 |
| ---- | ---- |
| rover | 1, 3 |
| jack | 1 |
| buster | 1 |
| winston | 1 |
| zorro | 2 |
| daisy | 2 |
| jaguar | 2, 3 |
| ford | 3 |
查询倒排索引很简单,例如查询“buster”,可知它出现在文件1(/home/dogs)中;查询“rover AND jaguar”,先查找“rover”(文件1和3),再查找“jaguar”(文件2和3),取交集得到文件3(/home/cars)。
为了实现倒排索引,我们需要两个持久化数据结构:
-
文件名到索引表
:使用DETS表将文件名表示为整数,以节省空间。
-
单词到文件索引表
:使用文件系统存储每个单词对应的文件索引。
5. 索引器的操作与代码结构
索引器的启动通过调用
indexer:start()
开始:
start() ->
indexer_server:start(output_dir()),
spawn_link(fun() -> worker() end).
这会启动一个名为
indexer_server
的服务器,并生成一个工作进程进行索引操作。
工作进程的操作流程如下:
graph TD;
A[开始] --> B[检查是否停止];
B --> C{获取下一个目录};
C -- 有目录 --> D[查找目录中的文件];
D --> E[对文件进行索引];
E --> F[检查点操作];
F --> B;
C -- 无目录 --> G[结束];
具体代码如下:
worker() ->
possibly_stop(),
case indexer_server:next_dir() of
{ok, Dir} ->
Files = indexer_misc:files_in_dir(Dir),
index_these_files(Files),
indexer_server:checkpoint(),
possibly_stop(),
sleep(10000),
worker();
done ->
true
end.
实际的索引操作在
index_these_files
中进行,使用mapreduce实现并行化:
index_these_files(Files) ->
Ets = indexer_server:ets_table(),
OutDir = filename:join(indexer_server:outdir(), "index"),
F1 = fun(Pid, File) -> indexer_words:words_in_file(Pid, File, Ets) end,
F2 = fun(Key, Val, Acc) -> handle_result(Key, Val, OutDir, Acc) end,
indexer_misc:mapreduce(F1, F2, 0, Files).
handle_result(Key, Vals, OutDir, Acc) ->
add_to_file(OutDir, Key, Vals),
Acc + 1.
add_to_file(OutDir, Word, Is) ->
L1 = map(fun(I) -> <<I:32>> end, Is),
OutFile = filename:join(OutDir, Word),
case file:open(OutFile, [write,binary,raw,append]) of
{ok, S} ->
file:pwrite(S, 0, L1),
file:close(S);
{error, E} ->
exit({ebadFileOp, OutFile, E})
end.
运行索引器的命令如下:
1> indexer:cold_start().
2> indexer:start().
...
N> indexer:stop().
6. 索引器的改进方向
这个索引器程序虽然复杂但结构简单易懂,它有启动、停止和错误恢复策略,使用mapreduce实现并行搜索,并尝试从文件中提取单词。然而,要将其改进为一个功能齐全的索引器,还需要进行以下改进:
-
改进单词提取
:不同文件类型(如Erlang、PDF、TXT等)需要不同的分析技术来提取相关单词,且需要考虑所有主要自然语言和拼写。
-
改进mapreduce
:当前的mapreduce在处理超大数据集时,键值合并步骤在内存中运行且无磁盘存储支持,需要考虑如何处理元素数量极大的数据集。
-
改进数据结构
:当前使用文件系统和单个DETS表实现倒排索引,扩展性不佳,对于全球所有机器的文件名,分布式哈希表可能更合适。
索引器的代码位于
indexer
目录中,包含九个文件,遵循复杂Erlang应用程序的命名约定。这种命名方式有优点也有缺点,优点是可以独立开发代码,缺点是公共库代码可能有不同的名称和版本,需要合并。
通过以上内容,我们介绍了并行计算、mapreduce和全文索引的相关知识,并实现了一个简单的索引器,为进一步开发和优化提供了基础。
小消息,大计算与分布式索引技术实践
7. 索引器代码详细解析
索引器的代码分布在多个文件中,下面对这些文件进行详细解析:
| 文件名称 | 功能描述 |
|---|---|
indexer.erl
|
主程序,导出
start()
、
stop()
和
cold_start()
等接口,是用户与索引器交互的入口。
|
indexer_porter.erl
| 实现词干提取算法,将单词还原为词根形式,减少索引中的单词数量。 |
indexer_server.erl
|
使用
gen_server
编写的服务器,负责管理全局数据,如要索引的目录名称和索引进度等。
|
indexer_filenames_dets.erl
|
实现文件名到索引的映射,是
lib_filenames_dets
的副本。
|
indexer_checkpoint.erl
| 实现检查点机制,将数据结构存储在磁盘上,以便在程序崩溃时进行恢复。 |
indexer_trigrams.erl
|
类似于
lib_trigrams.erl
,对单词进行三元组分析。
|
indexer_misc.erl
|
包含一些杂项例程,其中包含
mapreduce
的副本。
|
indexer_words.erl
| 从文件中提取单词,并调用相关例程对单词进行三元组分析和词干提取。 |
indexer_dir_crawler.erl
|
由
indexer_server
调用的可重入目录列表程序。
|
这种命名方式遵循了复杂Erlang应用程序的命名约定,通过将代码分散到多个文件中,使用命名空间约定可以独立开发代码,而无需担心代码的共享问题。但缺点是公共库代码可能会有不同的名称和版本,需要进行合并。
8. 并行计算与索引器的优势总结
并行计算和索引器的结合为处理大规模数据提供了有效的解决方案,以下是其主要优势:
-
提高性能
:通过使用
pmap和mapreduce等并行技术,可以充分利用多核CPU的计算资源,显著提高计算速度。例如,在对大量文件进行索引时,并行计算可以同时处理多个文件,减少整体处理时间。 - 可扩展性 :并行计算使得系统能够轻松应对不断增长的数据量。当数据量增加时,可以通过增加调度器数量或使用分布式网络节点来扩展系统的处理能力。
- 容错性 :索引器的检查点机制和错误恢复策略使得系统在遇到错误或崩溃时能够快速恢复,保证数据的完整性和处理的连续性。
9. 实际应用场景与案例分析
并行计算和索引器在许多实际应用场景中都有广泛的应用,以下是一些具体案例:
- 搜索引擎 :搜索引擎需要对大量的网页进行索引,以便快速响应用户的查询。使用并行计算和索引器可以提高索引的速度和效率,使得搜索引擎能够实时更新索引,提供更准确的搜索结果。
- 数据仓库 :数据仓库中存储着大量的历史数据,需要对这些数据进行快速查询和分析。索引器可以帮助建立高效的数据索引,提高数据查询的速度。
- 日志分析 :在大型系统中,日志文件通常非常庞大,需要对日志进行实时分析以发现潜在的问题。并行计算和索引器可以加速日志的处理和分析过程,及时发现系统中的异常情况。
10. 未来发展趋势与展望
随着数据量的不断增长和计算需求的提高,并行计算和索引技术将不断发展和创新。以下是一些未来的发展趋势:
-
更高效的并行算法
:研究人员将继续探索更高效的并行算法,以进一步提高计算性能和可扩展性。例如,改进
mapreduce算法,使其能够更好地处理超大数据集。 - 分布式计算 :分布式计算将成为未来的主流趋势,通过将计算任务分布到多个节点上,可以充分利用分布式网络的计算资源,提高系统的处理能力。
- 人工智能与机器学习 :人工智能和机器学习领域对数据处理和分析的需求越来越高,并行计算和索引技术将为这些领域提供更强大的支持。例如,在深度学习中,需要对大量的训练数据进行快速处理和索引。
11. 总结与建议
通过本文的介绍,我们了解了并行计算、
mapreduce
和全文索引的相关知识,并实现了一个简单的索引器。在实际应用中,可以根据具体需求对索引器进行改进和优化,以提高系统的性能和功能。
以下是一些建议:
- 优化单词提取算法 :针对不同的文件类型和自然语言,开发更高效的单词提取算法,提高索引的准确性。
-
改进
mapreduce实现 :考虑使用磁盘存储和分布式哈希表等技术,提高mapreduce在处理超大数据集时的性能和可扩展性。 - 加强错误处理和容错机制 :进一步完善索引器的错误处理和容错机制,确保系统在遇到各种异常情况时能够稳定运行。
总之,并行计算和索引技术为处理大规模数据提供了强大的工具,通过不断的研究和实践,我们可以开发出更高效、更可靠的系统,满足不同领域的需求。
graph LR;
A[并行计算] --> B[提高性能];
A --> C[可扩展性];
A --> D[容错性];
E[索引器] --> B;
E --> C;
E --> D;
B --> F[搜索引擎];
B --> G[数据仓库];
B --> H[日志分析];
C --> F;
C --> G;
C --> H;
D --> F;
D --> G;
D --> H;
通过以上的总结和分析,我们可以看到并行计算和索引技术在实际应用中的重要性和潜力。希望本文能够为读者提供有益的参考,激发更多的研究和实践。
超级会员免费看


被折叠的 条评论
为什么被折叠?



