一、hdfs的架构
Client:对文件的切分(块),与namenode、datanode交互
namenode:管理命名空间,管理块的映射信息,处理客户端的读写请求
datanode:存储实际的数据块,执行数据块的读写操作
二、hdfs常用命令
1、-moveFromLocal file(本地文件) target-dir 将文件剪切到指定目录
2、-get file(hdfs中的指定文件) tartget-dir 将文件拷贝到本地指定目录
3、-mv path1(原路径)path2(指定路径)将hdfs上的文件从原路径移动到指定路径
4、-rm [skipTrash] 删除指定文件(一般是放入回收站,skipTrash选项可以跳过放入回收站直接删除)
5、-cp [-f(选项将覆盖已存在目标)] [-p(选项将保留文件属性(时间戳,所有权等))]原路径 目标路径
6、-chmod [-R(对整个目录的文件修改权限)] 修改权限
7、-chown 改变文件所属用户组
8、-appendToFile <localfile> <dir> 合并小文件成一个大文件
三、hdfs的高级使用命令
1、hdfs文件限额配置:限制文件数量、限制文件大小
Ⅰ通过-count -q -h <dir> 来查看文件配额的限制
第一个none是文件数量限额(这里没有),第二个none是文件大小限额(也没有)
Ⅱ数量限额:hdfs dfsadmin -setQuota num(数量限额) dir(设置限额的目录)
⭐注意:实际文件存放个数=数量限额-1,下图中的2后面的0就表示还能存放的文件个数(之前已经有一个1.txt了)
取消数量限额:hdfs dfsadmin -clrQuota dir(需要清除限额的目录)
Ⅲ大小限额(文件):hdfs dfsadmin -setSpaceQuota 384M(这里是文件大小限额,最小也得384:一个块是128M,有三个冗余存储副本,所以最少也得blocksize*3) dir(目标目录)
ex:
取消大小限额:hdfs dfsadmin -clrSpaceQuota dir(需要清除限额的目录)
2、hdfs的安全模式
在安全模式下,如果说副本率(实际存在副本数 / 默认的副本数)<0.999,那么系统会自动复制副本直到副本率>=0.999。而当整个系统达到安全标准时,HDFS将自动离开安全模式;
⭐在安全模式下,文件系统只接受读数据请求,而不接受数据修改、删除等变更请求
安全模式操作命令:
1、hdfs dfsadmin -safemode get 查看安全模式是否开启
2、hdfs dfsadmin -safemode enter 打开安全模式
3、hdfs dfsadmin -safemode leave 关闭安全模式
3、hdfs基准测试
写入速度测试、读取速度测试(用批量的文件写入的时间来衡量)
4、hdfs的文件写入和读取过程
写入:
注意:下图为一个块的上传过程
读取:
四、HDFS的API操作
关于FileSystem类中的各种方法,参照FileSystem类的方法使用_阿生-CSDN博客,非常有用
1、使用文件系统访问数据:
在Java中操作HDFS,主要涉及到一下class:
●Configuration 多种服务器和客户端的配置
●FileSystem 该类的对象是一个文件系统对象,可以通过其静态方法get()来获取该对象 Ex:FileSystem fs=FileSystem.get(conf)---conf即使上述Configuration中的一种对象,用于确定fs的操作对象是哪种类型的文件系统
要点:(这里以创建文件夹为例)
1、创建两个对象(一个Configuration对象,一个FileSystem对象)
ex:
private static Configuration conf = null;
private static FileSystem fs = null;
2、设置初始化的方法,用于与hdfs建立连接
public void connect2HDFS() throws IOException {
//设置客户端的身份(具有相应权限,以本地电脑的权限只能够写)
System.setProperty("HADOOP_USER_NAME","hadoop");
//创建配置对象实例
conf = new Configuration();
//设置操作的文件系统是HDFS,并指定HDFS操作地址
conf.set("fs.defaultFS","hdfs://192.168.223.134:9000");
//创建FileSystem对象实例
fs = FileSystem.get(conf);
}
注意:setProperty的第二个参数表示以什么权限的用户进入(这里是hadoop,因为root用户也只有读写权限,但是hdfs默认的超级用户是hadoop(进那个管理网页查看(我的是虚拟机ip:9870-------只适用于hadoop3.1.3),所以我就改成hadoop了),第一个参数就是一个名字
Configuration的set方法,第一个参数是固定的(因为是hdfs,可以去hadoop的etc目录下查看core-site.xml看这些配置,第二个参数也可以),第二个参数是连接hdfs的关键,前面是虚拟机的ip地址,后面是core-site.xml里面规定的参数9000
⭐FileSystem的get方法,就是将Confguration的实例化对象(已初始化)传给FileSystem的对象,让他能够针对不同的文件系统进行不同的操作
初始化之后,也要进行对fs的关闭(即关闭客户端和hdfs的连接)fs.close()
public void close() throws IOException {
//首先判断一下文件系统实例是否为null,如果不为空就关闭
if (fs != null){
fs.close();
}
2、在hdfs里面创建目录
public void mkdir() throws IOException {
//首先判断文件夹是否存在
if (!fs.exists(new Path("/itljh"))){
//创建文件夹
fs.mkdirs(new Path("/itljh"));
}
}
fs是一个FileSystem对象,exist是表示目录是否存在,若存在则返回true,每个目录都得新建个Path对象来表示
3、往hdfs中上传文件和从hdfs中下载文件
上传文件:(主要是FileSystem的一个copyromLocalFile方法,第一个是本地文件的路径,第二个是hdfs的文件路径------------一定一定要记得带上文件名,两个路径都得带)
/**
* 在hdfs中上传data.csv文件
*/
public void upfile() throws IOException {
//两个路径,src是本地文件系统文件的路径,dst是hdfs中的路径(文件夹)
Path src = new Path("C:\\Users\\Lenovo\\Desktop\\实验室\\第三次作业(2)\\data.csv");
Path dst = new Path("/itljh/data.csv");
//判断文件是否存在
if (!fs.exists(new Path("/itljh/data.csv"))) {
//文件上传动作
fs.copyFromLocalFile(src, dst);
}
}
下载文件:与上传文件中不同的是,方法名是copyToLocalFile,第一个路径是hdfs中文件的路径,第二个路径是需要下载到本地的文件的路径)
/**
* 从hdfs中下载文件到本地
*/
public void getfile() throws IOException {
//hdfs文件路径
Path src = new Path("/itljh/data.csv");
//本地文件系统存放文件路径
Path dst = new Path("D:\\data.csv");
//文件下载
fs.copyToLocalFile(src,dst);
}
4、迭代遍历指定目录中的所有文件,并进行一定操作
迭代器的相关用法
package java.util; public interface Iterator<E> { boolean hasNext();//判断是否存在下一个对象元素 E next();//获取下一个元素 void remove();//移除元素 }
2.Iterable
Java中还提供了一个Iterable接口,Iterable接口实现后的功能是‘返回’一个迭代器,我们常用的实现了该接口的子接口有:Collection、List、Set等。该接口的iterator()方法返回一个标准的Iterator实现。实现Iterable接口允许对象成为Foreach语句的目标。就可以通过foreach语句来遍历你的底层序列。
Iterable接口包含一个能产生Iterator对象的方法-,并且Iterable被foreach用来在序列中移动。因此如果创建了实现Iterable接口的类,都可以将它用于foreach中。
原文链接:https://blog.csdn.net/a1439775520/article/details/95377398
1、调用listFiles方法(是literable的子接口之一)创建一个迭代器对象,两个参数:第一个参数表示要迭代遍历的目录(这里为根目录),第二个参数(boolen类型)表示是否进行递归遍历(是否进入子目录遍历)
2、遍历迭代器中的元素(while循环)
过程分析:
①判断迭代器中是否还有下一个元素(文件)
②有则进入遍历循环,用一个对象来接受迭代器中对应文件信息(next()方法)
③用于获取文件信息的方法有很多,一般都是get开头,如下面的getPath()就可以返回文件所在的路径,而getPath()下面又有一个方法getName()就可以返回文件名;类似的,getBlockLocations()方法可以获取到该文件块的信息,而其下面的length()方法就可以直接返回文件按被分成的块的个数
public void show_all_files() throws IOException {
//1、调用listfiles获取指定目录下所有的文件信息(返回值是一个迭代器),并将其传给一个迭代器对象(第一个参数是路径,第二个参数是是否递归获取所有文件信息(boolen类型)
RemoteIterator<LocatedFileStatus> iterator = fs.listFiles(new Path("/"), true);
//2、遍历迭代器
while(iterator.hasNext()){
//把每一个文件信息传给filestatus
LocatedFileStatus fileStatus = iterator.next();
//获取文件的绝对路径
System.out.println(fileStatus.getPath()+"-------"+fileStatus.getPath().getName());
//打印该文件被切分成多少个block
System.out.println("block数:"+(fileStatus.getBlockLocations().length));
}
}
5、小文件的合并
①直接在命令行对hdfs中的小文件进行合并,并将合并后的文件下载到本地(只能下载到本地,所以基本不常用)
hdfs dfs -getmerge /itljh/*.txt /usr/local/total.txt
第一个参数是hdfs中需要合并的文件路径,第二个参数是下载到本地的总的文件的路径
⭐②通过API在文件上传的过程中就将其合并成一个大文件上传到hdfs中
重点:先在本地将小文件数据提取出来,再通过foreach循环将其数据输出到hdfs中的合并的大文件中(这里发现一个规律:凡是涉及到文件信息的,都离不开staus这个后缀-------------------前面的locatedfilestatus,这里的filestatus数组,都是存放的文件信息,可以通过get的众多方法提取文件的信息)
public void merge_files() throws IOException {
//获得hdfs合并的大文件的输出流
FSDataOutputStream outputStream = fs.create(new Path("/itljh/total.txt"));
//获取本地文件系统(注意这里是FileSystem,不是fs,fs是获取到的远程的hdfs文件系统,这里的local获取到的是本地的文件系统
LocalFileSystem local = FileSystem.getLocal(new Configuration());
//数组存放路径下各个小文件的详情
FileStatus[] locatedFileStatusRemoteIterator = local.listStatus(new Path("D:\\input"));
for (FileStatus filestaus:locatedFileStatusRemoteIterator
) {
//输入流获取到各个小文件中的数据(通过本地文件系统打开文件所在路径从而将数据传入到输入流对象中)
FSDataInputStream inputStream = local.open(filestaus.getPath());
//将小文件的数据复制到大文件
IOUtils.copy(inputStream,outputStream);
//每次都要关闭一次输入流(因为每次循环都创建了)
IOUtils.closeQuietly(inputStream);
}
//关闭输出流
IOUtils.closeQuietly(outputStream);
//关闭本地文件系统
local.close();
}
6、hdfs的相关配置
所有配置都需要进入Hadoop的配置目录,也就是(前提是在hadoop安装目录下)etc/hadoop中的hdfs-site.xml来查看自己对于文件切片副本的个数的设置、hdfs文件权限的设置、文件切片大小的设置
Ⅰdfs.replication---------文件切片副本数的配置,下面的数字是设定的切片副本数
Ⅱ dfs.permissions------hdfs的文件权限的配置,false是代表hdfs文件权限没用,true是代表启用hdfs的权限限制
Ⅲ dfs.blocksize----------文件切片大小的配置,下面是切片块的大小
五、mapreduce
mapreduce处理的数据类型都是键值对形式<key,value>,多用于离线数据处理(只能处理静态数据)
1、编程规范:
● 用户编写的程序代码分为三个部分(都是类):Mapper,Reducer,Driver(客户端提交驱动程序)
●用户自定义的Mapper和Reducer都要继承各自的父类(Mapper类和Reducer类);
Mapper中的业务逻辑写在map()方法中;
Reducer中的业务逻辑写在reduce()方法中;
整个程序需要一个Driver类来进行提交,提交的是一个描述了各种必要信息的job对象(将组件和MR自带的部分拼接成完整的MR)-------------固定的
2、MR内部执行的流程
组件:读取数据组件InputFormat、输出数据组件OutPutFormat
行为:排序(key的字典序排序)、分组(reduce阶段key相同的为一组,一组调用一次reduce操作)
3、Hadoop序列化机制-------Writable
● 序列化:将结构化对象转换为字节流以便进行网络传输和存储;
● 反序列化:是将字节流转化为一系列结构化对象的过程,重新构建该对象;
Java序列化的机制:将对象表示成一个二进制的字节数组,里面包含了对象的具体信息;通过这种方式使得对象能够上传到网络或者数据库中,利于其数据的保存和传递,之后能够通过反序列化读取到对象的具体信息,使得对象也能够变成可传递的;
而要实现序列化,就需要实现java.io.Serializable接口
Hadoop的序列化机制的实现是通过Writable接口:
接口提供两个方法--------write、readFileds
write叫做序列化方法,用于把对象的指定的字段写出去;
readFileds叫做反序列化方法,用于从字节流中读取字段重构对象;
Hadoop没有提供对象比较功能,所以和Java中的Comparable接口合并,提供一个接口WritableComparable;该接口可用于用户自定义对象的比较规则(自己定义序列化、反序列化还有对象的比较标准的方法)
Hadoop封装的数据类型(这些数据类型都实现了WritableComparable接口,以便于用这些类型定义的数据都可以被序列化进行网络传输和文件存储,以及进行大小的比较)与Java中数据类型的对应关系如图:
而这一些Hadoop的数据类型的变量都能通过set()方法来将对应的Java类型的变量转换成Hadoop类型的变量,同时也可以通过get()方法将Hadoop变量转换成Java变量,例子如下:
private LongWritable outvalue =new LongWritable();
Long count = 10;
outvalue.set(count);
//此时outvalue的值就是10,类型还是LongWritable
for (LongWritable value : values){
//通过get方法去取value中的值(Long类型)
count += value.get();
}
4、经典案例:wordwcount
ⅠMapper导包(Maven依赖,这里不多赘述):注意,继承Mapper类的时候,要注意导进来的包的路径,后缀是mapred的包是老版本的,最好不用;要用mapreduce的
Ⅱ 创建wordcount的专属mapper类(继承Mapper类),必须要规定四个参数的类型
/**
* wordcount的Mapper类,对应maptask
* public class Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT>
* KEYIN:表示map阶段输入的key类型(默认组件下,是起始位置的偏移量,所以类型是LongWritable)
* VALUEIN:表示map阶段输入的value类型(默认为每一行的内同,为Text类型)
* TODO mapreduce有默认的读取数据的组件,叫做TextInputFormat,读取数据的行为是一行一行读取数据,返回<k,v>键值对
* TODO k:每一行的起始位置的偏移量(就是光标相对于第一行第一列的值(初始值为0))----------通常无意义
* TODO V:这一行的文本内容
* 下面:(你想要的键值对对应的类型,跟业务相关,这里是单词和次数)
* KEYOUT:表示map阶段输出的key类型
* VALUEOUT:表示map阶段输出的value类型
*/
public class wc_Mapper extends Mapper<LongWritable, Text,Text,LongWritable> {
}
Ⅲ 编写map方法(重点:变量复用)
public class wc_Mapper extends Mapper<LongWritable, Text,Text,LongWritable> {
//采用变量复用的方法,以减少每次输出键值对都需要new两个对象,原代码context.write(new Text(word),new LongWritable(1));
private Text outkey = new Text();
private final static LongWritable outvalue = new LongWritable(1);
/**
* map方法是具体业务逻辑实现的方法
* 其被调用的次数与输入的kv键值对有关,每当TextFormat返回一个键值对就调用一次map方法对数据进行处理
* 默认情况下,map方法是基于行来处理数据的
*/
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//拿一行数据转化为String类型
String line = value.toString();
//根据分隔符进行切割(//s+表示:s表示空白符,+表示任意多个).var生成包含各个单词的数组(以第一行为例):Hello band Hello trump
String[] words = line.split("\\s+");
//for循环遍历数组中的元素快捷键:.iter
for (String word : words) {
//输出数据 把每个单词都标记1-----<单词,1>
//使用上下文对象context的write方法输出键值对
outkey.set(word);
context.write(outkey,outvalue);
}
//循环结束后输出结果为<Hello,1>,<band,1>,<Hello,1>,<trump,1>
}
}
Ⅳ 创建wordcount专属的reduecer类(继承Ruducer类),同样有四个参数
/**
* keyin:从map阶段过来的key
* valuein:从map阶段过来的value
* 本需求中kvout与map输出的相同
* keyout:业务需要的key的类型,单词
* valueout:业务需要的value的类型,单词的总次数
*/
public class wc_Ruducer extends Reducer<Text, LongWritable, Text, LongWritable> {
}
Ⅴ 编写reduce方法
public class wc_Ruducer extends Reducer<Text, LongWritable, Text, LongWritable> {
private LongWritable outvalue =new LongWritable();
/**
* (默认)1、排序,根据字典序进行排序
* (默认)2、分组,key相同的为一组
* (默认)3、分组后同一组的数据组合成一个新的键值对,调用一次reduce方法
* 因此reduce方法是基于分组调用的,同一组中的数据将组成新的kv键值对
* 新的key是该组共同的key,新value是所有的value组成的迭代器
* Hello band Hello trump--------<Hello,Iterable[1,1]>,<band,1>,<trump,1>
*/
@Override
protected void reduce(Text key, Iterable<LongWritable> values, Context context) throws IOException, InterruptedException {
//统计变量
long count = 0;
for (LongWritable value : values) {
//通过get方法去除value中的值(Long类型)
count += value.get();
}
//输出对应值
outvalue.set(count);
context.write(key,outvalue);
}
}
Ⅵ 编写Driver类(大部分都是模板)
public class wc_Driver{
//该类就是客户端的驱动类,主要是构造job对象实例
//指定各个组件的属性,包括mapper类、reducer类,输入输出的数据类型、输入输出的数据路径,提交job作业----job.submit()
public static void main(String[] args) throws Exception{
//创建配置对象
Configuration conf = new Configuration();
//构建job对象,参数(配置对象,job的名字)
Job job = Job.getInstance(conf, wc_Driver.class.getSimpleName());
//下面是驱动的固定模板
//设置mr程序运行的主类
job.setJarByClass(wc_Driver.class);
//设置本次mr程序的mapper类、reducer类
job.setMapperClass(wc_Mapper.class);
job.setReducerClass(wc_Ruducer.class);
//指定map阶段输出的kv的数据类型
job.setMapOutputKeyClass(Text.class);
job.setOutputValueClass(LongWritable.class);
//指定reduce阶段输出的kv的数据类型,也就是最终输出的数据类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(LongWritable.class);
//配置本次作业的输入输出路径,和输出数据路径
// TODO 默认组件:TextInputFormat---输入;TextOutPutFormat---输出
//这里指定路径要从控制台输入,更加方便
FileInputFormat.setInputPaths(job,new Path(args[0]));
FileOutputFormat.setOutputPath(job,new Path(args[1]));
//提交作业,参数表示是否实时追踪作业的执行情况
boolean resultflag = job.waitForCompletion(true);
//退出程序,与job结果进行绑定,为true则正常退出,为false则异常退出
System.exit(resultflag ? 0 :1);
}
}
Ⅶ mapreduce的运行模式
1、单机模式/分布式运行?
2、需要的运算资源是Hadoop YARN分配/本机系统自己分配?
因此衍生出了两种运行模式:YARN集群模式和本地模式。而运行在何种模式,取决于参数mapreduce.framework.name
●yarn:YARN集群模式
●local:本地模式
而如果不指定,默认的就是local模式。关于模式的配置,需要在mapred-default.xml中进行定义,如果代码中(conf.set())、运行的环境中有配置(mapred-default.xml),会默认覆盖掉default配置。因此,一定要在代码中去设置-----------------------(conf.set("mapreduce.framework.name","运行模式"))
本地运行结果:(直接运行Driver类中的main函数,注意:还需要在控制台顶端程序运行的地方,对里面的进行编辑------因为我们在main中设定的文件输入和输出的路径是从终端输入的(args[]),所以要进行配置,配置如下)
终端文件输入路径和输出路径的配置:
统计文件:
输出文件:
说明:我没有配置YARN环境和集群,所以后续MR代码都是运行在本地文件系统
5、Partition分区(业务需求中需要输出多个文件时要考虑到的方法)
引子:输出结果文件个数等于ReduceTask个数(修改ReduceTask可以通过job对象的方法job.setNumReduceTasks(num)来对Reducetask进行修改),相应的,最终得到的结果也被分到了多个输出结果文件中;
因此,当MR中有多个reducetask执行的时候,此时maptask就面临一个问题:自己的输出数据究竟要交给哪个reducetask来处理?-------------------这就是数据分区问题
● 默认情况下,MR只有一个reducetask来进行数据处理
● 而当有多个reducetask的时候,就需要用到数据分区
分区的默认规则:(不能保证平均分配,但是key相同的肯定会分到一个分区)
HashParttition-----------跟hashcode有关,分区的结果和map输出的key有关
reducetask的设置:
而如果默认的分区规则不能够满足我们业务的需求,就需要对分区的规则进行重写
重写定义的partition类:(这里以疫情统计为例---目标是州相同的数据放到同一个分区(即输出到多个文件))
package cn.itljh.mapreduce.covid.Partitioner;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;
import java.util.HashMap;
//Reduce端接受的的键值对的类型一定要在声明Partitioner类中先声明(下面的<Text,Text>)
public class States_Partitioner extends Partitioner<Text,Text> {
//模拟一下美国各州的数据字典,实际中可以从redis进行读取加载,如果数据量不大,也可以创建数据集合保存
public static HashMap<String,Integer> state = new HashMap<String,Integer>();
static {
//州的对应编号
state.put("Alabama",0);
state.put("Alaska",1);
state.put("Arizona",2);
state.put("California",3);
state.put("Colorado",4);
}
/**
* TODO 下面即自定义分区中分区规则实现的方法,只要返回值相同,数据就就会被分到同一个分区
* TODO 而所谓同一个分区,就是分到同一个ReduceTask来处理
* @param key state 州
* @param value 文本数据
* @param i
* @return
*/
@Override
public int getPartition(Text key, Text value, int i) {
Integer code = state.get(key.toString());
if (code!=null)
{
//取出对应编号
return code;
}
//不在这些州的数据分到5分区
return 5;
}
}
而对于自定义分区功能的实现,则需要我们在主程序的Driver类中对于分区进行说明,具体如下:(这些设定的声明需要在reduce程序的设计之后)
reducetask最好是跟我们的分区个数保持一致
6、Combiner规约
Combiner是MR的一种优化手段,其作用是对map端的输出先做一次局部合并,以减少在map节点和reduce节点之间的数据传输量(本质上就是一个reduce)
Combiner在默认情况下是不启用的。Combiner是作用在一个maptask上,是一个局部聚合;而reducer是对所有maptask的输出结果进行计算,是一个全局聚合;
●具体实现步骤:自定义一个CustomCombiner类,继承Reducer类,重写reduce方法;重写完成后,需要job.setCombinerClass(CustomCobiner.class)才能生效
●注意事项:Combiner在普通场景慎用,在以下场景禁用:
-业务与数据个数相关的;
-业务与整体排序相关的
ex:在wordcount中开启Combiner:
// TODO 设置mapreduce的Combiner类(局部单词统计的逻辑和reducer一样)
job.setCombinerClass(wc_Ruducer.class);
7、MR的自定义分组规则(在reduce开始之前的key相同的分组)(注意,是先排序后分组)
●写类继承WritableComparator,重写Compare方法
●只要Compare方法返回为0,MR框架在分组的时候就会认为前后两个相等,将其分为一组
●在job对象中进行设置,让自己的重写分组类生效
job.setGroupingComparatorClass(xxxxx.class--------即自己重写的分组类)
类的设置:
/**
* MR中自定义分组的类
*/
public class CovidGroupingComparator extends WritableComparator {
protected CovidGroupingComparator()
{
super(CovidCountBean_OfS_C_C.class,true); //允许创建对象实例
}
@Override
public int compare(WritableComparable a, WritableComparable b) {
//类型转换
CovidCountBean_OfS_C_C a1 = (CovidCountBean_OfS_C_C) a;
CovidCountBean_OfS_C_C b1 = (CovidCountBean_OfS_C_C) b;
//分组规则:只要州相同就分到同一组
//相等就返回0(直接compareto即可)
return a1.getStates().compareTo(b1.getStates());
}
}
job的设置:
六、MR编程指南
1、技巧:
●MR执行流程要足够清楚,能够知道数据在MR中的流程;
●业务的需求解读准确,明白需要做什么;
●牢牢把握住key的选择,因为MR很多行为跟key相关,比如:排序、分区、分组;
●学会自定义组件修改默认行为,当默认的行为不满足业务需求,可以尝试自定义规则;
●学会画图梳理业务的执行流程,确定每个阶段的数据类型;
2、实用小方法(自己总结的)
Ⅰ如果reduce端输出的键值对中有一方(键或值)用不上输出,那么可以在Reducer程序声明的时候就可以将其中一方没用的的类型声明为NullWritable,相应的在job的设置、Reduce程序里面的context.write()也要改动,具体如下:
reduce程序里面的改动:
job的改动:(设置reduce输出数据的类型)
Ⅱ 在自定义对象实现序列化机制的时候,如果属性中有String类型的数据,需要序列化和反序列化的时候,dataOutPut和dataInPut的方法分别如下:(UTF)
public void write(DataOutput dataOutput) throws IOException {
dataOutput.writeUTF(this.states);
dataOutput.writeUTF(this.county);
dataOutput.writeLong(this.cases);
}
@Override
public void readFields(DataInput dataInput) throws IOException {
this.states = dataInput.readUTF();
this.county = dataInput.readUTF();
this.cases = dataInput.readLong();
}
Ⅲ 在自定义分组规则时,如果key是自定义分组对象,有多个key因分组规则被分到同一组时,在reduce阶段取的key就是第一个<k,v>的key;因此指定分组的同时也要制定好排序规则才能更好的筛选出想要的数据
public class CovidTopOne_Reducer extends Reducer<CovidCountBean_OfS_C_C, NullWritable,CovidCountBean_OfS_C_C,NullWritable> {
NullWritable outvalue = NullWritable.get();
@Override
protected void reduce(CovidCountBean_OfS_C_C key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
// todo 不遍历迭代器,此时的key就是第一个键值对中的key
context.write(key,outvalue);
}
}
而如果说想要取出排序前几的key,则可以用到一个迭代器和一个计数器;迭代器用于迭代各个value的值,而计数器则是限制取出key的个数-------相当于取出对应value的对应的key的值
(具体原理看下一点----reduce分组中key与values迭代器之间的关系)
public class CovidTopOne_Reducer extends Reducer<CovidCountBean_OfS_C_C, NullWritable,CovidCountBean_OfS_C_C,NullWritable> {
NullWritable outvalue = NullWritable.get();
@Override
protected void reduce(CovidCountBean_OfS_C_C key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
// todo 不遍历迭代器,此时的key就是第一个键值对中的key
//context.write(key,outvalue);
//这里以限制前三个为例
int count = 0;
//这个count的计数器的定义必须在reduce方法内部,保证每一次reduce操作只会取前三个key;如果放在外面的话,这个就只能输出三个数据
for(NullWritable value : values)
{
if (count<3)
{
context.write(key,outvalue);
count++;
}
else
{return;}
}
}
}
⭐Ⅳ reduce分组中key和values迭代器之间的关系
通过查看reduce方法的源码可以得知:当reduce阶段在run(即跑程序的时候),调用的判断函数-------getCurrentKey-------------即现在的key是取决于现在的value;我们取的是哪个value,那么对应的取出来的就是对应value的key
因此,有一下三种情况值得注意:
①不迭代values,直接输出kv-----输出的key是分组中第一个kv键值对的key
②迭代values,输出kv-----------此时的key会随着value的变化而变化,与之对应
③迭代完后输出kv---------------输出的是排序中的最后一个kv键值对
七、MapTask的并行度机制
1、概念:是指map阶段有多少个并行的task共同处理任务
2、逻辑规划:MapTask并行度的决定机制;在job提交作业到集群之前在客户端就已经完成了;由读取数据的组件FileInputFormat.getSplits()完成;逻辑规划完毕之后,会生成一个逻辑规划文件------job.split,该文件会随着job的提交而提交到准备区当中,由后续程序读取执行;根据该规划文件(job.split)决定启动多少个maptask(默认是一个逻辑切片对应一个maptask)
3、逻辑规划规则:
●逐个遍历待处理目录下的文件,以切片大小作为规划
●默认情况下,切片的大小=块的大小,即默认情况下切片大小为128MB
👆一个数据块对应一个切片文件,对应一个maptask处理
而如果剩余的文件的大小<splitSize*1.1,则剩余文件全部组成一个切片(用于防止空间的浪费)
八、ReduceTask并行度机制
reducetask的数量的决定是可以手动设置的(之前的分区设置)--------根据业务设置(对于结果文件的要求)
而如果数据分布不均匀,有可能在reduce阶段产生数据倾斜(资源分配不均)