Hadoop DistributedCache使用及原理

转自:http://hpuxtbjvip0.blog.163.com/blog/static/3674131320132794940734/


概览

DistributedCache是一个提供给Map/Reduce框架的工具,用来缓存文件(text, archives, jars and so on文件的默认访问协议为(hdfs://).

DistributedCache将拷贝缓存的文件到Slave节点在任何Job在节点上执行之前。

文件在每个Job中只会被拷贝一次,缓存的归档文件会被在Slave节点中解压缩。

符号链接

每个存储在HDFS中的文件被放到缓存中后都可以通过一个符号链接使用。

URI hdfs://namenode/test/input/file1#myfile你可以在程序中直接使用myfile来访问file1这个文件。myfile是一个符号链接文件。

缓存在本地的存储目录

<property>

<name>mapred.local.dirname>

<value>${hadoop.tmp.dir}/mapred/localvalue>

<description>The local directory where MapReduce stores intermediate

data files.May be a comma-separated list of

directories on different devices in order to spread disk i/o.

Directories that do not exist are ignored.

description>

property>

<property>

<name>local.cache.sizename>

<value>10737418240value>(默认大小:10GB

<description>The limit on the size of cache you want to keep, set by default

to 10GB. This will act as a soft limit on the cache directory for out of band data.

description>

property>

实际在DataNode节点中的存储目录:

/netqin/hadoop/tmp{${hadoop.tmp.dir}}/mapred/local/taskTracker/archive/hadoop-server01{NameNode主机名称}

Archive文件会被解压缩

例子

packagecom.netqin.examples;

importjava.io.BufferedReader;

importjava.io.FileReader;

importjava.io.IOException;

importjava.net.URI;

importjava.util.StringTokenizer;

importorg.apache.hadoop.conf.Configuration;

importorg.apache.hadoop.filecache.DistributedCache;

importorg.apache.hadoop.fs.Path;

importorg.apache.hadoop.io.IntWritable;

importorg.apache.hadoop.io.Text;

importorg.apache.hadoop.mapreduce.Job;

importorg.apache.hadoop.mapreduce.Mapper;

importorg.apache.hadoop.mapreduce.Reducer;

importorg.apache.hadoop.mapreduce.lib.input.FileInputFormat;

importorg.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

importorg.apache.hadoop.util.GenericOptionsParser;

publicclassCacheDemo {

publicstaticvoidUseDistributedCacheBySymbolicLink()throwsException {

FileReader reader =newFileReader("hdfs://mail.py");

BufferedReader br =newBufferedReader(reader);

String s =null;

while((s = br.readLine()) !=null) {

System.out.println(s);

}

br.close();

reader.close();

}

publicstaticclassTokenizerMapperextends

Mapper {

privatefinalstaticIntWritableone=newIntWritable(1);

privateTextword=newText();

protectedvoidsetup(Context context)throwsIOException,

InterruptedException {

System.out.println("Now, use the distributed cache and syslink");

try{

UseDistributedCacheBySymbolicLink();

}catch(Exception e) {

e.printStackTrace();

}

}

publicvoidmap(Object key, Text value, Context context)

throwsIOException, InterruptedException {

StringTokenizer itr =newStringTokenizer(value.toString());

while(itr.hasMoreTokens()) {

word.set(itr.nextToken());

context.write(word,one);

}

}

}

publicstaticclassIntSumReducerextends

Reducer {

privateIntWritableresult=newIntWritable();

publicvoidreduce(Text key, Iterable values,

Context context)throwsIOException, InterruptedException {

intsum = 0;

for(IntWritable val : values) {

sum += val.get();

}

result.set(sum);

context.write(key,result);

}

}

publicstaticvoidmain(String[] args)throwsException {

Configuration conf =newConfiguration();

String[] otherArgs =newGenericOptionsParser(conf, args)

.getRemainingArgs();

if(otherArgs.length!= 2) {

System.err.println("Usage: wordcount ");

System.exit(2);

}

DistributedCache.createSymlink(conf);

String path ="/tmp/test/mail.py";

Path filePath =newPath(path);

String uriWithLink = filePath.toUri().toString() +"#"+"mail.py";

DistributedCache.addCacheFile(newURI(uriWithLink), conf);

// Path p = new Path("/tmp/hadoop-0.20.2-capacity-scheduler.jar#hadoop-0.20.2-capacity-scheduler.jar");

// DistributedCache.addArchiveToClassPath(p, conf);

Job job =newJob(conf,"CacheDemo");

job.setJarByClass(CacheDemo.class);

job.setMapperClass(TokenizerMapper.class);

job.setCombinerClass(IntSumReducer.class);

job.setReducerClass(IntSumReducer.class);

job.setOutputKeyClass(Text.class);

job.setOutputValueClass(IntWritable.class);

FileInputFormat.addInputPath(job,newPath(otherArgs[0]));

FileOutputFormat.setOutputPath(job,newPath(otherArgs[1]));

System.exit(job.waitForCompletion(true) ? 0 : 1);

}

}

=========

DistributedCache

DistributedCache可将具体应用相关的、大尺寸的、只读的文件有效地分布放置。

DistributedCache是Map/Reduce框架提供的功能,能够缓存应用程序所需的文件 (包括文本,档案文件,jar文件等)。

应用程序在JobConf中通过url(hdfs://)指定需要被缓存的文件。DistributedCache假定由hdfs://格式url指定的文件已经在FileSystem上了。

Map-Redcue框架在作业所有任务执行之前会把必要的文件拷贝到slave节点上。 它运行高效是因为每个作业的文件只拷贝一次并且为那些没有文档的slave节点缓存文档。

DistributedCache根据缓存文档修改的时间戳进行追踪。 在作业执行期间,当前应用程序或者外部程序不能修改缓存文件。

distributedCache可以分发简单的只读数据或文本文件,也可以分发复杂类型的文件例如归档文件和jar文件。归档文件(zip,tar,tgz和tar.gz文件)在slave节点上会被解档(un-archived)。 这些文件可以设置执行权限

用户可以通过设置mapred.cache.{files|archives}来分发文件。 如果要分发多个文件,可以使用逗号分隔文件所在路径。也可以利用API来设置该属性:DistributedCache.addCacheFile(URI,conf)/DistributedCache.addCacheArchive(URI,conf)andDistributedCache.setCacheFiles(URIs,conf)/DistributedCache.setCacheArchives(URIs,conf)其中URI的形式是hdfs://host:port/absolute-path#link-name在Streaming程序中,可以通过命令行选项-cacheFile/-cacheArchive分发文件。

用户可以通过DistributedCache.createSymlink(Configuration)方法让DistributedCache当前工作目录下创建到缓存文件的符号链接。 或者通过设置配置文件属性mapred.create.symlinkyes。 分布式缓存会截取URI的片段作为链接的名字。 例如,URI是hdfs://namenode:port/lib.so.1#lib.so, 则在task当前工作目录会有名为lib.so的链接, 它会链接分布式缓存中的lib.so.1

DistributedCache可在map/reduce任务中作为 一种基础软件分发机制使用。它可以被用于分发jar包和本地库(native libraries)。DistributedCache.addArchiveToClassPath(Path, Configuration)和DistributedCache.addFileToClassPath(Path, Configuration)API能够被用于 缓存文件和jar包,并把它们加入子jvm的classpath。也可以通过设置配置文档里的属性mapred.job.classpath.{files|archives}达到相同的效果。缓存文件可用于分发和装载本地库。

http://www.open-open.com/lib/view/open1337349822015.html

Hadoop有一个叫做 分布式缓存(distributed cache)的机制来将 数据分发到集群上的所有节点上。为了节约网络带宽,在每一个作业中,各个文件通常只需要复制到一个节点一次。
缓存文件复制位置:mapred-site.xml中
mapred.local.dir
/home/hadoop/tmp
操作步骤:
1.将数据的分发到每个节点上:
DistributedCache.addCacheFile(new URI("hdfs://cloud01:9000/user/hadoop/mrinput/ST.txt"), conf);
注意,此操作一定要在创建Job,将conf传递给Job之前进行,否则数据文件的路径不会被Mapper中取到。
2.在每个Mapper中获取文件URI,再进行相关操作:
URI[] uris=DistributedCache.getCacheFiles(context.getConfiguration());
比如读取该文件:
FileSystem fs = FileSystem.get(URI.create("hdfs://cloud01:9000"), context.getConfiguration());
  FSDataInputStream in = null;
  in = fs.open(new Path(uris[0].getPath()));
  BufferedReader br=new BufferedReader(new InputStreamReader(in));

hadoop中的DistributedCache 2

WordCount.javaHadoop的分布式缓存机制使得一个job的所有map或reduce可以访问同一份文件。在任务提交后,hadoop将由-files和-archive选项指定的文件复制到HDFS上(JobTracker的文件系统)。在任务运行前,TaskTracker从JobTracker文件系统复制文件到本地磁盘作为缓存,这样任务就可以访问这些文件。对于job来说,它并不关心文件是从哪儿来的。在使用DistributedCache时,对于本地化文件的访问,通常使用Symbolic Link来访问,这样更方便。通过URI hdfs://namenode/test/input/file1#myfile指定的文件在当前工作目录中被符号链接为myfile。这样job里面可直接通过myfile来访问文件,而不用关心该文件在本地的具体路径。

示例如下:

复制代码
packageorg.myorg;

importjava.io.BufferedReader;
importjava.io.FileReader;
importjava.io.IOException;
importjava.net.URI;
importjava.util.StringTokenizer;

importjava.io.IOException;
importjava.util.*;

importorg.apache.hadoop.filecache.DistributedCache;
importorg.apache.hadoop.fs.Path;
importorg.apache.hadoop.conf.*;
importorg.apache.hadoop.io.*;
importorg.apache.hadoop.mapred.*;
importorg.apache.hadoop.util.*;

public classWordCount
{
public static voidUseDistributedCacheBySymbolicLink() throwsException
{
FileReaderreader= newFileReader("god.txt");
BufferedReaderbr= newBufferedReader(reader);
Strings1= null;
while((s1=br.readLine())!= null)
{
System.out.println(s1);
}
br.close();
reader.close();
}


public static classMap extendsMapReduceBase implementsMapper
{

public voidconfigure(JobConfjob)
{
System.out.println("Now,usethedistributedcacheandsyslink");
try{
UseDistributedCacheBySymbolicLink();
}
catch(Exceptione)
{
e.printStackTrace();
}

}

private final staticIntWritableone= newIntWritable(1);
privateTextword= newText();

public voidmap(LongWritablekey,Textvalue,OutputCollectoroutput,Reporterreporter) throwsIOException
{
Stringline=value.toString();
StringTokenizertokenizer= newStringTokenizer(line);
while(tokenizer.hasMoreTokens())
{
word.set(tokenizer.nextToken());
output.collect(word,one);
}
}
}

public static classReduce extendsMapReduceBase implementsReducer
{
public voidreduce(Textkey,Iteratorvalues,OutputCollectoroutput,Reporterreporter) throwsIOException
{
intsum=0;
while(values.hasNext())
{
sum+=values.next().get();
}
output.collect(key, newIntWritable(sum));
}
}

public static voidmain(String[]args) throwsException
{
JobConfconf= newJobConf(WordCount. class);
conf.setJobName("wordcount");

conf.setOutputKeyClass(Text. class);
conf.setOutputValueClass(IntWritable. class);

conf.setMapperClass(Map. class);
conf.setCombinerClass(Reduce. class);
conf.setReducerClass(Reduce. class);

conf.setInputFormat(TextInputFormat. class);
conf.setOutputFormat(TextOutputFormat. class);

FileInputFormat.setInputPaths(conf, newPath(args[0]));
FileOutputFormat.setOutputPath(conf, newPath(args[1]));

DistributedCache.createSymlink(conf);
Stringpath="/xuxm_dev_test_61_pic/in/WordCount.java";
PathfilePath= newPath(path);
StringuriWithLink=filePath.toUri().toString()+"#"+"god.txt";
DistributedCache.addCacheFile( newURI(uriWithLink),conf);

JobClient.runJob(conf);
}

}

复制代码

执行方法参考http://hadoop.apache.org/common/docs/r0.19.2/cn/mapred_tutorial.html#%E4%BE%8B%E5%AD%90%EF%BC%9AWordCount+v1.0

  程序运行的结果是在jobtracker中的task的log可以看到打印后的/xuxm_dev_test_61_pic/in/WordCount.java文件的内容。

  如果程序中要用到很多小文件,那么使用Symbolic Link将非常方便。

请在执行前先将WordCount.java文件放到指定位置,否则就会找不到文件

概念:

reduce-side join技术是灵活的,但是有时候它仍然会变得效率极低。由于join直到reduce()阶段才会开始,我们将会在网络中传递shuffle所有数据,而在大多数情况下,我们会在join阶段丢掉大多数传递的数据。因此我们期望能够在map阶段完成整个join操作。

主要技术难点:

在map阶段完成join的主要困难就是mapper可能需要与一个它自己不能获得的数据进行join操作,如果我们能够保证这样子的数据可被mapper获得,那我们这个技术就可用。举个例子, 如果我们知道两个源数据被分为同样大小的partition,而且每个partition都以适合作为join key的key值排序的话,那每个mapper()就可以获取所有join操作需要的数据。事实上,Hadoop的org.apache.hadoop.mared.join包中包含了这样的帮助类来实现mapside join,但不幸的是,这样的情况太少了。而且使用这样的类会造成额外的开销。因此,我们不会继续讨论这个包。

什么情况下使用?

情况1:如果我们知道两个源数据被分为 同样大小的partition,而且每个partition都以适合作为join key的 key值排序

情况2:当join大型数据时,通常只有一个源数据十分巨大,另一个数据可能就会呈数量级的减小。例如,一个电话公司的用户数据可能只有千万条用户数据,但他的交易记录数据可能会有十亿条数量级以上的具体电话记录。当小的数据源可以被分配到mapper的内存中时,我们可以获得效果明显的性能提高,只要通过将小的数据源拷贝到每一台mapper机器上,使mapper在map阶段就进行join操作。这个操作就叫做replicate join。

解决方案:

Hadoop有一个叫做 分布式缓存(distributed cache)的机制来将数据分发到集群上的所有节点上。它通常用来分发所有mapper需要的包含“background”数据的文件。例如你使用Hadoop来分类文档,你可能会有一个关键字的列表,你将使用distributed cache来保证所有mapper能够获得这些keywords("background data")。
操作步骤:
1.将数据分发到每个节点上:
[java] view plain copy
  1. DistributedCache.addCacheFile(newPath(args[0]).toUri(),conf);
2.在每个mapper上使用DistributedCache.getLocalCacheFiles()来获取文件,之后再进行相应的操作:
[java] view plain copy
  1. DistributedCache.getLocalCacheFiles();

新出现的问题:

我们的又一个限制是我们其中一个join的表必须足够小以至于能保存到内存中。尽管在不对称大小的输入数据中,较小的那个数据可能仍然不够小(不够小到可以放入内存中。)
1.我们可以通过重新安排数据加工步骤来使它们有效。例如:如果你需要一个所有用户在415区的排序数据时,在滤除一定记录前就将Orders以及Customers表连接起来虽然正确,但是效率却不高。Customers和Orders表都可能大到不能放入内存中。此时我们可以预处理数据使Customers或者Orders表变小。
2.有时候我们不论怎样预处理数据都不能使数据足够小,那我们应该在map时过滤掉不属于415 area的用户。详见《Hadoop in Action》 Chapter5.2.3 semijoin

用Hadoop1.0.3实现KMeans算法

从理论上来讲用MapReduce技术实现KMeans算法是很Natural的想法:在Mapper中逐个计算样本点离哪个中心最近,然后Emit(样本点所属的簇编号,样本点);在Reducer中属于同一个质心的样本点在一个链表中,方便我们计算新的中心,然后Emit(质心编号,质心)。但是技术上的事并没有理论层面那么简单。

Mapper和Reducer都要用到K个中心(我习惯称之为质心),Mapper要读这些质心,Reducer要写这些质心。另外Mapper还要读存储样本点的数据文件。我先后尝试以下3种方法,只有第3种是可行的,如果你不想被我误导,请直接跳过前两种。

一、用一个共享变量在存储K个质心

由于K很小,所以我们认为用一个Vector来存储K个质心是没有问题的。以下代码是错误的:

?
class MyJob extends Tool{
   static Vector centers= new Vector(K);
   static class MyMapper extends Mapper{
     //read centers
  } 
   static class MyMapper extends Reducer{
     //update centers
  }
   void run(){
    until ( convergence ){
      map();
      reduce();
    }
}

发生这种错误是因为对hadoop执行流程不清楚,对数据流不清楚。简单地说Mapper和Reducer作为MyJob的内部静态类,它们应该是独立的--它们不应该与MyJob有任何交互,因为Mapper和Reducer分别在Task Tracker的不同JVM中运行,而MyJob以及MyJob的内部其他类都在客户端上运行,自然不能在不同的JVM中共享一个变量。

详细的流程是这样的:

首先在客户端上,JVM加载MyJob时先初始化静态变量,执行static块。然后提交作业到Job Tracker。

在Job Tracker上,分配Mapper和Reducer到不同的Task Tracker上。Mapper和Reducer线程获得了MyJob类静态变量的初始拷贝(这份拷贝是指MyJob执行完静态块之后静态变量的模样)。

在Task Tracker上,Mapper和Reducer分别地读写MyJob的静态变量的本地拷贝,但是并不影响原始的MyJob中的静态变量的值。

二、用分布式缓存文件存储K个质心

既然不能通过共享外部类变量的方式,那我们通过文件在map和reduce之间传递数据总可以吧,Mapper从文件中读取质心,Reducer把更新后的质心再写入这个文件。这里的问题是:如果确定要把质心放在文件中,那Mapper就需要从2个文件中读取数据--质心文件和样本数据文件。虽然有MutipleInputs可以指定map()的输入文件有多个,并可以为每个输入文件分别指定解析方式,但是MutipleInputs不能保证每条记录从不同文件中传给map()的顺序。在我们的KMeans中,我们希望质心文件全部被读入后再逐条读入样本数据。

于是乎就想到了DistributedCache,它主要用于Mapper和Reducer之间共享数据。DistributedCacheFile是缓存在本地文件,在Mapper和Reducer中都可使用本地Java I/O的方式读取它。于是我又有了一个错误的思路:

?
class MyMaper{
Vector centers= new Vector(K);
void setup(){
//读取cacheFile,给centers赋值
}
void map(){
//计算样本离哪个质心最近
}
}
class MyReducer{
Vector centers= new Vector(K);
void reduce(){
//更新centers
}
void cleanup(){
//把centers写回cacheFile
}
}

错因:DistributedCacheFile是只读的在任务运行前,TaskTracker从JobTracker文件系统复制文件到本地磁盘作为缓存,这是单向的复制,是不能写回的。试想在分布式环境下,如果不同的mapper和reducer可以把缓存文件写回的话,那岂不又需要一套复杂的文件共享机制,严重地影响hadoop执行效率。

三、用分布式缓存文件存储样本数据

其实DistributedCache还有一个特点,它更适合于“大文件”(各节点内存容不下)缓存在本地。仅存储了K个质心的文件显然是小文件,与之相比样本数据文件才是大文件。

此时我们需要2个质心文件:一个存放上一次的质心prevCenterFile,一个存放reducer更新后的质心currCenterFile。Mapper从prevCenterFile中读取质心,Reducer把更新后有质心写入currCenterFile。在Driver中读入prevCenterFile和currCenterFile,比较前后两次的质心是否相同(或足够地接近),如果相同则停止迭代,否则就用currCenterFile覆盖prevCenterFile(使用fs.rename),进入下一次的迭代。

这时候Mapper就是这样的:

?
class MyMaper{
Vector centers= new Vector(K);
void map(){
//逐条读取质心,给centers赋值
}
void cleanup(){
//逐行读取cacheFile,计算每个样本点离哪个质心最近
//然后Emit(样本点所属的簇编号,样本点)
}
}

源代码

试验数据是在Mahout项目中作为example提供的,600个样本点,每个样本是一个60维的浮点向量。点击下载

为样本数据建立一个类Sample.java。

View Code

KMeans.java

View Code

注意在Driver中创建Job实例时一定要把Configuration类型的参数传递进去,否则在Mapper或Reducer中调用DistributedCache.getLocalCacheFiles(context.getConfiguration());返回值就为null。因为空构造函数的Job采用的Configuration是从hadoop的配置文件中读出来的(使用new Configuration()创建的Configuration就是从hadoop的配置文件中读出来的),请注意在main()函数中有一句:DistributedCache.addCacheFile(dataFile.toUri(), conf);即此时的Configuration中多了一个DistributedCacheFile,所以你需要把这个Configuration传递给Job构造函数,如果传递默认的Configuration,那在Job中当然不知道DistributedCacheFile的存在了。

Further

方案三还是不如人意,质心文件是很小的(因为质心总共就没几个),用map()函数仅仅是来读一个质心文件根本就没有发挥并行的作用,而且在map()中也没有调用context.write(),所以Mapper中做的事情可以放在Reducer的setup()中来完成,这样就不需要Mapper了,或者说上面设计的就不是MapReduce程序,跟平常的单线程串行程序是一样的。sigh

原文来自:博客园(华夏35度)http://www.cnblogs.com/zhangchaoyang 作者:Orisun

1、DistributedCache In Hadoop

此篇文章主要是前一篇的后续,主要讲Hadoop的分布式缓存机制的原理与运用。

分布式缓存在MapReduce中称之为DistributedCache,它可以方便map task之间或者reduce task之间共享一些信息,同时也可以将第三方包添加到其classpath路径中去。Hadoop会将缓存数据分发到集群的所有准备启动的节点上,复制到在mapred.temp.dir中配置的目录。

2、DistributedCache的使用

DistributedCache的使用的本质其实是添加Configuraton中的属性:mapred.cache.{files|archives}。图方便的话,可以使用DistributedCache类的静态方法。

不省事法:

conf.set("mapred.cache.files", "/data/data");

conf.set("mapred.cache. archives", "/data/data.zip");

省事法:

DistributedCache.addCacheFile(URI,Configuration)

DistributedCache.addArchiveToClassPath(Path,Configuration,FileSystem)

需要注意的是,上面几行代码需要写在Job类初始化之前,否则在运行会中找不到文件(被折磨了很长时间),因为Job初始化时将传入Configuration对象克隆一份给了JobContext。

在MapReduce的0.21版本以后的org.apache.hadoop.mapreduce均移到org.apache.hadoop.mapred包下。但文档中提供的configure方法是重写的MapReduceBase中的,而新版本中map继承于mapper,reduce继承于reducer,所以configure方法一律改成了setup。要获得cache数据,就得在map/reduce task中的setup方法中取得cache数据,再进行相应操作:

  1. @Override
  2. protectedvoidsetup(Context context)throwsIOException,
  3. InterruptedException {
  4. super.setup(context);
  5. URI[] uris = DistributedCache.getCacheFiles(context
  6. .getConfiguration());
  7. Path[] paths = DistributedCache.getLocalCacheFiles(context
  8. .getConfiguration());
  9. // TODO
  10. }

而三方库的使用稍微简单,只需要将库上传至hdfs,再用代码添加至classpath即可:

DistributedCache.addArchiveToClassPath(new Path("/data/test.jar"), conf);

3、symlink的使用

Symlink其实就是hdfs文件的一个快捷方式,只需要在路径名后加入#linkname,之后在task中使用linkname即使用相应文件,如下:

conf.set("mapred.cache.files", "/data/data#mData");

conf.set("mapred.cache. archives", "/data/data.zip#mDataZip");

  1. @Override
  2. protectedvoidsetup(Context context)throwsIOException,
  3. InterruptedException {
  4. super.setup(context);
  5. FileReader reader =newFileReader(newFile("mData"));
  6. BufferedReader bReader =newBufferedReader(reader);
  7. // TODO
  8. }

在使用symlink之前,需要告知hadoop,如下:

conf.set("mapred.create.symlink", "yes"); // 是yes,不是true

DistributedCache.createSymlink(Configuration)

4、注意事项

1)缓存文件(数据、三方库)需上传至HDFS,方能使用;

2)缓存较小的情况下,建议将数据全部读入相应节点内存,提高访问速度;

3)缓存文件是read-only的,不能修改。若要修改得重新输出,将新输出文件作为新缓存进入下一次迭代。


你可能感兴趣的:(Hadoop DistributedCache使用及原理)