关于MapReduce论文的阅读笔记
1. 计算模型的效果:
-
计算模型具有可以直接进行并行化的特点
-
能够自动处理集群中节点宕机的问题
-
减少了计算机开发人员对于并行计算以及计算集群的专业知识的要求
-
能够适应不同大小的集群, 易于横向扩展
2. 该计算模型运行的环境:
-
计算机集群大小为数百或几千台
-
处理数据的大小为TB量级
-
需要处理的任务能被分解为一组或多组
Map-Reduce
结构
3. 计算模型的主要工作流程
-
输入需要计算的数据, 用于计算这些数据的
Map
函数, 用于生成结果的Reduce
函数 到Master
节点-
其中
Map
函数的接口为(String key, String value)
,Reduce
函数的接口为(String key, Iterator values)
-
这里的
Reduce
中的key
表示Reduce
需要在intermediateResult
中使用的数据的键值 -
Reduce
接口中的Iterator
是一个智能迭代器, 会根据目前的Map
-
Map
接口中的key
主要的目的是进行多层MapReduce
的相互衔接, 源数据是没有键值对的, 但是上层的Reduce
的结果是通过键值对存储
-
-
-
Master
节点对于输入的数据进行逻辑划分-
分为多个(万个级别)子任务
-
存储于
GFS(Google File System -- 一种分布式存储系统)
中
-
-
对于每个子任务分配不同的
Worker
节点, 并分别开始运行Map
函数-
运行过程中不断向
Master
汇报当前节点状态以及运行状态 -
生成该子任务对应的中间结果
intermediateResult
, 将结果存放于GFS
中-
intermediateResult
的存储形式是键值对的形式. -
每个子任务生成的
intermediateResult
单独存储 -
单个子任务生成的
intermediateResult
可以通过键值访问该键值对应的一个数据列表
-
-
将运行结果文件的地址发送给
Master
-
-
Master
对于节点的状态数据以及结果地址进行处理-
如果
Map
步骤已经运行完, 则通知工作节点转换使用Reduce
函数, 并分配intermediateResult
的地址(不是要求完全运行完, 根据论文, 应该一边进行Map
的收尾, 一边开始进行Reduce
)
-
-
Worker
节点运行Reduce
函数, 对于intermediateResult
进行进一步处理-
运行过程中不断向
Master
汇报当前节点状态以及运行状态 -
生成该
Reduce
任务对应的最终结果finalResult
, 将结果存放于GFS
中 -
将运行结果文件的地址发送给
Master
-
-
Master
确认所有过程运行完成, 则将GFS
中的地址返回给用户
4. 适用的处理任务
在论文中, 作者提到了以下任务可以通过MapReduce
计算框架进行效率的提高:
-
Distributed Grep (分布式查找)
-
Count URL Access Frequence (统计URL访问频率)
-
Reverse Web-Link Graph (对于网页之间的引用链接进行反向 – 用于统计页面的被引用数, 用于计算页面的重要程度)
-
Term-Vector per Host (对于一个网站中的所有网页中的所有词汇计算频率)
-
Inverted Index (倒排索引 – 用于创建关键词到文本的索引, 用于搜索引擎的关键词搜索)
-
Distributed Sort (分布式排序 – 具体干了啥我还没搞懂)
5. 论文中提到的计算集群的主要问题
-
对于计算集群而言,集群中的节点的可靠性无法得到保证
-
这里主要考虑的问题表现形式是工作节点对于主节点的ping命令无响应 – 如果出错则重新分配任务进行处理
-
-
有错误数据导致代码异常退出:
-
如果同一数据多次重复错误, 则认为其数据有误, 调用用户定义的逻辑处理该错误 – 忽略或者以某种方式输出.
-
6. 对于计算模型的分析
论文中的异常处理, 本地化设计以及副产品等内容都是为了计算模型服务的. 所以对于该计算系统学习的关键是对于计算模型的学习
-
计算模型的核心逻辑:
-
将键值对的输入通过
MAP
函数转化为中间状态的键值对-
其中如果将系统中的每一个键值认为是一个信息维度, 则
Map
函数是将n维的初始信息投影到k维的中间状态 -
假设输入
N
个初始信息文件, 每个初始文件的信息维度为n
维. 经过Map
函数处理后的信息维度为k
维. 则其变化过程为N*n -> N*k (k<n)
– 所以其子任务的数量为N
(但是注意, 论文中的中间结果的数据结构是键值对而不是向量. 且根据论文中的范例, 这里的键值对中的键值不要求唯一. 所以这里的分析并不准确, 没有充分考虑到利用可重复键值的情况)所以这里的分析是对MapReduce过程添加约束后的结果. 如果任务适应当前的分析逻辑, 则其一定可以在MapReduce中实现
-
-
通过
Reduce
函数将中间结果转化为最终结果-
这里在文中的主要方式是将每个
Map
函数输出文件的k
维信息中的一维取出,进行处理得到r
维结果N*1 -> 1*r_i
– 一次Reduce
函数调用的结果是将k
维度中的某个维度转化为r_i
维的结果. 由于经过Map
处理后的k
维之间没有明确的相关关系, 所以最终得到的r = Σ r_i
这里的分析实际上有问题
-
-
-
对于论文中调用范例的分析
-
对于文档中每个单词出现次数的统计
map(String key, String value):
for each word w in value:
EmitIntermediate(w,"1");
// 这个map函数的输入是单个的文档, 这里使用的value就是文档的内容
// 分析可以发现在这个函数中没有进行累加操作, 如果一个单词在文档中多次出现则构造多个<word,"1">的键值对 -- 所以这里的键值对输出实际上没有进行键值的去重
reduce(String key, Iterator values):
int result = 0;
for each v in values:
result += ParseInt(v);
Emit(AsString(result));
// 这里的iterator是通过对于key的搜索进行的数据获取. 也就是说在所有文档生成的中间结果中搜索该键值对应的数据 -
分布式字符串查找
-
Map函数: 输入文档以及待查找的字符串, 输出文档中找到的数据结果(字符串的起始位置)
输入: 一个文档, 待查找字符串; 输出: (根据逻辑, 获得的数据是个长度为n的整形数组) 获取n个键值相同的结果
所以可以这样认为, 不强行确保中间结果中键值的唯一性的原因是达成数组的模拟 – 避免进行数组的append
-
Reduce函数: 只是一个简单的Identityfunction, 将每个文档中的搜索结果进行直接输出
-
-
根据访问日志统计每个URL被访问到的次数
-
Map函数: 每在日志中读到一个URL的访问记录则在中间结果中添加<URL, "1">键值对
-
Reduce函数: 对于每一个键值为某个URL的键值对进行统计, 生成<URL, count>的结果
所以, 不只是上面提到的数组操作利用可重复键值对进行完成, 进行累加操作的时候也是通过可重复键值对进行完成的 – 可能这样设计的目的是避免对于中间结果的随机读取以及写入. 提高硬盘效率
-
-
计算对某个网络地址的引用图 – 即是建立由被引用网站到引用网站的键值对链接
-
Map函数: 输入<Source, Target>的键值对, 输出<Target, Source>的键值对
-
Reduce函数: 输入<Target, Source>的键值对, 输出<Target, list<Source>>的键值对
这里只是简单地通过将ListAppend拆散成重复键值对的形式进行计算, 来减少任务之间的耦合
这里例子很好的显示出MapReduce的一个设计主要矛盾 – 通过拆解不同任务间的共享元素达到任务可以异步运行的目的. 其中需要拆解的共享元素一个是数组, 一个是累计值
-
-
计算内容的单词频率:
-
任务描述: 实际上这就是个简单的单词统计, 需要输出的结果就是
<DocumentGroupId, list<word, frequency>>
,其中DocumentGroupId在总结果中唯一, DocumentGroupId的list中word唯一 -
这个示例在论文中没有详细的函数描述, 但是根据上面的几个例子可以判断其设计思路
-
最粗暴的思路: 对于每个
DocumentGroup
分别进行Map,Reduce过程, 可以避免不同GroupId
中相同单词的键值之间的干扰 -
统一进行Map的思路:
-
对于
Map
函数,每个文档生成的中间结果是<GroupId, <word, "1">>
-
每个
Reduce
过程, 输入两个键值GroupId, word
进行累加, 生成<GroupId, <word, "n">>
-
再进行第二次
Map
, 简单的IdentityFunction
– 由于根据定义以及函数接口Map只能进行单个键值对的操作, 而<GroupId, <word, "n">> --> <GroupId, list<word, "n">>
需要的不是单个键值对的操作, 所以该Map函数没有作用. -
再进行第二次
Reduce
过程, 输入GroupId
, 对于<GroupId, <word, "n">>
进行综合, 得到<GroupId, list<word, "n">>
-
-
单机应用逻辑:
Map<GroupId,Map<String, int>> getGroupWordVector(){
Set<GroupId> groupIdSet = GetGroupIdSet();
Map<GroupId, list<DocumentId>> DocumentMap = GetGroupId2DocumentIdMap();
Map<GroupId,Map<String, int>> FinalResult = new ... ;
for(GroupId groupId : groupIdSet){
Map<String, int> GroupResult = new ... ;
list<DocumentId> targetList = DocumentMap[groupId];
for(DocumentId targetDocument : targetList){
/* Content value = GetContentFromDocumentId(targetDocument);
for(String word : value){
//if not exist: create and set 1
GroupResult[word]++;
} 注释中的部分是单个任务的处理部分*/
}
FinalResult[groupId] = GroupResult;
}
return FinalResult;
}对比单机逻辑和
MapReduce
逻辑, 可以得到以下结果:-
为了增加对分布式的支持, 以
Document
为粒度进行了数据的分配, 也就是说每个任务需要执行的Map+Reduce
部分是单机逻辑中的9~13
行 -
为了将单机逻辑分解为离散的任务就需要分解单机任务间的相互依赖: 这里的依赖就是
GroupResult
, 将其分解为Map
中的<Groupid, <word,"1">>
和Reduce
中的累加
-
-
-
-
倒排索引:
-
任务描述: 生成
<word,List<documentId>>
的最终结果 -
单机应用逻辑:
Map<word,Set<documentId>> funct(){
Map<word,Set<documentId>> FinalRes = new ... ;
for(documentId targetDocument : documentIdList){
Content content = GetContentFromDocumentId(targetDocument);
for(String word : content){
// if not exist: create and init
FinalRes[word].Add(targetDocument);
}
}
return FinalRes;
} -
MapReduce
应用逻辑:-
单个任务的粒度:
for(String word : content){
// if not exist: create and init
FinalRes[word].Add(targetDocument);
}依赖对象的类型为
Map<...,Set<...>>
. 对于Map
天然可以使用Dict
进行描述,对于
Set
, 有两种处理方案:-
将其处理为
List
但是在进行Append
的时候进行去重 – 在Reduce中每试图添加一个DocumentId
之前先对已有的内容进行扫描... Map (DocumentId documentId, Content domumentContent){
for(String word : domumentContent){
EmitIntermediate(word, documentId)
}
} -
将其处理为新一层的
Dict
在进行Reduce
的时候进行去重// 需要根据是否有对多层字典的支持进行处理, 如果没有多层键值, 则使用多类键值进行替代
... Map (DocumentId documentId, Content domumentContent){
for(String word : domumentContent){
EmitIntermediate(word, documentId);
EmitIntermediate(documentId, 1); // 具体怎么设计,我还没想好
}
}
-
-
-
-
分布式排序:
-
任务描述: 没看懂….
-
关于论文中范例的分析总结:
-
作者在论文中设计的中间状态使用字典进行保存,且不要求键值唯一的目的是能够支持更灵活的对全局参数进行拆解, 以达到支持更多总任务的目的. 例如: List, 累加量, 都可以使用非唯一的字典进行数据的存储
-
避免使用数组追加或者是累加量累加的目的是为了避免对硬盘进行随机访问, 提高效率(但是具体效果需要基于实际的实现进行分析, 如果都能够在内存中进行保存, 那么使用累加也不是什么大问题, 还可以减少Reduce中循环的次数)
-
一般使用一次Map-Reduce流程对一层For循环进行解构.
-
多层For循环的解构除了依赖多层
MapReduce
流程以外还需要使用多层键值对<key1, <key2, value>>
或者是多类键值对<key1, key2> <key2, value>
-