5.9 MapReduce与Tez对比
Tez是一个基于Hadoop YARN构建的新计算框架,将任务组成一个有向无环图(DAG)去执行作业,所有的作业都可以描述成顶点和边构成的DAG。
Tez为数据处理提供了统一的接口,不再像MapReduce计算引擎一样将任务分为作业Map和Reduce阶段。在Tez中任务由输入(input)、输出(output)和处理器(processor)三部分接口组成,处理器可以做Map的事情,也可以做Reduce需要的事情。Tez中的数据处理构成DAG的顶点(Vertex),任务之间的数据连接则构成了边(Edge)。
5.9.1 通过案例代码对比MapReduce和Tez
一个包含Mapper和Reducer任务的作业,在Tez中可以看成是一个简单的DAG,如图所示。
用Tez表示MapReduce过程
Mapper的输入和Reducer的输出,分别作为Tez中的输入和输出接口,Mapper和Reducer任务则可以看成两顶点vertex1和vertex2,Mapper和Reducer中间的Shuffle过程则可以看成是两顶点的边。通过使用Tez编写案例来了解下Tez基本编程模型和运行原理。大数据最好的入门案例便是单词计数(wordcount)案例,如果要掌握一个案例,最好的方式便是实操。接下来写Tez版本的WordCount案例。在
MapReduce版本的WordCount案例中,Map阶段所做的逻辑如下:
(1)读取一行数据。
(2)将一行的数据按固定的分隔符进行分割,如空格。
(3)将分割后的单词,按键-值对形式输出,键是单词,值是1。
Reduce阶段所做的逻辑如下:
(1)分别读取每个键对应集合中的值,并进行加总。
(2)将结果以键值对形式输出,键是单词,值是加总后的值。在Tez中需要两顶点分别来处理MapReduce中Map和Reduce两个阶段的内容,以及构建一个DAG图来将两顶点连接起来。
1.顶点1——实现类似Map的逻辑
下面是第一顶点的逻辑:
#java代码
//在MapReduce中,Map阶段需要继承Mapper对象,但是由于在Tez中,将Map和Reduce
都当成一个顶点,因而都会继承SimpleProcessor
public static class TokenProcessor extends SimpleProcessor {
//顶点输出的value,和MapReduce中的Map阶段输出一致
IntWritable one = new IntWritable(1);
//顶点输出的key,和MapReduce中的Map阶段输出一致
Text word = new Text();
@Override
public void run() throws Exception {
//读取输入的数据,其中变量INPUT指代输入任务在DAG图中的名称
KeyValueReader kvReader = (KeyValueReader) getInputs().get(INPUT).getReader();
//输出当前处理后的数据,其中变量SUMMATION指代数据输出的下一环节的名称
KeyValueWriter kvWriter = (KeyValueWriter) getOutputs().get(SUMMATION).
getWriter();
//遍历输入的数据
while (kvReader.next()) {
//对读取的每一行数据,按照空格进行切分成一个个单词,并存储到一个可迭代的对象中
StringTokenizer itr = new StringTokenizer(kvReader.getCurrentValue().
toString());
//遍历迭代对象中的单词
while (itr.hasMoreTokens()) {
word.set(itr.nextToken());
//按kv形式输出,k是单词,v是1
kvWriter.write(word, one);
}
}
}
}
读完上面顶点1的实现逻辑,我们通过下图来比对Tez和Map阶段主体
业务逻辑。
Tez和MapReduce Map阶段比较
通过图可知,Tez顶点1的实现逻辑和基本执行原理同Mapper基本一致,不同点在于Tez把输入和输出放在了run()方法中,而Mapper把输入和输出及遍历文件中的每一行数据交给整个计算框架去实现。
2.顶点2——实现类似Reduce的逻辑
下面是第二顶点的逻辑:
//SimpleMRProcessor是SimpleProcessor的子类
//因此这个阶段其实TokenProcessor处理器所用的都是一个超类
public static class SumProcessor extends SimpleMRProcessor {
public SumProcessor(ProcessorContext context) {
super(context);
}
@Override
public void run() throws Exception {
//读取顶点1的数据,其中,变量TOKENIZER指代顶点1在DAG图中的名称
KeyValuesReader kvReader = (KeyValuesReader) getInputs().get(TOKENIZER).
getReader();
//将当前处理器处理后的数据输出,其中,变量OUTPUT指定输出任务在DAG图中的名称
KeyValueWriter kvWriter = (KeyValueWriter) getOutputs().get(OUTPUT).
getWriter();
//遍历输入的数据
while (kvReader.next()) {
//获取输入的键-值对类型数据的key
Text word = (Text) kvReader.getCurrentKey();
int sum = 0;
//获取输入键-值对类型数据的value,这个value和Reducer中reduce输入键相对应
for (Object value : kvReader.getCurrentValues()) {
sum += ((IntWritable) value).get();
}
kvWriter.write(word, new IntWritable(sum));
}
}
}
读完SumProcessor的逻辑,我们通过下图来对比SumProcessr和MapReduce中Reduce的方法。
Tez和MapReduce Reduce阶段比较
从图中我们看到,Reduc代码和顶点2的逻辑一致。
3.构建DAG图并提交任务
Tez需要将所有的任务构建成一个DAG图,然后才进行任务的提交。这是和MapReduce计算引擎最大的差别。下面我们来看看Tez是如何构造整个DAG的:
private DAG createDAG(TezConfiguration tezConf, String inputPath, String
outputPath,
int numPartitions) throws IOException {
//构造一个输入源dataSource,用于指定输入路径(inputPath)
// 以Text方式(TextInputFormat)读取数据
DataSourceDescriptor dataSource = MRInput.createConfigBuilder(new
Configuration(tezConf),
TextInputFormat.class, inputPath).groupSplits(!isDisableSplitGrouping())
.generateSplitsInAM(!isGenerateSplitInClient()).build();
//构造一个输出,用于将数据以Text(TextOutputFormat)方式,将数据写入到指定的输
出路径(outputPath)下
DataSinkDescriptor dataSink = MROutput.createConfigBuilder(new Configuration
(tezConf),
TextOutputFormat.class, outputPath).build();
//构造第一顶点,名为TOKENIZER变量指代的值:将TokenProcessor构造成一个顶点
//并指定这个顶点的数据源来自dataSource,同时为这个输入起名,即INPUT变量指代的值
Vertex tokenizerVertex = Vertex.create(TOKENIZER, ProcessorDescriptor.
create(
TokenProcessor.class.getName())).addDataSource(INPUT, dataSource);
//构造第二顶点,名为SUMMATION变量指代的值:将SumProcessor构造成一个顶点
//并指定这个顶点的数据输出为dataSink,同时为这个输出起名,即变量OUTPUT指代的值
Vertex summationVertex = Vertex.create(SUMMATION,
ProcessorDescriptor.create(SumProcessor.class.getName()),
numPartitions)
.addDataSink(OUTPUT, dataSink);
//构造上面两个顶点连接的边,并指定边的源即tokenizerVertex的数据输出是Text,
IntWritable的键值对形式
//声明了从源到目标顶点的分区器是采用HashPartitioner
OrderedPartitionedKVEdgeConfig edgeConf = OrderedPartitionedKVEdgeConfig
.newBuilder(Text.class.getName(), IntWritable.class.getName(),
HashPartitioner.class.getName())
.setFromConfiguration(tezConf)
.build();
//创建一个名为WordCount空DAG
DAG dag = DAG.create("WordCount");
dag.addVertex(tokenizerVertex)
.addVertex(summationVertex)
.addEdge(
Edge.create(tokenizerVertex, summationVertex, edgeConf.create
DefaultEdgeProperty()));
return dag;
}
通过createDAG方法构造完一个DAG图后就可以启动整个作业。下面是Tez版本完整的代码:
public class WordCount extends TezExampleBase {
static String INPUT = "Input";
static String OUTPUT = "Output";
static String TOKENIZER = "Tokenizer";
static String SUMMATION = "Summation";
private static final Logger LOG = LoggerFactory.getLogger(WordCount.class);
public static class TokenProcessor extends SimpleProcessor {
IntWritable one = new IntWritable(1);
Text word = new Text();
public TokenProcessor(ProcessorContext context) {
super(context);
}
@Override
public void run() throws Exception {
KeyValueReader kvReader = (KeyValueReader) getInputs().get(INPUT).
getReader();
KeyValueWriter kvWriter = (KeyValueWriter) getOutputs().get(SUMMATION).
getWriter();
while (kvReader.next()) {
StringTokenizer itr = new StringTokenizer(kvReader.getCurrentValue().
toString());
while (itr.hasMoreTokens()) {
word.set(itr.nextToken());
kvWriter.write(word, one);
}
}
}
}
public static class SumProcessor extends SimpleMRProcessor {
public SumProcessor(ProcessorContext context) {
super(context);
}
@Override
public void run() throws Exception {
KeyValuesReader kvReader = (KeyValuesReader) getInputs().get(TOKENIZER).
getReader();
KeyValueWriter kvWriter = (KeyValueWriter) getOutputs().get(OUTPUT).
getWriter();
while (kvReader.next()) {
Text word = (Text) kvReader.getCurrentKey();
int sum = 0;
for (Object value : kvReader.getCurrentValues()) {
sum += ((IntWritable) value).get();
}
kvWriter.write(word, new IntWritable(sum));
}
}
}
private DAG createDAG(TezConfiguration tezConf, String inputPath, String
outputPath,
int numPartitions) throws IOException {
DataSourceDescriptor dataSource = MRInput.createConfigBuilder(new
Configuration(tezConf),
TextInputFormat.class, inputPath).groupSplits(!isDisableSplit
Grouping())
.generateSplitsInAM(!isGenerateSplitInClient()).build();
DataSinkDescriptor dataSink = MROutput.createConfigBuilder(new Configuration
(tezConf),
TextOutputFormat.class, outputPath).build();
Vertex tokenizerVertex = Vertex.create(TOKENIZER, ProcessorDescriptor.
create(
TokenProcessor.class.getName())).addDataSource(INPUT, dataSource);
Vertex summationVertex = Vertex.create(SUMMATION,
ProcessorDescriptor.create(SumProcessor.class.getName()),
numPartitions)
.addDataSink(OUTPUT, dataSink);
OrderedPartitionedKVEdgeConfig edgeConf = OrderedPartitionedKVEdge
Config
.newBuilder(Text.class.getName(), IntWritable.class.getName(),
HashPartitioner.class.getName())
.setFromConfiguration(tezConf)
.build();
DAG dag = DAG.create("WordCount");
dag.addVertex(tokenizerVertex)
.addVertex(summationVertex)
.addEdge(
Edge.create(tokenizerVertex, summationVertex, edgeConf.create
DefaultEdgeProperty()));
return dag;
}
@Override
protected int runJob(String[] args, TezConfiguration tezConf,
TezClient tezClient) throws Exception {
DAG dag = createDAG(tezConf, args[0], args[1],
args.length == 3 ? Integer.parseInt(args[2]) : 1);
LOG.info("Running WordCount");
return runDag(dag, isCountersLog(), LOG);
}
public static void main(String[] args) throws Exception {
int res = ToolRunner.run(new Configuration(), new WordCount(), args);
System.exit(res);
}
}
5.9.2 Hive中Tez和LLAP相关的配置
Tez和MapReduce这两种计算引擎从架构到编写具体的项目代码其实有很多共通的地方,因此在配置Tez的环境参数方面也基本差不多。下面是Tez常见的配置。
• ·tez.am.resource.memory.mb:配置集群中每个Tez作业的ApplicationMaster所能占用的内存大小。
·tez.grouping.max-size、tez.grouping.min-size:配置集群中每个Map任务分组分片最大数据量和最小数据量。
·hive.tez.java.opts:配置Map任务的Java参数,如果任务处理的数据量过大,可以适当调节该参数,避免OOM(内存溢出)。选择合理的垃圾回收器,提升每个任务运行的吞吐量。
·hive.convert.join.bucket.mapjoin.tez:配置是否开启转换成桶MapJoin的表连接。默认是false,表示不开启。
·hive.merge.tezifles:是否合并Tez任务最终产生的小文件。
·hive.tez.cpu.vcores:配置每个容器运行所需的虚拟CPU个数。
·hive.tez.auto.reducer.parallelism:配置是否开启作业自动调节在Reduce阶段的任务并行度。
·hive.tez.bigtable.minsize.semijoin.reduction:设置当大表的行数达到该配置指定的行数时可以启用半连接。
·hive.tez.dynamic.semijoin.reduction:设置动态启用半连接操作进行过滤数据。
·hive.llap.execution.mode:配置Hive使用LLAP的模式,共有以下5种模式。
·none:所有的操作都不使用LLAP。 ·map:只允许Map阶段的操作使用LLAP。
·all:所有的操作都尽可能尝试使用LLAP,如果执行失败则使用容器的 方式运行。
·only:所有的操作都尽可能尝试使用LLAP,如果执行失败,则查询失 败。 ·auto:由Hive控制LLAP模式。
·hive.llap.object.cache.enabled:是否开启LLAP的缓存,缓存可以缓存执行计划、散列表。
·hive.llap.io.use.lrfu:指定缓存的策略为LRFU模式,替换掉默认FIFO模式。
·hive.llap.io.enabled:是否启用LLAP的数据I/O。
·hive.llap.io.cache.orc.size:LLAP缓存数据的大小,默认是1GB。
·hive.llap.io.threadpool.size:LLAP在进行I/O操作的线程池大小,默认为10。
·hive.llap.io.memory.mode:LLAP内存缓存的模式,共有以下3种模式。
·cache:将数据和数据的元数据放到自定义的堆外缓存中。 ·allocator:不使用缓存,使用自定义的allocator。
·none:不使用缓存。因为上面两种方式有可能导致性能急剧下降。
·hive.llap.io.memory.size:LLAP缓存数据的最大值。
·hive.llap.auto.enforce.vectorized:是否强制使用向量化的运行方式,默认为true。
·hive.llap.auto.max.input.size、hive.llap.auto.min.input.size:是否检查输入数据的文件大小,如果为-1则表示不检查。hive.llap.auto.max.input.size默认值为10GB,hive.llap.auto.min.input.size默认值为1GB。