概述
代码地址:https://github.com/liubin95/hadoop
组成部分
HDFS
Hadoop Distributed File System :Hadoop分布式文件存储系统
Map Reduce
批处理数据
Yarn
资源调度组件
优缺点
优
- 高拓展:可以通过增加廉价的机器拓展集群
- 高可靠:数据多节点,多副本,不会丢失
- 高容错:任务失败可以重启
- 高效:Map Reduce并行执行,效率高
缺
- 高延迟:Map Reduce 是离线计算,而且是磁盘计算,延迟高
- 不适合存储大量小文件:
NameNode
都会消耗内存来存储文件目录和块信息。所以,过多的小文件会消耗大量的内存- Mapper阶段默认情况下:每一个小文件会被切成一片,每一片文件会创建一个MapperTask,会消耗大量资源
- 不支持文件的修改:仅支持文件的追加操作,不支持随机修改
- 不擅长有向图计算:前一个Map Reduce任务的输出作为后一个Map Recuce的输入。这样会产生大量的IO操作
配置
- 优先级从高到低。前面覆盖后面的
- 代码指定
package com.liubin.hadoop.hdfs;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileStatus;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.LocatedFileStatus;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.RemoteIterator;
import org.apache.hadoop.io.IOUtils;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
/**
* HDFSClient.
*
* @author huawei.
* @version 0.0.1.
* @serial 2021-03-26 : base version.
*/
public class HDFSClient {
private static final Logger LOGGER = LoggerFactory.getLogger(HDFSClient.class);
private static FileSystem fileSystem;
@BeforeAll
static void beforeAll() throws URISyntaxException, IOException, InterruptedException {
final Configuration configuration = new Configuration();
// 配置副本数
configuration.set("dfs.replication", "1");
fileSystem = FileSystem.get(new URI("hdfs://localhost:9000"), configuration, "liubin");
LOGGER.info("开启链接");
}
@AfterAll
static void afterAll() throws IOException {
fileSystem.close();
LOGGER.info("关闭链接");
}
}
- 客户端配置
D:\hadoop-3.2.1\etc\hadoop\core-site.xml
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
<configuration>
<property>
<name>dfs.replication</name>
<value>1</value>
</property>
</configuration>
- 服务端配置
/opt/hadoop-3.2.1/etc/hadoop/core-site.xml
,同客户端配置方式 - 默认值
\hadoop-hdfs\3.2.1\hadoop-hdfs-3.2.1.jar/hdfs-default.xml
,查询地址
HDFS
组成部分
NameNode 一主
- 处理客户端请求
- 管理block数据的映射信息(类似索引)
- 管理HDFS命名空间
- 配置副本策略
- 可以借助Zookeeper 实现HA(高可用),NameNode HA 官网方案
DataNode 多从
- 存储和管理block数据
- 执行具体的文件读写操作
- 定期向NameNode汇报存储的block数据信息(心跳)
Secondary NameNode
- 不是NameNode热备,不能直接替换NameNode工作
- 辅助NameNode工作,比如定期合并Fsimage 和 Edits ,并推送给NameNode
概述
- 文件是分块存储的(block),块大小可以配置,dfs.blocksize,默认128M。大小的设置取决于磁盘传输速率
- 多副本,dfs.replication,副本数最大就是
dataNode
数量,单节点多副本无意义
命令
hadoop fs <args>
# 例,列出/users下文件和目录
hadoop fs -ls /users
操作和Linux文件系统命令类似,具体参考官网
Map Reduce
概述
组成部分
Map阶段
- 代码靠近数据:通常,计算节点和存储节点是相同的。也就是说,MapReduce框架和Hadoop分布式文件系统在同一组节点上运行
InputFormat
- 对文件进行split(切片),默认的切片规则会将每一个文件切一片,对应一个MapTask,如果小文件过多,会极度消耗资源
- 对切片的每一
键值对
,调用Mapper中的map(WritableComparable,Writable,Context)
方法
常用类型
- TextInputFormat 默认输入Format,<数字(文件中的位置),Text(一行数据)>
- KeyValueTextInputFormat 默认
\t
分割一行,<Text(\t
之前部分),Text(\t
之后部分> - NLineInputFormat 和TextInputFormat 一样,但是可以控制多少行(
setNumLinesPerSplit
)切一片 - CombineTextInputFormat 处理小文件, 具体见下文
- 自定义,见小文件处理中SequenceFile 压缩例子
Mapper
- 通过
Job.setMapperClass(Class)
方法传递给作业 - 通过对
context.write(WritableComparable,Writable)
的调用来输出数据
Shuffle阶段
- Map阶段和Reduce阶段中间的过程即Shuffle
Partitioner 分区
- 将map阶段的数据,重新分区,相同分区的交给同一个ReduceTask处理
- 通过
setNumReduceTasks
设置ReduceTask的数量。可以实现将同一类的数据保存到一个文件
// 设置reduce的数量
job.setNumReduceTasks(4);
NumReduceTasks
等于分区数量才会生成对应数量的文件NumReduceTasks
大于分区数量时,会生成空文件NumReduceTasks
小于分区数量时,会报错
WritableComparable 排序
- Hadoop 会默认对数据的Key进行排序
- 实现
WritableComparable
接口 - Map阶段结束后,数据会写到缓存区。达到阈值后进行一次排序(快排),然后溢写到磁盘。所有数据处理完,对所有有序的文件进行一次合并排序(归并排序)
- Reduce阶段结束后,会对所有数据进行一次排序(归并排序)
- 只能在单个文件中进行排序
@Override
public int compareTo(FlowComparatorBean o) {
if (o.getTotalFlow() > this.totalFlow) {
return -1;
} else if (o.getTotalFlow() < this.totalFlow) {
return 1;
}
return 0;
}
Combiner 合并
- 在Map阶段之后,对MapTask的结果进行一次局部汇总
- 减少和ReduceTask的网络传输,算是一种本地的reduce
- 不适合平均值操作
- 继承Reducer即可,setCombinerClass 设置使用的combiner
// 使用Combiner 优化,使得进入reduce的数据量更少
job.setCombinerClass(WordCountReducer.class);
GroupingComparator 分组
- 分区决定数据去哪一个Reduce,分组决定数据的Key
- 将数据分组成
reduce
入参。按Key和规则,收集一组Iterable<Writeable>
处理
@Override
protected void reduce(FlowComparatorBean key, Iterable<Text> values, Context context)
throws IOException, InterruptedException {
context.write(values.iterator().next(), key);
}
- 继承
WritableComparator
,在job.setGroupingComparatorClass(class);
设置
package com.liubin.hadoop.mapreduce.order;
import org.apache.hadoop.io.WritableComparable;
import org.apache.hadoop.io.WritableComparator;
import java.util.Objects;
/**
* OrderComparator.
*
* @author 刘斌
* @version 0.0.1
* @serial 2021-04-02 : base version.
*/
public class OrderGroupingComparator extends WritableComparator {
public OrderGroupingComparator() {
super(OrderComparator.class, true);
}
@Override
public int compare(WritableComparable a, WritableComparable b) {
// id相同,是相同的Key
final OrderComparator a1 = (OrderComparator) a;
final OrderComparator b1 = (OrderComparator) b;
if (Objects.equals(a1.getOrderId(), b1.getOrderId())) {
return 0;
} else {
return 1;
}
}
}
Reduce阶段
OutputFormat
- 自定义reduce结果的输出形式
- 实现方式和
InputFormat
类似
Reduce
- 通过
Job.setReducerClass(Class)
方法传递给作业 - 注意入参是<key,Iterable<Writeable>> 形式,注意循环
Join
- 将不同文件的数据聚合在一起
Reduce Join
- 在Reduce阶段将数据拼装
- 通过Mapper 的setup方法,获取数据来源文件。注意:即使没有的字段也要设置默认值,否则序列化会报错
@Override
protected void setup(Context context) throws IOException, InterruptedException {
// 获取文件名
final FileSplit fileSplit = (FileSplit) context.getInputSplit();
// 不同文件设置不同的标识位
this.name = fileSplit.getPath().getName();
}
- 将关联的字段作为Key,这样数据就会分到一个Reduce的一组键值对中
- 根据表示位获取到不同的对象,最后拼装数据。注意:对象的拷贝
问题
- 数据倾斜:大量数据集中在少数的ReduceTask中
- Reduce 负载太大
Map Join
- 将小表缓存到内存中,提前处理,减少Reduce的负载和数据倾斜
- 设置缓存
job.setCacheFiles(new URI[] {new URI("file:///D:/tmp/input/join/pd.txt")});
- 在Map阶段
setup
方法中读取缓存文件
@Override
protected void setup(Context context) throws IOException, InterruptedException {
// 获取缓存的文件
final URI cacheFile = context.getCacheFiles()[0];
final BufferedReader bufferedReader =
new BufferedReader(
new InputStreamReader(
new FileInputStream(cacheFile.getPath()), StandardCharsets.UTF_8));
String line;
// 读取文件
while (StringUtils.isNoneEmpty(line = bufferedReader.readLine())) {
// 03 苹果
final String[] split = line.split("\t");
// 缓存到内存
this.idAndName.put(split[0], split[1]);
}
IOUtils.closeStream(bufferedReader);
}
- 在
map
方法中使用缓存
计数器
- Hadoop默认维护若干计数器,以描述多项指标
使用
// 计数器(组名,计数器名)
final Counter counterTrue = context.getCounter("map", "true");
//+1
counterTrue.increment(1L);
命令
# jar包路径,输入路径,输出路径
hadoop jar ./share/hadoop/mapreduce/map-reduce-1.0-SNAPSHOT.jar /users/liubin/input /users/liubin/output/0401
序列化
- Java 自带的Serializable 是重量级序列化,不适合大量io和网络传输操作。所以Hadoop自定义了writable序列化接口
write
和readFields
中字段顺序要保持一致
Yarn
组成部分
ResourceManager 集群一个
- 管理集群中的所有资源
NodeManager 一个机器一个
- 负责每一台机器的资源(cpu,内存,磁盘,网络)监视
ApplicationMaster 一个Job一个
- 向ResourceManager 申请Job需要的资源
- Job的监控和容错
Container 资源的抽象
- 资源的封装(类似于yarn资源的计量单位)
提交任务流程
- client(客户端)向ResourceManager 提交Job(即MR程序)
- ResourceManager 为Job分配一个Container ,并要求所在的NodeManager 在Container 中启动ApplicationMaster
- ApplicationMaster 启动后注册到ResourceManager ,然后重复步骤4-7
- ApplicationMaster 通过RPC协议向ResourceManager 申请资源
- 与资源所在的NodeManager 通信,要求它启动Task(经过ApplicationMaster 切分的Job,即MapTask、ReduceTask)
- NodeManager 为Task设置运行时环境(包括环境变量、JAR包、二进制程序等),然后启动Task
- Task通过RPC协议向ApplicationMaster 汇报自己的状态和进度,以便ApplicationMaster 监控和容错
- Job行完成后,ApplicationMaster向ResourceManager注销并关闭自己
小文件处理
Har 压缩
将一组文件压缩为一个*.har格式的文件,减少NameNode内存消耗
压缩
hadoop archive -archiveName name -p <parent> [-r <replication factor>] <src>* <dest>
# 例,将/users/liubin/input下文件压缩到/users/liubin/output/input.har
hadoop archive -archiveName input.har /users/liubin/input /users/liubin/output/
- name:指定存档文件名,其名应该以 .har 为后缀。
- -p :表示父目录,指定该参数后,后面的src可使用相对路径。dest使用相对路径时,其相对基础目录为计算用户目录而非-p指定的目录。
- -r 指示所需的复制因子;如果未指定此可选参数,则使用10的复制因子。所谓复制因子即为map任务的个数。
- src:源路径:可以指定指定一个,也可以指定多个,空格分隔
- dest:目标路径:最后一个路径为存放路径。
使用压缩文件
通过har 协议使用har压缩文件
hadoop fs -ls har:///users/liubin/output/input.har
SequenceFile 压缩
- 将小文件合并成<文件全路径,文件内容(二进制)>的形式储存
自定义MR程序实现
- 自定义
FileInputFormat
,设置不分区,使用自定义的RecordReader
package com.liubin.hadoop.mapreduce.wholefile;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.InputSplit;
import org.apache.hadoop.mapreduce.JobContext;
import org.apache.hadoop.mapreduce.RecordReader;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import java.io.IOException;
/**
* WholeFileInputFormat.
*
* @author 刘斌
* @version 0.0.1
* @serial 2021-04-01 : base version.
*/
public class WholeFileInputFormat extends FileInputFormat<Text, BytesWritable> {
@Override
protected boolean isSplitable(JobContext context, Path filename) {
//不分区,一次读取一个文件
return false;
}
@Override
public RecordReader<Text, BytesWritable> createRecordReader(
InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException {
return new WholeFileRecordReader();
}
}
- 自定义
RecordReader
,将每一个文件读取成<文件名,文件流>的形式
package com.liubin.hadoop.mapreduce.wholefile;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.InputSplit;
import org.apache.hadoop.mapreduce.RecordReader;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;
import java.io.IOException;
/**
* WholeFileRecordReader. 每一个文件执行一次,新对象
*
* @author 刘斌
* @version 0.0.1
* @serial 2021-04-01 : base version.
*/
public class WholeFileRecordReader extends RecordReader<Text, BytesWritable> {
private FileSplit fileSplit;
private Configuration configuration;
private final Text text = new Text();
private final BytesWritable bytesWritable = new BytesWritable();
private boolean isProgress = true;
@Override
public void initialize(InputSplit split, TaskAttemptContext context)
throws IOException, InterruptedException {
this.fileSplit = (FileSplit) split;
this.configuration = context.getConfiguration();
}
@Override
public boolean nextKeyValue() throws IOException, InterruptedException {
if (isProgress) {
// 缓存区
byte[] bytes = new byte[(int) fileSplit.getLength()];
// 读取的文件path
final Path path = fileSplit.getPath();
// Hadoop 文件系统实例
final FileSystem fileSystem = path.getFileSystem(configuration);
// 获取文件的流
final FSDataInputStream inputStream = fileSystem.open(path);
// 读取到缓存区
IOUtils.readFully(inputStream, bytes, 0, (int) fileSplit.getLength());
// 设置到value
this.bytesWritable.set(bytes, 0, bytes.length);
// 设置key
this.text.set(path.toString());
// 关闭输入流
IOUtils.closeStream(inputStream);
this.isProgress = false;
return true;
}
return false;
}
@Override
public Text getCurrentKey() throws IOException, InterruptedException {
return this.text;
}
@Override
public BytesWritable getCurrentValue() throws IOException, InterruptedException {
return this.bytesWritable;
}
@Override
public float getProgress() throws IOException, InterruptedException {
return 0;
}
@Override
public void close() throws IOException {}
}
- Driver 类设置参数
// 设置使用自定义的输入类
job.setInputFormatClass(WholeFileInputFormat.class);
// 输出使用SequenceFileOutputFormat
job.setOutputFormatClass(SequenceFileOutputFormat.class);
- 使用时,需要自定义InputFormat读取文件
CombineTextInputFormat
- 通过InputFormat减少小文件的mapper阶段的切片,从而减少MapperTask的数量,减少资源的浪费
- 虚拟化划分:根据setMaxInputSplitSize将所有输入文件,重新虚拟分区
- 切片 :根据虚拟分区,重新切片,分区小于setMaxInputSplitSize的会合并
使用
final Configuration configuration = new Configuration();
final Job job = Job.getInstance(configuration);
// 使用CombineTextInputFormat处理小文件
job.setInputFormatClass(CombineTextInputFormat.class);
// 设置分区的最大值
CombineTextInputFormat.setMaxInputSplitSize(job, 10 * 1024 * 1024);
JVM 重用
- Map阶段结束后,JVM会继续执行其他Map。节约时间
- 配置
mapred-site.xml
中mapreduce.job.jvm.numtasks
压缩
- 可以在MapReduce任何阶段使用压缩
- 计算密集型,少用压缩;IO密集型,多用压缩
类型
gzip
- 适用于压缩后小于文件块大小的场景
Bzip2
- 速度慢,压缩率高。适用于追求极致压缩率的场景
LZO
- 适合于文件压缩后还大于块大小的场景
Snappy
- 压缩速度极致快
位置
Map之前
- 自动匹配拓展名,自动解压缩文件
- 配置方式:
core-site.xml
中io.compression.codecs
可以多种方式配置
Map 和Reduce之间
- 选择速度快的压缩类型,比如:LZO
- 配置方式:
mapred-site.xml
中mapreduce.map.output.compress
:开启mapreduce.map.output.compress.codec
:指定压缩的类型
Reduce之后
- 减少磁盘存储量
- 也会减少下一个Job的输入量
- 配置方式:
mapred-site.xml
中mapreduce.output.fileoutputformat.compress
:开启mapreduce.output.fileoutputformat.compress.codec
:指定压缩类型mapreduce.output.fileoutputformat.compress.type
:如果是SequenceFiles,可以指定类型(NONE、RECORD、BLOCK)