序号 | 命令 | 命令解释 |
1 | top | 查看内存 |
2 | df-h | 查看磁盘存储情况 |
3 | iotop | 查看磁盘IO读写(yum intsall iotop安装) |
4 | iotop -o | 直接查看比较高的磁盘读写程序 |
5 | netstat -tunlp | grep 端口号 | 查看端口占用情况 |
6 | uptime | 查看报告系统运行时长及平均负载 |
7 | ps aux | 查看进程 |
awk、 sed、 cut、 sort
➢ dfs.namenode.http-address: 50070
➢ dfs.datanode.http-address: 50075
➢ SecondaryNameNode辅助名称节点端口号: 50090
➢ dfs.datanode.address:50010➢ fs.defaultFS:8020 或者9000
➢ yarn.resourcemanager.webapp.address:8088
➢ 历史服务器web 访问端口:19888
(1)配置文件:
core-site.xml、hdfs-site.xml、mapred-site.xml、yarn-site.xml
hadoop-env.sh、yarn-env.sh、mapred-env.sh、slaves(2)简单的集群搭建过程:
JDK 安装
配置SSH 免密登录
配置hadoop 核心文件:
格式化namenode
1. 客户端通过Distributed FileSystem向NameNode请求下载文件,NameNode通过查询元数据,找到文件块所在的DataNode地址。
2. 挑选一台DataNode(就近原则,然后随机)服务器,请求读取数据。
3. DataNode开始传输数据给客户端(从磁盘里面读取数据输入流,以Packet为单位来做校验)。
4. 客户端以Packet为单位接收,先在本地缓存,然后写入目标文件。
1. 客户端通过Distributed FileSystem模块向NameNode请求上传文件,NameNode检查目标文件是否已存在,父目录是否存在。
2. NameNode返回是否可以上传。
3. 客户端请求第一个 Block上传到哪几个DataNode服务器上。
4. NameNode返回3个DataNode节点,分别为dn1、dn2、dn3。
5. 客户端通过FSDataOutputStream模块请求dn1上传数据,dn1收到请求会继续调用dn2,然后dn2调用dn3,将这个通信管道建立完成。
6. dn1、dn2、dn3逐级应答客户端。
7. 客户端开始往dn1上传第一个Block(先从磁盘读取数据放到一个本地内存缓存),以Packet为单位,dn1收到一个Packet就会传给dn2,dn2传给dn3;dn1每传一个packet会放入一个确认队列等待确认。
8. 当一个Block传输完成之后,客户端再次请求NameNode上传第二个Block的服务器。(重复执行3-7步)。
@Test
public void testUploadPacket() throws IOException {
//1 准备读取本地文件的输入流
final FileInputStream in = new FileInputStream(newFile("e:/lagou.txt"));
//2 准备好写出数据到hdfs的输出流
final FSDataOutputStream out = fs.create(new Path("/lagou.txt"), new Progressable() {
public void progress() {
//这个progress方法就是每传输64KB(packet)就会执行一次,
System.out.println("&");
}
});
//3 实现流拷贝
IOUtils.copyBytes(in, out, configuration); //默认关闭流选项是true,所以会自动关闭
//4 关流 可以再次关闭也可以不关了
}
package com.job.hdfs;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.*;
import org.apache.hadoop.fs.permission.FsPermission;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.util.Progressable;
import org.apache.hadoop.yarn.webapp.hamlet.Hamlet;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.*;
import java.net.URI;
import java.net.URISyntaxException;
public class HdfsClientDemo {
FileSystem fs = null;
Configuration configuration = null;
@Before
public void init() throws URISyntaxException, IOException, InterruptedException {
//1 获取Hadoop 集群的configuration对象
configuration = new Configuration();
// configuration.set("fs.defaultFS", "hdfs://linux121:9000");
// configuration.set("dfs.replication", "2");
//2 根据configuration获取Filesystem对象
fs = FileSystem.get(new URI("hdfs://linux121:9000"), configuration, "root");
}
@After
public void destory() throws IOException {
//4 释放FileSystem对象(类似数据库连接)
fs.close();
}
@Test
public void testMkdirs() throws URISyntaxException, IOException, InterruptedException{
// FileSystem fs = FileSystem.get(configuration);
//3 使用FileSystem对象创建一个测试目录
fs.mkdirs(new Path("/api_test2"));
}
//上传文件
@Test
public void copyFromLocalToHdfs() throws URISyntaxException, IOException, InterruptedException {
//上传文件
//src:源文件目录:本地路径
//dst:目标文件目录,hdfs路径
fs.copyFromLocalFile(new Path("e:/lagou.txt"), new Path("/lagou.txt"));
//上传文件到hdfs默认是3个副本,
//如何改变上传文件的副本数量?
//1 configuration对象中指定新的副本数量
}
//下载文件
@Test
public void copyFromHdfsToLocal() throws URISyntaxException, IOException, InterruptedException {
// boolean:是否删除源文件
//src:hdfs路径
//dst:目标路径,本地路径
fs.copyToLocalFile(true, new Path("/lagou.txt"), new
Path("e:/lagou_copy.txt"));
}
//删除文件或者文件夹
@Test
public void deleteFile() throws URISyntaxException, IOException, InterruptedException {
fs.delete(new Path("/api_test2"), true);
}
//遍历hdfs的根目录得到文件以及文件夹的信息:名称,权限,长度等
@Test
public void listFiles() throws URISyntaxException, IOException, InterruptedException {
//得到一个迭代器:装有指定目录下所有文件信息
RemoteIterator remoteIterator = fs.listFiles(newPath("/"), true);
//遍历迭代器
while (remoteIterator.hasNext()) {
LocatedFileStatus fileStatus = remoteIterator.next();
//文件名称
final String fileName = fileStatus.getPath().getName();
//长度
final long len = fileStatus.getLen();
//权限
final FsPermission permission = fileStatus.getPermission();
//分组
final String group = fileStatus.getGroup();
//用户
final String owner = fileStatus.getOwner();
System.out.println(fileName + "\t" + len + "\t" + permission + "\t" + group + "\t" + owner);
//块信息
final BlockLocation[] blockLocations =
fileStatus.getBlockLocations();
for (BlockLocation blockLocation : blockLocations) {
final String[] hosts = blockLocation.getHosts();
for (String host : hosts) {
System.out.println("主机名称" + host);
}
}
System.out.println("---------------------------------");
}
}
//文件以及文件夹判断
@Test
public void isFile() throws URISyntaxException, IOException, InterruptedException {
final FileStatus[] fileStatuses = fs.listStatus(new Path("/"));
for (FileStatus fileStatus : fileStatuses) {
final boolean flag = fileStatus.isFile();
if (flag) {
System.out.println("文件:" + fileStatus.getPath().getName());
} else {
System.out.println("文件夹:" + fileStatus.getPath().getName());
}
}
}
//使用IO流操作HDFS
//上传文件:准备输入流读取本地文件,使用hdfs的输出流写数据到hdfs
@Test
public void uploadFileIO() throws IOException {
//1. 读取本地文件的输入流
final FileInputStream inputStream = new FileInputStream(new File("e:/lagou.txt"));
//2. 准备写数据到hdfs的输出流
final FSDataOutputStream outputStream = fs.create(new Path("/lagou.txt"));
// 3.输入流数据拷贝到输出流 :数组的大小,以及是否关闭流底层有默认值
IOUtils.copyBytes(inputStream, outputStream, configuration);
//4.可以再次关闭流
IOUtils.closeStream(outputStream);
IOUtils.closeStream(inputStream);
}
//下载文件
@Test
public void downLoadFileIO() throws IOException {
//1. 读取hdfs文件的输入流
final FSDataInputStream in = fs.open(new Path("/lagou.txt"));
//2. 本地文件的输出流
final FileOutputStream out = new FileOutputStream(new File("e:/lagou_io_copy.txt"));
//3. 流的拷贝
IOUtils.copyBytes(in, out, configuration);
//4.可以再次关闭流
IOUtils.closeStream(out);
IOUtils.closeStream(in);
}
//seek定位读取hdfs指定文件 :使用io流读取/lagou.txt文件并把内容输出两次,本质就是读取文件内容两次并输出
@Test
public void seekReadFile() throws IOException {
//1 创建一个读取hdfs文件的输入流
final FSDataInputStream in = fs.open(new Path("/lagou.txt"));
//2.控制台数据:System.out
//3 实现流拷贝,输入流--》控制台输出
// IOUtils.copyBytes(in, System.out, configuration);
IOUtils.copyBytes(in, System.out, 4096, false);
//4. 再次读取文件
in.seek(0); //定位从0偏移量(文件头部)再次读取
IOUtils.copyBytes(in, System.out, 4096, false);
//5.关闭输入流
IOUtils.closeStream(in);
}
}
一、Shuffle 机制
1)Map 方法之后Reduce 方法之前这段处理过程叫Shuffle
2)Map 方法之后,数据首先进入到分区方法,把数据标记好分区,然后把数据发送到环形缓冲区;环形缓冲区默认大小100m,环形缓冲区达到80%时,进行溢写;溢写前对数据进行排序,排序按照对key 的索引进行字典顺序排序,排序的手段快排;溢写产生大量溢写文件,需要对溢写文件进行归并排序;对溢写的文件也可以进行Combiner 操作,前提是汇总操作,求平均值不行。最后将文件按照分区存储到磁盘,等待Reduce 端拉取。
3)每个Reduce 拉取Map 端对应分区的数据。拉取数据后先存储到内存中,内存不够了,再存储到磁盘。拉取完所有数据后,采用归并排序将内存和磁盘中的数据都进行排序。在进入Reduce 方法前,可以对数据进行分组操作。
二、Hadoop 优化
0)HDFS 小文件影响
(1)影响NameNode 的寿命,因为文件元数据存储在NameNode 的内存中
(2)影响计算引擎的任务数量,比如每个小的文件都会生成一个Map 任务
1)数据输入小文件处理:
(1)合并小文件:对小文件进行归档(Har)、自定义Inputformat 将小文件存储成SequenceFile 文件。
(2)采用ConbinFileInputFormat 来作为输入,解决输入端大量小文件场景。
(3)对于大量小文件Job,可以开启JVM 重用。2) Map阶段
(1)增大 环形缓冲 区 大小。由 100m扩大到 200m
(2)增大 环形缓冲区溢写的比例。 由 80%扩大到 90%
(3)减少 对溢写文件的 merge次数 。 10个 文件,一次 20个 merge
(4)不影响 实际业务的前提下, 采用 Combiner提前合并 ,减少 I/O。3)Reduce阶段
(1)合理设置 Map和 Reduce数:两个都不能设置太少,也不能设置太多。太少,会导致 Task等待,延长处理时间;太多,会导致 Map、 Reduce任务间竞争资源,造成处理超时等错误。
(2)设置 Map、 Reduce共存:调整 slowstart.completedmaps参数,使 Map运行到一定程度后, Reduce也开始运行,减少 Reduce的等待时间。
(3)规避使用 Reduce,因为 Reduce在用于连接数据集的时候将会产生大量的网络消耗。
(4)增加每个 Reduce去 Map中拿数据的并行数
(5)集群 性能可以的前提下,增大 Reduce端存储数据内存的大小。4)IO传输
(1)采用数据压缩的方式,减少网络 IO的 的时间 。安装 Snappy和 LZOP压缩编码器 。
(2)使用 SequenceFile二进制文件5)整体
(1)MapTask默认 内存大小为 1G,可以 增加 MapTask内存 大小 为 4-5g
(2)ReduceTask默认 内存大小为 1G,可以 增加 ReduceTask内存 大小 为 4-5g
(3)可以 增加 MapTask的 cpu核数 增加 ReduceTask的 CPU核数
(4)增加每个 Container的 CPU核数和内存大小
(5)调整每个 Map Task和 Reduce Task最大重试次数
三、压缩
压缩格式 | Hadoop自带? | 算法 | 文件扩展名 | 支持切分 | 换成压缩格式后,原来的程序是否需要修改 |
DEFLATE | 是,直接使用 | DEFLATE | .deflate | 否 | 和文本处理一样,不需要修改 |
Gzip | 是,直接使用 | DEFLATE | .gz | 否 | 和文本处理一样,不需要修改 |
bzip2 | 是,直接使用 | bzip2 | .bz2 | 是 | 和文本处理一样,不需要修改 |
LZO | 否,需要安装 | LZO | .lzo | 是 | 需要建索引,还需要指定输入格式 |
Snappy | 否,需要安装 | Snappy | .snappy | 否 | 和文本处理一样,不需要修改 |
提示:如果面试过程问起,我们一般回答压缩方式为Snappy,特点速度快,缺点无法切分(可以回答在链式MR 中,Reduce 端输出使用bzip2 压缩,以便后续的map 任务对数据进行split)
四、切片机制
1)简单地按照文件的内容长度进行切片
2)切片大小,默认等于Block 大小
3)切片时不考虑数据集整体,而是逐个针对每一个文件单独切片
提示:切片大小公式:max(0,min(Long_max,blockSize))
作业提交过程之YARN
作业提交
第1步:Client调用job.waitForCompletion方法,向整个集群提交MapReduce作业。
第2步:Client向RM申请一个作业id。
第3步:RM给Client返回该job资源的提交路径和作业id。
第4步:Client提交jar包、切片信息和配置文件到指定的资源提交路径。
第5步:Client提交完资源后,向RM申请运行MrAppMaster。
作业初始化
第6步:当RM收到Client的请求后,将该job添加到容量调度器中。
第7步:某一个空闲的NM领取到该Job。
第8步:该NM创建Container,并产生MRAppmaster。
第9步:下载Client提交的资源到本地。
任务分配
第10步:MrAppMaster向RM申请运行多个MapTask任务资源。
第11步:RM将运行MapTask任务分配给另外两个NodeManager,另两个NodeManager分别领取任务并创建容器。
任务运行
第12步:MR向两个接收到任务的NodeManager发送程序启动脚本,这两个NodeManager分别启动MapTask,MapTask对数据分区排序。
第13步:MrAppMaster等待所有MapTask运行完毕后,向RM申请容器,运行ReduceTask。
第14步:ReduceTask向MapTask获取相应分区的数据。
第15步:程序运行完毕后,MR会向RM申请注销自己。
进度和状态更新
YARN中的任务将其进度和状态返回给应用管理器, 客户端每秒(通过mapreduce.client.progressmonitor.pollinterval设置)向应用管理器请求进度更新, 展示给用户。
作业完成
除了向应用管理器请求作业进度外, 客户端每5秒都会通过调用waitForCompletion()来检查作业是否完成。时间间隔可以通过mapreduce.client.completion.pollinterval来设置。作业完成之后, 应用管理器和Container会清理工作状态。作业的信息会被作业历史服务器存储以备之后用户核查。
1)Hadoop 调度器重要分为三类:
FIFO 、Capacity Scheduler(容量调度器)和Fair Sceduler(公平调度器)。
Hadoop2.7.2 默认的资源调度器是 容量调度器
2)区别:
FIFO 调度器:先进先出,同一时间队列中只有一个任务在执行。
容量调度器:多队列;每个队列内部先进先出,同一时间队列中只有一个任务在执行。队列的并行度为队列的个数。
公平调度器:多队列;每个队列内部按照缺额大小分配资源启动任务,同一时间队列中有多个任务执行。队列的并行度大于等于队列的个数。
3)一定要强调生产环境中不是使用的FifoScheduler,面试的时侯会发现候选人大概了解这几种调度器的区别,但是问在生产环境用哪种,却说使用的FifoScheduler(企业生产环境一定不会用这个调度的)
Hadoop 默认不支持LZO 压缩,如果需要支持LZO 压缩,需要添加jar 包,并在hadoop的cores-site.xml 文件中添加相关压缩配置。
1)在hdfs-site.xml 文件中配置多目录,最好提前配置好,否则更改目录需要重新启动集群
2)NameNode 有一个工作线程池,用来处理不同DataNode 的并发心跳以及客户端并发的元数据操作。dfs.namenode.handler.count=20 * log2(Cluster Size),比如集群规模为10 台时,此参数设置为60
3)编辑日志存储路径dfs.namenode.edits.dir 设置与镜像文件存储路径dfs.namenode.name.dir 尽量分开,达到最低写入延迟4)服务器节点上YARN 可使用的物理内存总量,默认是8192(MB),注意,如果你的节点内存资源不够8GB,则需要调减小这个值,而YARN 不会智能的探测节点的物理内存总量。yarn.nodemanager.resource.memory-mb
5)单个任务可申请的最多物理内存量,(默认是 8192 MB)。 yarn.scheduler.maximum-allocation-mb
搭建完 Hadoop集群后需要对 HDFS读写性能和 MR计算能力 测试。测试 jar包在 hadoop的 share文件夹下。
1)如果 M R 造成系统宕机。此时要控制 Y arn 同时运行的任务数,和每个任务申请的最大内存。调整 参数: yarn.scheduler.maximum allocation mb 单个任务可申请的最多物理内存量,默认是 8192MB
2)如果写入文件过量造成 NameNode 宕机。那么调高 K afka 的存储大小,控制从 Kafka到 HDFS 的写入速度。高峰期的时候用 K afka 进行缓存,高峰期过去数据同步会自动跟上。
1)提前在 map进行 combine,减少传输的数据量
在 Mapper加上 combiner相当于提前进行 reduce,即把一个 Mapper中的相同 key进行了聚合,减少 shuffle过程中传输的数据量,以及 Reducer端的计算量。如果导致数据倾斜的 key 大量分布在不同的 mapper的时候,这种方法就不是很有效了。2)导致数据倾斜的 key 大量分布在不同的 mapper
(1)局部聚合加全局聚合。
第一次在 map阶段对那些导致了数据倾斜的 key 加上 1到 n的随机前缀,这样本来相同的 key 也会被分到多个 Reducer中进行局部聚合,数量就会大大降低。
第二次 mapreduce,去掉 key的随机前缀,进行全局聚合。
思想:二次 mr,第一次将 key随机散列到不同 reducer进行处理达到负载均衡目的。第二次再根据去掉 key的随机前缀,按原 key进行 reduce处理。这个方法进行两次 mapreduce,性能稍差。(2)增加 Reducer,提升并行度
JobConf.setNumReduceTasks(int)
(3)实现 自定义分区根据数据分布情况,自定义散列函数,将 key均匀分配到不同 Reducer
mapTask数量
splitSize=max{minSize,min{maxSize,blockSize}}
map数量由处理的数据分成的block数量决定default_num = total_size / split_size;
reduceTask数量
reduce的数量 job.setNumReduceTasks(x);
x为reduce的数量。不设置的话默认为1。
1、有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M。返回频数最高的100 个词。
方案:
Step1:顺序读文件中,对于每个词x,取hash(x)%5000,然后按照该值存到5000个小文件(记为f0,f1,...,f4999)中,这样每个文件大概是200k左右,如果其中的有的文件超过了1M大小,还可以按照类似的方法继续往下分,直到分解得到的小文件的大小都不超过1M;
Step2:对每个小文件,统计每个文件中出现的词以及相应的频率(可以采用trie树/hash_map等),并取出出现频率最大的100个词(可以用含100个结点的最小堆),并把100词及相应的频率存入文件,这样又得到了5000个文件;
Step3:把这5000个文件进行归并(类似与归并排序);
2、给定 a、b 两个文件,各存放 50 亿个 url,每个 url 各占 64字节,内存限制是 4G,让你找出 a、b 文件共同的 url?
假如每个url大小为10bytes,那么可以估计每个文件的大小为50G×64=320G,远远大于内存限制的4G,所以不可能将其完全加载到内存中处理,可以采用分治的思想来解决。
Step1:遍历文件a,对每个url求取hash(url)%1000,然后根据所取得的值将url分别存储到1000个小文件(记为a0,a1,...,a999,每个小文件约300M);
Step2:遍历文件b,采取和a相同的方式将url分别存储到1000个小文件(记为b0,b1,...,b999);巧妙之处:这样处理后,所有可能相同的url都被保存在对应的小文件(a0vsb0,a1vsb1,...,a999vsb999)中,不对应的小文件不可能有相同的url。然后我们只要求出这个1000对小文件中相同的url即可。
Step3:求每对小文件ai和bi中相同的url时,可以把ai的url存储到hash_set/hash_map中。然后遍历bi的每个url,看其是否在刚才构建的hash_set中,如果是,那么就是共同的url,存到文件里面就可以了。
3、现有海量日志数据保存在一个超级大的文件中,该文件无法直接读入内存,要求从中提取某天出访问百度次数最多的那个IP?
Step1:从这一天的日志数据中把访问百度的IP取出来,逐个写入到一个大文件中;
Step2:注意到IP是32位的,最多有2^32个IP。同样可以采用映射的方法,比如模1000,把整个大文件映射为1000个小文件;
Step3:找出每个小文中出现频率最大的IP(可以采用hash_map进行频率统计,然后再找出频率最大的几个)及相应的频率;
Step4:在这1000个最大的IP中,找出那个频率最大的IP,即为所求。
半数 机制 2n+1
10台 服务器: 3台
20台 服务器: 5台
100台 服务器: 11台
台数并不是越多越好。 太多 选举时间过长影响性能。
ls、 get、 create
Taildir Source 断点续传、多目录 。 Flume1.6以前需要自己自定义Source记录每次读取文件位置,实现断点续传。
File Channel 数据 存储在磁盘,宕机数据可以保存。但是传输 速率 慢。 适合对 数据传输可靠性要求高的场景 比如 金融行业。
Memory Channel 数据 存储在内存中,宕机数据丢失。传输速率快 。适合 对数据传输可靠性要求不高的场景,比如,普通的日志数据。
Kafka Channel 减少 了 Flume的 Sink阶段, 提高了传输效率 。
Source到 Channel是 Put事务
Channel到 Sink是 Take事务
可以使用这种source-tailDir,它会监控一个目录下的新增的文件和文件新增的内容
Taildir Source 维护了一个 json 格式的 position File,其会定期的往 position File 中更新每个文件读取到的最新的位置,因此能够实现断点续传。
首先介绍下flume中的channel是位于 source 和 sink 之间的缓冲区。
flume 自带两种缓冲区,file channel 和 memory channel。
(1)file channel:硬盘缓冲区,性能低,但是安全。系统宕机也不会丢失数据。
(2)memory channel:内存缓冲区,性能高,但是有可能丢数据,在不关心数据有可能丢失的情况下使用。
put事务流程: 实现将源数据写入到管道channel
(1)doPut:把数据写入临时缓冲区 putList 。
(2)doCommit:检查channel是否有足够的缓冲区,有则合并到channel的队列
(3)doRollBack:如果 channel 不行,我们就回滚数据。
take事务流程:实现从管道channel中取数据到sink中
(1)doTake:先将数据取到临时缓冲区takeList。
(2)doCommit:如果数据全部发送成功,就清除临时缓冲区。
(3)doRollBack:如果数据发送过程中出现异常,doRollBack将临时缓冲区的数据还给channel 队列
(1)拦截器注意事项
项目 中 自定义了 ETL拦截器和区分类型拦截器。
采用两个拦截器的优缺点:优点,模块化开发和可移植性;缺点,性能会低一些
(2)自定义 拦截器步骤
a)实现 Interceptor
b)重写四个方法
➢ initialize 初始化
➢ public Event intercept(Event event) 处理单个Event
➢ public List
➢ close 方法
c)静态内部类,实现Interceptor.Builder
(3)作用
Source 将 Event 写入到 Channel 之前可以使用拦截器对Event 进行各种形式的处理,Source 和 Channel 之间可以有多个拦截器,不同拦截器使用不同的规则处理 Event,包括时间、主机、UUID、正则表达式等多种形式的拦截器。还可以通过实现自定义拦截器来实现特定的需求。
Channel Selectors,可以让不同的项目日志通过不同的Channel到不同的Sink中去。官方文档上Channel Selectors 有两种类型:Replicating Channel Selector (default)和Multiplexing Channel Selector
这两种Selector的区别是:Replicating 会将source过来的events发往所有channel,而Multiplexing可以选择该发往哪些Channel。
Ganglia
不会,Channel 存储可以存储在File 中,数据传输自身有事务。
开发中在flume-env.sh 中设置JVM heap 为4G 或更高,部署在单独的服务器上(4 核 8线程16G 内存)
-Xmx 与-Xms 最好设置一致,减少内存抖动带来的性能影响,如果设置不一致容易导致频繁fullgc。
通过配置dataDirs指向多个路径,每个路径对应不同的硬盘,增大 Flume吞吐量。官方说明如下:
Comma separated list of directories for storing log files. Using multiple directories on separate disks can improve file channel peformance
checkpointDir和 backupCheckpointDir也尽量配置在不同硬盘对应的目录中,保证checkpoint坏掉后,可以快速使用 backupCheckpointDir恢复数据
(1)Source
(1)增加 source 个数,可以增大 source 读取能力:具体做法:如果一个目录下生成的文件过多,可以将它拆分成多个目录。每个目录都配置一个 source 。
(2)增大 batchSize:可以增大一次性批处理的 event 条数,适当调大这个参数,可以调高 source 搬运数据到 channel 的性能。
(2)Channel
(1)memory :性能好,但是,如果发生意外,可能丢失数据。
(2)使用 file channel 时,dataDirs 配置多个不同盘下的目录可以提高性能。
(3)transactionCapacity 需要大于 source 和 sink 的batchSize 参数。
(3)sink
(1) 增加Sink的个数可以增加Sink消费event的能力。Sink也不是越多越好够用就行,过多的Sink会占用系统资源,造成系统资源不必要的浪费。
(2) batchSize参数决定Sink一次批量从Channel读取的event条数,适当调大这个参数可以提高Sink从Channel搬出event的性能。
Flume采集日志是通过流的方式直接将日志收集到存储层,而kafka是将日志缓存在kafka集群,待后期可以采集到存储层。
Flume采集中间停了,可以采用文件的方式记录之前的日志,而kafka是采用offset的方式记录之前的日志。
(1) Flume的channel分为很多种,可以将数据写入到文件。
(2) 防止非首个agent宕机的方法数可以做集群或者主备
(1)HDFS存入大量小文件,有什么影响?
元数据层面:每个小文件都有一份元数据,其中包括文件路径,文件名,所有者,所属组,权限,创建时间等,这些信息都保存在 Namenode内存中。所以小文件过多,会占用Namenode服务器大量内存,影响 Namenode性能和使用寿命
计算层面:默认情况下 MR会对每个小文件启用一个 Map任务计算,非常影响计算性能。同时也影响磁盘寻址时间。(2)HDFS小文件处理
官方默认的这三个参数配置写入HDFS后会产生小文件, hdfs.rollInterval、 hdfs.rollSize、hdfs.rollCount
基于以上hdfs.rollInterval=3600 hdfs.rollSize=134217728 hdfs.rollCount =0hdfs.roundValue=3600 hdfs.roundUnit= second几个参数 综合作用,效果如下:
1)tmp文件在达到 128M时会滚动生成正式文件
2)tmp文件创建超 3600秒 时会滚动生成正式文件
举例:在2018-01-01 05:23的时侯 sink接收到数据,那会产生如下 tmp文件:/atguigu/20180101/atguigu.201801010520.tmp 即使文件内容没有达到128M,也会在 06:23时滚动生成正式文件File channel 容量 1000000条
Memory channel 容量 100条
Kafka 官方自带压力测试脚本(kafka-consumer-perf-test.sh、kafka-producer-perf-test.sh)。Kafka 压测时,可以查看到哪个地方出现了瓶颈(CPU,内存,网络IO)。一般都是网络IO达到瓶颈。
Kafka 机器数量=2*(峰值生产速度*副本数/100)+1
7 天
每天的数据量*7 天/70%
公司自己开发的监控器;
开源的监控器:KafkaManager、KafkaMonitor、kafkaeagle
分区数并不是越多越好,一般分区数不要超过集群机器数量。分区数越多占用内存越大ISR等),一个节点集中的分区也就越多,当它宕机的时候,对系统的影响也就越大。
分区数一般设置为: 3-10个
一般我们设置成 2个或 3个, 很多企业设置为 2个 。
通常情况:多少个日志类型就多少个 Topic。 也有对日志类型进行合并的。
Ack=0,相当于异步发送,消息发送完毕即 offset增加,继续生产。
Ack=1,leader收到 leader replica 对一个消息的接受 ack才增加 offset,然后继续生产。
Ack=-1,leader收到所有 replica 对一个消息的接受 ack才增加 offset,然后继续生产。
ISR(In-Sync Replicas),副本同步队列。 ISR中包括 Leader和 Follower。如果 Leader进程挂掉,会在 ISR队列中选择一个服务作为新的 Leader。有 replica.lag.max.messages(延迟条数)和 replica.lag.time.max.ms(延迟时间)两个参数决定一台服务是否可以加入 ISR副本队列,在 0.10版本移除了 replica.lag.max.messages参数,防止服务频繁的进去队列。任意一个维度超过阈值都会把
Follower剔除出 ISR,存入 OSR Outof-Sync Replicas列表,新加入的 Follower也会先存放在 OSR中。
consumer group靠coordinator实现了Rebalance
这里有三种rebalance的策略:range、round-robin、sticky
比如我们消费的一个主题有12个分区:p0,p1,p2,p3,p4,p5,p6,p7,p8,p9,p10,p11 假设我们的消费者组里面有三个消费者
range策略 range策略就是按照partiton的序号范围 p0~3 consumer1 p4~7 consumer2 p8~11 consumer3 默认就是这个策略;
round-robin策略 就是轮询分配 consumer1:0,3,6,9 consumer2:1,4,7,10 consumer3:2,5,8,11 但是前面的这两个方案有个问题:12 -> 2 每个消费者会消费6个分区
假设consuemr1挂了:p0-5分配给consumer2,p6-11分配给consumer3 这样的话,原本在consumer2上的的p6,p7分区就被分配到了 consumer3上。
3. sticky策略 最新的一个sticky策略,就是说尽可能保证在rebalance的时候,让原本属于这个consumer 的分区还是属于他们,然后把多余的分区再均匀分配过去,这样尽可能维持原来的分区分配的策略
consumer1:0-3 consumer2: 4-7 consumer3: 8-11 假设consumer3挂了 consumer1:0-3,+8,9 consumer2: 4-7,+10,11
如何提升吞吐量:
参数一:
buffer.memory
:设置发送消息的缓冲区,默认值是33554432,就是32MB参数二:
compression.type
:默认是none,不压缩,但是也可以使用lz4压缩,效率还是不错的,压缩之后可以减小数据量,提升吞吐量,但是会加大producer端的cpu开销参数三:
batch.size
:设置batch的大小,如果batch太小,会导致频繁网络请求,吞吐量下降;如果batch太大,会导致一条消息需要等待很久才能被发送出去,而且会让内存缓冲区有很大压力,过多数据缓冲在内存里,默认值是:16384,就是16kb,也就是一个batch满了16kb就发送出去,一般在实际生产环境,这个batch的值可以增大一些来提升吞吐量,如果一个批次设置大了,会有延迟。一般根据一条消息大小来设置。如果我们消息比较少。配合使用的参数linger.ms,这个值默认是0,意思就是消息必须立即被发送,但是这是不对的,一般设置一个100毫秒之类的,这样的话就是说,这个消息被发送出去后进入一个batch,如果100毫秒内,这个batch满了16kb,自然就会发送出去。
LeaderNotAvailableException:这个就是如果某台机器挂了,此时leader副本不可用,会导致你写入失败,要等待其他follower副本切换为leader副本之后,才能继续写入,此时可以重试发送即可;如果说你平时重启kafka的broker进程,肯定会导致leader切换,一定会导致你写入报错,是LeaderNotAvailableException。
NotControllerException:这个也是同理,如果说Controller所在Broker挂了,那么此时会有问题,需要等待Controller重新选举,此时也是一样就是重试即可。
NetworkException:网络异常 timeout a. 配置retries参数,他会自动重试的 b. 但是如果重试几次之后还是不行,就会提供Exception给我们来处理了,我们获取到异常以后,再对这个消息进行单独处理。我们会有备用的链路。发送不成功的消息发送到Redis或者写到文件系统中,甚至是丢弃。
重试会带来一些问题:
- 消息会重复有的时候一些leader切换之类的问题,需要进行重试,设置retries即可,但是消息重试会导致,重复发送的问题,比如说网络抖动一下导致他以为没成功,就重试了,其实人家都成功了.
- 消息乱序消息重试是可能导致消息的乱序的,因为可能排在你后面的消息都发送出去了。所以可以使用"max.in.flight.requests.per.connection"参数设置为1, 这样可以保证producer同一时间只能发送一条消息。两次重试的间隔默认是100毫秒,用"retry.backoff.ms"来进行设置 基本上在开发过程中,靠重试机制基本就可以搞定95%的异常问题。
分区:
1、没有设置key 我们的消息就会被轮训的发送到不同的分区。
2、设置了key kafka自带的分区器,会根据key计算出来一个hash值,这个hash值会对应某一个分区。如果key相同的,那么hash值必然相同,key相同的值,必然是会被发送到同一个分区。但是有些比较特殊的时候,我们就需要自定义分区
每个consumer内存里数据结构保存对每个topic的每个分区的消费offset,定期会提交offset,老版本是写入zk,但是那样高并发请求zk是不合理的架构设计,zk是做分布式系统的协调的,轻量级的元数据存储,不能负责高并发读写,作为数据存储。
现在新的版本提交offset发送给kafka内部topic:__consumer_offsets,提交过去的时候, key是group.id+topic+分区号,value就是当前offset的值,每隔一段时间,kafka内部会对这个topic进行compact(合并),也就是每个group.id+topic+分区号就保留最新数据。
__consumer_offsets可能会接收高并发的请求,所以默认分区50个(leader partitiron -> 50 kafka),这样如果你的kafka部署了一个大的集群,比如有50台机器,就可以用50台机器来抗offset提交的请求压力. 消费者 -> broker端的数据 message -> 磁盘 -> offset 顺序递增 从哪儿开始消费?-> offset 消费者(offset)
web页面管理的一个管理软件(kafka Manager) 修改bin/kafka-run-class.sh脚本,第一行增加JMX_PORT=9988 重启kafka进程
另一个软件:主要监控的consumer的偏移量。就是一个jar包 java -cp KafkaOffsetMonitor-assembly-0.3.0-SNAPSHOT.jar com.quantifind.kafka.offsetapp.OffsetGetterWeb –offsetStorage kafka \(根据版本:偏移量存在kafka就填kafka,存在zookeeper就填zookeeper) –zk hadoop1:2181 –port 9004 –refresh 15.seconds –retain 2.days。
heartbeat.interval.ms:consumer心跳时间间隔,必须得与coordinator保持心跳才能知道consumer是否故障了, 然后如果故障之后,就会通过心跳下发rebalance的指令给其他的consumer通知他们进行rebalance的操作
session.timeout.ms:kafka多长时间感知不到一个consumer就认为他故障了,默认是10秒
max.poll.interval.ms:如果在两次poll操作之间,超过了这个时间,那么就会认为这个consume处理能力太弱了,会被踢出消费组,分区分配给别人去消费,一般来说结合业务处理的性能来设置就可以了。
fetch.max.bytes:获取一条消息最大的字节数,一般建议设置大一些,默认是1M 其实我们在之前多个地方都见到过这个类似的参数,意思就是说一条信息最大能多大?
Producer 发送的数据,一条消息最大多大, -> 10M
Broker 存储数据,一条消息最大能接受多大 -> 10M
Consumer max.poll.records: 一次poll返回消息的最大条数,默认是500条
connection.max.idle.ms:consumer跟broker的socket连接如果空闲超过了一定的时间,此时就会自动回收连接,但是下次消费就要重新建立socket连接,这个建议设置为-1,不要去回收
enable.auto.commit: 开启自动提交偏移量
auto.commit.interval.ms: 每隔多久提交一次偏移量,默认值5000毫秒
_consumer_offset auto.offset.reset:earliest 当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,从头开始消费 topica -> partition0:1000 partitino1:2000 latest 当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,消费新产生的该分区下的数据 none topic各分区都存在已提交的offset时,从offset后开始消费;只要有一个分区不存在已提交的offset,则抛出异常
LEO:在kafka里面,无论leader partition还是follower partition统一都称作副本(replica)。
每次partition接收到一条消息,都会更新自己的LEO,也就是log end offset,LEO其实就是最新的offset + 1
HW:高水位 LEO有一个很重要的功能就是更新HW,如果follower和leader的LEO同步了,此时HW就可以更新 HW之前的数据对消费者是可见,消息属于commit状态。HW之后的消息消费者消费不到。
每天总数据量 100g 每天 产生 1亿 条 日志 10000万 /24/60/60=1150条 /每 秒钟
平均每秒钟 1150条
低谷每 秒 钟: 50条
高峰每秒钟 1150条 * (2-20倍)= 2300条 -23000条
每条日志 大小 0.5k-2k
每秒多少数据量: 2.3M-20MB
1)Flume记录
2)日志有记录
3)短期 没事
1)如果是 Kafka消费能力不足,则可以考虑增加 Topic的分区数,并且同时提升消费组的消费者数量,消费者数 = 分区数。(两者缺一不可)
2)如果是下游的数据处理不及时: 提高每批次拉取的数量。批次拉取数据过少(拉取数据 /处理时间 <生产速度),使处理的数据小于生产的数据,也会造成数据积压。
Producer的幂等性指的是当发送同一条消息时,数据在 Server端只会被持久化一次,数据不丟不重,但是这里的幂等性是有条件的:
1)只能保证 Producer在单个会话内不丟不重,如果 Producer出现意外挂掉再重启是无法保证的 (幂等性情况下,是无法获取之前的状态信息,因此是无法做到跨会话级别的不丢不重) 。
2)幂等性不能跨多个 Topic-Partition 只能保证单个 Partition内的幂等性 ,当涉及多个Topic-Partition时,这中间的状态并没有同步时,这中间的状态并没有同步。
Kafka从 0.11版本开始引入了事务支持。事务可以保证 Kafka在 Exactly Once语义的基础上,生产和消费可以跨分区和会话,要么全部成功,要么全部失败。
1)Producer事务
为了实现跨分区跨会话的事务,需要引入一个全局唯一的Transaction ID,并将 Producer获得的 PID和 Transaction ID绑定。这样当 Producer重启后就可以通过正在进行的 Transaction ID获得原来的 PID。
为了管理Transaction Kafka引入了一个新的组件 Transaction Coordinator。 Producer就是通过和 Transaction Coordinator交互获得 Transaction ID对应的任务状态。 Transaction Coordinator还负责将事务所有写入 Kafka的一个内部 Topic,这样即使整个服务重启,由于事务状态得到保存,进行中的事务状态可以得到恢复,从而继续进行。
2)Consumer事务
上述事务机制主要是从Producer方面考虑,对于 Consumer而言,事务的保证就会相对较弱,尤其时无法保证 Commit的信息被精确消费。这是由于 Consumer可以通过 offset访问任意信息,而且不同的 Segment File生命周期不同,同一事务的消息可能会出现重启后被删除的情况。
幂等性 +ack-1+事务
Kafka数据重复,可以再下一级: SparkStreaming、 redis或者 hive中 dwd层去重去重的手段:分组、按照 id开窗 只取 第一个值;
1、Broker参数配置(server.properties)
1.1 网络和IO操作线程配置优化
# broker处理消息的最大线程的 (默认为3)
num.network.threads=cpu核数 + 1# broker处理磁盘IO的线程数
num.io.threads=cpu核数*2
1.2 log数据文件刷盘策略
# 每当producer写入10000条消息时,刷数据到磁盘
log.flush.interval.messages=10000# 每间隔1秒钟时间,刷数据到磁盘
log.flush.interval.ms=1000
1.3 日志保留策略配置
# 保留三天,也可以更短(log.cleaner.delete.retention.ms)
log.retention.hours=72
1.4 Replica相关配置
# 这个参数指新创建一个topic时,默认的Replica数量,Replica过少会影响数据的可用性,太多则会浪费存储资源,一般建议2~3为宜
offsets.topic.replication.factor:3
2、Producer优化(producer.properties)
# 在producer端用来存放尚未发送出去的Message的缓冲区大小。缓冲区满了之后,可以选择阻塞发送或抛出异常,
# 由block.on.buffer.full的配置来决定
buffer.memory:33554432 (32M)# 默认发送不进行压缩,推荐配置一种适合的压缩算法,可以大幅度的减缓网络压力和Borker的存储压力
compression.type:none
3、Kafka内存调整(kafka-server-start.sh)
# 默认内存1个G,生产环境尽量不要超过6个G
export KAFKA_HEAP_OPTS="-Xms4g -Xmx4g"
1)Kafka本身是分布式集群,同时采用分区技术,并发度高。
2)顺序写磁盘
Kafka的 producer生产数据,要写入到 log文件中,写的过程是一直追加到文件末端,
为顺序写。官网有数据表明,同样的磁盘,顺序写能到 600M/s,而随机写只有 100K/s。这
与磁盘的机械机构有关,顺序写之所以快,是因为其省去了大量磁头寻址的时间。
3)零复制技术
Hive 和数据库除了拥有类似的查询语言,再无类似之处。
1)数据存储位置
Hive 存储在 HDFS 。数据库将数据保存在块设备或者本地文件系统中。
2)数据更新
Hive 中不建议对数据的改写。而数据库中的数据通常是需要经常进行修改的,
3)执行延迟
Hive 执行执行延迟较高。数据库的执行延迟较低。当然,这个是有条件的,即数据规模较小,延迟较高。数据库的执行延迟较低。当然,这个是有条件的,即数据规模较小,当数据规模大到超过数据库的处理能力的时候,当数据规模大到超过数据库的处理能力的时候,Hive的并行计算显然能体现出优势。的并行计算显然能体现出优势。
4)数据规模数据规模
Hive支持很大规模的数据支持很大规模的数据计算计算;数据库可以支持的数据规模较小。;数据库可以支持的数据规模较小。
(1)创建表时:创建内部表时,会将数据移动到数据仓库指向的路径;若创建外部表,仅记录数据所在的路径, 不对数据的位置做任何改变。
(2)删除表时:在删除表的时候,内部表的元数据和数据会被一起删除, 而外部表只删除元数据,不删除数据。这样外部表相对来说更加安全些,数据组织也更加灵活,方便共享源数据。
1 Sort By 分区内 有序;
2 Order By:全局排序,只有一个 Reducer
3 Distrbute By:类似 MR中 Partition,进行分区,结合 sort by使用。
4 Cluster By:当 Distribute by和 Sorts by字段相同时,可以使用 Cluster by方式。 Cluster by除了具有 Distribute by的功能外还兼具 Sort by的功能。但是排序只能是 升序 排序,不能指定排序规则为 ASC或者 DESC。
RANK() 排序相同时会重复,总数不会变
DENSE_RANK() 排序相同时会重复,总数会减少
ROW_NUMBER() 会根据顺序计算
1)OVER():指定分析函数工作的数据窗口大小,这个数据窗口大小可能会随着行的变而变化
2)CURRENT ROW:当前行
3)n PRECEDING:往前 n行数据
4)n FOLLOWING:往后 n行数据
5)UNBOUNDED:起点 UNBOUNDED PRECEDING 表示从前面的起点,UNBOUNDED FOLLOWING表示到后面的终点
6)LAG(col,n):往前第 n行数据
7)LEAD(col,n):往后第:往后第n行数据行数据
8)NTILE(n):把有序分区中的行分发到指定数据的组中,各个组有编号,编号从:把有序分区中的行分发到指定数据的组中,各个组有编号,编号从1开开始,对于每一行,始,对于每一行,NTILE返回此行所属的组的编号。注意:返回此行所属的组的编号。注意:n必须为必须为int类型。类型。
在项目中是否 自定义过 UDF、 UDTF函数 以及用他们 处理 了什么问题 及自定义 步骤
1 自定义过。
2 用 UDF函数解析公共字段;用 UDTF函数解析事件字段。
自定义UDF 继承 UDF 重写 evaluate方法
自定义UDTF:继承自 GenericUDTF,重写 3个方法: initialize(自定义输出的列名和类型 process(将结果返回 forward( close
为什么要自定义
UDF/UDTF,因为自定义函数,可以自己埋点 Log打印日志,出错或者数据异常,方便调试 .
MapJoin
如果不指定MapJoin或者不符合MapJoin的条件,那么Hive解析器会将join操作转化成CommonJoin,即:在Reduce阶段完成Join。容易发生数据倾斜。可以用MapJoin把小表全部加载到内存在map端运行join,避免reduce处理。
行列过滤
列处理:在SELECT中,只拿需要的列,如果有,尽量使用分区过滤,少用select *;
行处理:在分区裁剪中,当使用外关联时,如果将副表的过滤条件写在where后面,那么就会先关联全表,之后在过滤。
采用分桶技术
采用分区技术
合理设置Map数
通常情况下,作业会通过input的目录产生一个或多个map任务
主要的决定因素有:input的文件总个数,input的文件大小,集群设置的文件块大小
map数不是越多越好,也不是保证每个map处理接近1258M的文件块就好
针对这两种情况:要适当减少map数和增加map数来做调整
小文件的合并
在Map执行前合并小文件,减少Map数:CombineHiveInputFormat具有对小文件进行合并的功能(系统默认的格式)。
HiveInputFormat没有对小文件合并功能。
合理设置Reduce数
Reduce个数不是越多越好
过多的启动和初始化Reduce,也会消耗时间和资源。
有多少个Reduce,就会有多少个输出文件,如果生成了很多个小文件,那么这些小文件作为下一个任务的属入,就会产生小文件输入的问题。
在设置Reduce个数的时候也需要考虑两个原则:处理大数据量利用合适的Reduce数,使单个Reduce任务处理数据量大小合适
常用参数
-- 输出合并小文件 -- 默认true,在map-only任务结束时合并小文件 SET hive.merge.mapfiles = true; -- 默认false,在 map-reduce任务结束时合并小文件 SET hive.merge.mapredfiles = true; -- 默认256M SET hive.merge.size.per.task = 268435456; -- 当输出文件的平均大小小于16M该值时,启动一个独立的map-reduce任务进行文件merge SET hive.merge.smallfiles.avgsize = 16777216;
开启map端combiner(不影响最终业务逻辑)
SET hive.map.aggr = true;
压缩(选择快的)
设置map端输出,中间结果压缩。(不完全是解决数据倾斜的问题,但是减少了IO读写和网络传输,能提高效率)
1)group by
注: group by 优于 distinct group 情形: group by 维度过小,某值的数量过多
后果:处理某值的 reduce非常耗时
解决方式:采用 sum() group by的方式来替换 count(distinct)完成计算。
2)count(distinct) count(distinct xx)情形:某特殊值过多
后果:处理此特殊值的 reduce耗时;只有一个 reduce任务
解决方式: count distinct时,将值为空的情况单独处理,比如可以直接过滤空值的行,在最后结果中加 1。如果还有 其他计算,需要进行 group by,可以先将值为空的记录单独处理,再和其他计算结果进行 union。3)mapjoin
4)不同数据类型关联产生数据倾斜
情形:比如用户表中 user_id字段为 int log表中 user_id字段既有 string类型也有 int类型。当按照 user_id进行两个表的 Join操作时。
后果:处理此特殊值的 reduce耗时;只有一个 reduce任务 默认的 Hash操作会按 int型的 id来进行分配,这样会导致所有 string类型 id的记录都分配到一个 Reducer中。
解决方式:把数字类型转换成字符串类型
select * from users a left outer join logs b on a.usr_id = cast(b.user_id as string)5)开启数据倾斜时负载均衡
set hive.groupby.skewindata=true; 思想 就是先随机分发并处理,再按照 key group by来分发处理。
操作操作::当选项设定为当选项设定为true,生成的查询计划会有两个,生成的查询计划会有两个MRJob。第一个 MRJob 中, Map的输出结果集合会随机分布到 Reduce中,每个 Reduce做部分聚合操作,并输出结果,这样处理的结果是相同的 GroupBy Key有可能被分发到不同的Reduce中,从而达到负载均衡的目的;
第二个 MRJob再根据预处理的数据结果按照 GroupBy Key分布到 Reduce中(这个过程可以保证相同的原始 GroupBy Key被分布到同一个 Reduce中),最后完成最终的聚合操作。点评:它使计算变成了两个 mapreduce,先在第一个中在 shuffle 过程 partition 时随机给 key 打标记, 使每个 key 随机均匀分布到各个 reduce 上计算,但是这样只能完成部分计算,因为相同 key没有分配到相同 reduce上。
所以需要第二次的 mapreduce,这次就回归正常 shuffle,但是数据分布不均匀的问题在第一次 mapreduce已经有了很大的改善,因此基本解决数据倾斜。因为大量计算已经在第一次mr中随机分布到各个节点完成。6)控制空值分布
将为空的 key转变为字符串加随机数或纯随机数,将因空值而造成倾斜的数据分不到多个 Reducer。
注:对于异常值如果不需要的话,最好是提前在 where条件里过滤掉,这样可以使计算量大大减少
实践中,可以使用 case when对空值赋上随机值。此方法比直接写 is not null更好,因为前者 job数为 1,后者为 2.使用 case when实例 1:
select userid, name from user_info a join ( select case when userid is null then cast (rand(47)* 100000 as int ) else userid end from user_read_log ) b on a.userid = b.userid
使用 case when实例 2:
select '${date}' as thedate, a.search_type, a.query, a.category, a.cat_name, a.brand_id, a.brand_name, a.dir_type, a.rewcatid, a.new_cat_name, a.new_brand_id, f.brand_name as new_brand_name, a.pv, a.uv, a.ipv, a.ipvuv, a.trans_amt, a.trans_num, a.alipay_uv from fdi_search_query_cat_qp_temp a left outer join brand f on f.pt='${date}000000' and case when a.new_brand_id is null then concat('hive',rand() ) else a.new_brand_id end = f.brand_id;
如果上述的方法还不能解决,比如当有多个 JOIN的时候,建议建立临时表,然后拆分HIVE SQL语句。
分区在HDFS上的表现形式是一个目录, 分桶是一个单独的文件
分区: 细化数据管理,直接读对应目录,缩小mapreduce程序要扫描的数据量
分桶:分桶是相对分区进行更细粒度的划分。分桶将整个数据内容安装某列属性值得hash值进行区分。
作用:
1、提高join查询的效率(用分桶字段做连接字段)
2、提高采样的效率
UDF: 单行进入,单行输出
UDAF: 多行进入,单行输出
UDTF: 单行输入,多行输出
HiveSQL ->AST(抽象语法树) -> QB(查询块) ->OperatorTree(操作树)->优化后的操作树->mapreduce任务树->优化后的mapreduce任务树.
过程描述如下:
(1)、SQL Parser:Antlr定义SQL的语法规则,完成SQL词法,语法解析,将SQL转化为抽象 语法树AST Tree;
(2)、Semantic Analyzer:遍历AST Tree,抽象出查询的基本组成单元QueryBlock;
(3)、Logical plan:遍历QueryBlock,翻译为执行操作树OperatorTree;
(4)、Logical plan optimizer: 逻辑层优化器进行OperatorTree变换,合并不必要的 ReduceSinkOperator,减少shuffle数据量;
(5)、Physical plan:遍历OperatorTree,翻译为MapReduce任务;
(6)、Physical plan optimizer:物理层优化器进行MapReduce任务的变换,生成最终的执行计划;
1)rowkey 长度原则
2)rowkey 散列原则
3)rowkey 唯一原则
1)生成随机数、hash、散列值
2)字符串反转
一个HRegionServer会负责管理很多个region
一个region包含很多个store
划分规则:一个列族就划分成一个store,如果一个表中只有1个列族,那么每一个region中只有一个store
一个store里面只有一个memstore
memstore是一块内存区域,数据会先写入到memstore进行缓冲,然后再把数据刷到磁盘
一个store里面有很多个StoreFile, 最后数据是以很多个HFile这种数据结构的文件保存在hdfs上
StoreFile是HFile的抽象对象,如果说到StoreFile就等于HFile。
每次memstore刷写数据到磁盘 就生成对应的一个新的HFile文件出来
hbase读数据流程
(1)首先从zk找到meta表的region位置,然后读取meta表中的数据,meta表中存储了用户表的 region信息
(2)根据要查询的namespace、表名和rowkey信息。找到写入数据对应的region信息
(3)找到这个region对应的regionServer,然后发送请求
(4)查找对应的region
(5)先从memstore查找数据,如果没有,再从BlockCache上读取
HBase上Regionserver的内存分为两个部分
一部分作为Memstore,主要用来写;
另外一部分作为BlockCache,主要用于读数据;
(6)如果BlockCache中也没有找到,再到StoreFile上进行读取
从storeFile中读取到数据之后,不是直接把结果数据返回给客户端,而是把数据先写入到 BlockCache中,目的是为了加快后续的查询;然后在返回结果给客户端。
hbase写数据流程
(1)首先从zk找到meta表的region位置,然后读取meta表中的数据,meta表中存储了用户表的 region信息
(2)根据namespace、表名和rowkey信息。找到写入数据对应的region信息
(3)找到这个region对应的regionServer,然后发送请求
(4)把数据分别写到HLog(write ahead log)和memstore各一份
(5)memstore达到阈值后把数据刷到磁盘,生成storeFile文件
(6)删除HLog中的历史数据
在hbase中每当有memstore数据flush到磁盘之后,就形成一个storefile,当storeFile的数量达到一定程度后,就需要将storefile 文件来进行 compaction 操作。
Compact 的作用
① 合并文件
② 清除过期,多余版本的数据
③ 提高读写数据的效率HBase 中实现了两种 compaction 的方式:minor and major.这两种 compaction 方式的区别
① Minor 操作只用来做部分文件的合并操作以及包括minVersion=0 并且设置 ttl 的过期版本清理,不做任何删除数据、多版本数据的清理工作。
② Major 操作是对 Region 下的HStore下的所有StoreFile执行合并操作,最终的结果是整理合并出一个文件。
需求分析
(1)百亿数据:证明数据量非常大;
(2)存入HBase:证明是跟HBase的写入数据有关;
(3)保证数据的正确:要设计正确的数据结构保证正确性;
(4)在规定时间内完成:对存入速度是有要求的。
解决思路
(1)数据量百亿条,什么概念呢?假设一整天60x60x24 =86400秒都在写入数据,那么每秒的写入条数高达100万条,HBase当然是支持不了每秒百万条数据的,所以这百亿条数据可能不是通过实时地写入,而是批量地导入。批量导入推荐使用BulkLoad方式,性能是普通写入方式几倍以上;
(2)存入HBase:普通写入是用JavaAPI put来实现,批量导入推荐使用BulkLoad;
(3)保证数据的正确:这里需要考虑RowKey的设计、预建分区和列族设计等问题;
(4)在规定时间内完成也就是存入速度不能过慢,并且当然是越快越好,使用BulkLoad。
预分区的目的主要是在创建表的时候指定分区数,提前规划表有多个分区region,以及每个分区region的区间范围,这样在存储的时候rowkey按照分区的区间存储,可以避免region热点问题。
方案1
##shell 方法
create 'tb_splits', {NAME => 'cf',VERSIONS=> 3},{SPLITS => ['10','20','30']}
方案2
//java程序控制
//(1)取样,先随机生成一定数量的rowkey,将取样数据按升序排序放到一个集合里;
//(2)根据预分区的region个数,对整个集合平均分割,即是相关的splitKeys;
HBaseAdmin.createTable(HTableDescriptortableDescriptor,byte[][]splitkeys)
//可以指定预分区的splitKey,即是指定region间的rowkey临界值。
(1)内部是一个bit数组,初始值均为0
(2)插入元素时对元素进行hash并且映射到数组中的某一个index,将其置为1,再进行多次不同的hash算法,将映射到的index置为1,同一个index只需要置1次。
(3)查询时使用跟插入时相同的hash算法,如果在对应的index的值都为1,那么就可以认为该元素可能存在,注意:只是可能存在
(4)所以BlomFilter只能保证过滤掉不包含的元素,而不能保证误判包含
设置:在建表时对某一列设置BloomFilter即可
region中的rowkey是有序存储,若时间比较集中。就会存储到一个region中,这样一个region的数据变多,其它的region数据很少,加载数据就会很慢,直到region分裂,此问题才会得到缓解。
(1)开启bloomfilter过滤器,开启bloomfilter比没开启要快3、4倍。
(2)Hbase对于内存有特别的需求,在硬件允许的情况下配足够多的内存给它。
(3)通过修改hbase-env.sh中的 export HBASE_HEAPSIZE=3000 #这里默认为1000m
(4)增大RPC数量
通过修改hbase-site.xml中的hbase.regionserver.handler.count属性,可以适当的放大RPC数量,默认值为10有点小。
(1) Row Key设计:rowkey的设计原则
(2) 列族的设计:不要在一张表里定义太多的column family
(3) 预分区:提前把分区规划好,提高hbase性能
(4)内存优化:GC过程持续太久会导致RegionServer处于长期不可用状态
(5) Time To Live:设置表中数据的存储生命期,过期数据将自动被删除
(6)多HTable并发写:创建多个HTable客户端用于写操作,提高写数据的吞吐量
(7)多HTable并发读:创建多个HTable客户端用于读操作,提高读数据的吞吐量
(8) 缓存查询结果:对于频繁查询HBase的应用场景,可以考虑在应用程序中做缓存
(9) Blockcache:读请求先到Memstore中查数据,查不到就到BlockCache中查,再查不到就会到磁盘上读,并把读的结果放入BlockCache
目前我们都知道基于rowkey去查询hbase表数据效率是非常高,原因在于hbase的rowkey本身就有序,但是在实际的业务中,我们往往要按照多条件进行组合查询,这样一来,我们就事先并不
知道满足条件的rowkey信息数据是什么,就无法基于rowkey来快速检索数据。这样我们可以构建hbase的二级索引,二级索引其本质是为了构建原始hbase表中rowkey与列之间的映射关系,把这种映射关系记录下来,在查询数据的时候,就可以先按照条件去查询映射关系表,然后就得到了满足条件的rowkey数据,最后通过rowkey再去查询原始hbase表数据就可以了。
比较简单的方案可以使用phoenix来构建hbase的二级索引。以sql语句的形式去操作hbase表,进行数据的快速检索。
/opt/module/sqoop/bin/sqoop import \
--connect \
--username \
--password \
--target-dir \
--delete target dir \
--num mappers \
--fields terminated by \
--query "$2" 'and $CONDITIONS;'
--check-column \ # 指定判断检查的依据字段
--incremental \ # 用来指定增量导入的模式(Mode),append和lastmodified
--last-value \ # 上一次导入结束的时间
--m \ # mapTask的个数
--merge-key \ # 主键
补充:
(1)如果使用merge-key合并模式 如果是新增的数据则增加,因为incremental是lastmodified模式,那么当有数据更新了,而主键没有变,则会进行合并。
(2)--check-column字段当数据更新和修改这个字段的时间也要随之变化,mysql中建表时该字段修饰符,字段名timestamp default current_timestamp on update current_timestamp
hive中的Null在底层是以 "\N" 来存储,而MySQL中的Null在底层就是Null,为了保证数据两端的一致性,转化的过程中遇到null-string,null-non-string数据都转化成指定的类型,通常指定成"\N"。
在导出数据时采用 –input-null-string "\N" --inputnull-non-string "\N" 两个参数。
导入数据时采用 –null-string "\N" --null-non-string"\N"。
场景
如Sqoop在导出到Mysql时,使用4个Map任务,过程中有2个任务失败,那此时MySQL中存储了另外两个Map任务导入的数据,此时老板正好看到了这个报表数据。而开发工程师发现任务失败后,会调试问题并最终将全部数据正确的导入MySQL,那后面老板再次看报表数据,发现本次看到的数据与之前的不一致,这在生产环境是不允许的。
解决办法1
设置map数量为1个(不推荐,面试官想要的答案不只这个),这样效率会非常之低。
解决办法2
使用—staging-table选项,将hdfs中的数据先导入到辅助表中,当hdfs中的数据导出成功后,辅助表中的数据在一个事务中导出到目标表中(也就是说这个过程要不完全成功,要不完全失败)。
为了能够使用staging这个选项,staging表在运行任务前或者是空的,要不就使用—clear-staging-table配置,如果staging表中有数据,并且使用了—clearstaging-table选项,sqoop执行导出任务前会删除
staging表中所有的数据。
示例
sqoop export \
--connect url \
--username root \
--password 123456 \
--table app_cource_study_report \
--columns watch_video_cnt,complete_video_cnt,dt \
--fields-terminated-by "\t" \
--export-dir "/user/hive/warehouse/tmp.db/app_cource_study_${day}" \
--staging-table app_cource_study_report_tmp \
--clear-staging-table \
--input-null-string '\N' \
--null-non-string "\N"
Sqoop任务5分钟-2个小时的都有。取决于数据量。
只有Map阶段 ,没有 Reduce阶段 的 任务。
1) Local:运行在一台机器上,通常是练手或者测试环境。
2) Standalone:构建一个基于 Mster+Slaves的资源调度集群, Spark任务提交给 Master运行。是 Spark自身的一个调度系统。
3) Yarn: Spark客户端直接连接 Yarn,不需要额外构建 Spark集群。有 yarn-client和yarn-cluster两种模式,主要区别在于: Driver程序的运行节点。
4) Mesos:国内大环境比较少用。
Shell 脚本。
1)在提交任务时的几个重要参数
executor-cores —— 每个 executor使用的内核数,默认为 1,官方建议 2-5个,我们企业是 4个
num-executors —— 启动 executors的数量,默认为 2
executor-memory —— executor内存大小,默认 1G
driver-cores —— driver使用内核数,默认为 1
driver-memory —— driver内存大小,默认 512M
spark-submit \
--master local[5] \
--driver-cores 2 \
--driver-memory 8g \
--executor-cores 4 \
--num-executors 10 \
--executor-memory 8g \
--class PackageName.ClassName XXXX.jar \
--name "Spark Job Name" \
InputPath \
OutputPath
如果这里通过--queue 指定了队列,那么可以免去写--master
RDD 在Lineage 依赖方面分为两种Narrow Dependencies 与Wide Dependencies 用来解决数据容错时的高效性以及划分任务时候起到重要作用。
Stage:根据 RDD之间的依赖关系的不同将 Job划分成不同的 Stage,遇到一个宽依赖则划分一个 Stage。
Task Stage是一个 TaskSet,将 Stage根据分区数划分成一个个的 Task。RDD和它依赖的父RDD(s)的关系有两种不同的类型,即窄依赖(narrow dependency)和宽依赖(wide dependency)。
宽依赖:指的是多个子RDD的Partition会依赖同一个父RDD的Partition
窄依赖:指的是每一个父RDD的Partition最多被子RDD的一个Partition使用。
1)map (func):返回一个新的 RDD,该 RDD由每一个输入元素经过 func函数转换后组成 .
2)mapPartitions(func) :类似于 map,但独立地在 RDD的每一个分片上运行,因此在类型为 T的 RD上运行时 func的函数类型必须是 Iterator[T] => Iterator[U]。假设有 N个元素,有 M个分区,那么 map的函数的将被调用 N次 ,而 mapPartitions被调用 M次 ,一个函数一次处理所有分区。
3)reduceByKey (func,[numTask]) :在一个 (K,V)的 RDD上调用,返回一个 (K,V)的RDD,使用定的 reduce函数,将相同 key的值聚合到一起, reduce任务的个数可以通过第二个可选的参数来设置。
4)aggregateByKey (zeroValue:U,[partitioner: Partitioner]) (seqOp: (U, V) => U,combOp: (U, U) => U: 在 kv对的 RDD中,,按 key将 value进行分组合并,合并时,将每个 value和初始值作为 seq函数的参数,进行计算,返回的结果作为一个新的 kv对,然后再将结果按照 key进行合并,最后将每个分组的 value传递给 combine函数进行计算(先将前两个value进行计算,将返回结果和下一个 value传给 combine函数,以此类推),将 key与计算结果作为一个新的 kv对输出。
5)combineByKey(createCombiner: V=>C, mergeValue: (C, V) =>C, mergeCombiners: (C, C) =>C): 对相同K,把 V合并成一个集合。
1.createCombiner: combineByKey() 会遍历分区中的所有元素,因此每个元素的键要么还没有遇到过,要么就和之前的某个元素的键相同。如果这是一个新的元素,combineByKey()会使用一个叫作会使用一个叫作createCombiner()的函数来创建那个键对应的累加器的初的函数来创建那个键对应的累加器的初始值始值
2.mergeValue: 如果这是一个在处理当前分区之前已经遇到的键,它会使用如果这是一个在处理当前分区之前已经遇到的键,它会使用mergeValue()方法将该键的累加器对应的当前值与这个新的值进行合并方法将该键的累加器对应的当前值与这个新的值进行合并
3.mergeCombiners: 由于每个分区都是独立处理的,由于每个分区都是独立处理的, 因此对于同一个键可以有多个累因此对于同一个键可以有多个累加器。如果有两个或者更多的分区都有对应同一个键的累加器,加器。如果有两个或者更多的分区都有对应同一个键的累加器, 就需要使用用户提供的就需要使用用户提供的 mergeCombiners() 方法将各个分区的结果进行合并。方法将各个分区的结果进行合并。
1 reduce
2 collect
3 first
4 take
5 aggregate
6 countByKey
7 foreach
8 saveAsTextFile
reduceBykey
groupByKey
…ByKey:
未经优化的HashShuffle
优化后的Shuffle:
普通的SortShuffle:
当 shuffle read task 的 数 量 小 于 等 于 spark.shuffle.sort。
bypassMergeThreshold 参数的值时(默认为 200),就会启用 bypass 机制。
reduceByKey:按照key 进行聚合,在shuffle 之前有combine(预聚合)操作,返回结果是RDD[k,v]。
groupByKey:按照 key进行分组,直接进行 shuffle。
1)关系:
两者都是用来改变RDD的 partition数量的, repartition底层调用的就是 coalesce方法: coalesce(numPartitions, shuffle = true)
2)区别:
repartition一定会发生 shuffle coalesce根据传入的参数来判断是否发生 shuffle
一般情况下增大rdd的 partition数量使用 repartition,减少 partition数量时使用coalesce
都是做RDD持久化的
cache:内存,不会截断血缘关系,使用计算过程中的数据缓存。
checkpoint:磁盘,截断血缘关系,在 ck之前必须没有任何任务提交才会生效, ck过程会额外提交一次任务。
累加器(accumulator)是 Spark中提供的一种分布式的变量机制,其原理类似于mapreduce,即分布式的改变,然后聚合这些改变。累加器的一个常见用途是在调试时对作业执行过程中的事件进行计数。而广播变量用来高效分发较大的对象。
共享变量出现的原因:
通常在向Spark 传递函数时,比如使用 map() 函数或者用 filter() 传条件时,可以使用驱动器程序中定义的变量,但是集群中运行的每个任务都会得到这些变量的一份新的副本,更新这些副本的值也不会影响驱动器中的对应变量。Spark的两个共享变量,累加器与广播变量,分别为结果 聚合与广播这两种常见的通信模式突破了这一限制。
使用foreachPartition代替 foreach,在 foreachPartition内 获取数据库的连接。
方法1:
(1)按照 key 对数据进行聚合(groupByKey)
(2)将 value 转换为数组,利用 scala 的 sortBy 或者 sortWith 进行排序(mapValues)数据量太大,会 OOM 。方法2
(1)取出所有的 key
(2)对 key 进行迭代,每次取出一个 key 利用 spark 的排序算子进行排序方法3
(1)自定义分区器,按照 key 进行分区,使不同的 key 进到不同的分区
(2)对每个分区运用 spark 的排序算子进行排序
这里举个例子。比如我们有几百个文件,会有几百个map 出现,读取之后进行 join 操作,会非常的慢。这个时候我们可以进行 coalesce 操作,比如 240 个 map ,我们合成 60个 map ,也就是窄依赖。这样再 shuffle ,过程产生的文件数会大大减少。提高 join 的时间性能。
(1)基于内存计算,减少低效的磁盘交互;
(2)高效的调度算法,基于DAG;
(3)容错机制Lingage,主要是DAG和Lianage,即使spark不使用内存技术,也大大快于mapreduce。
RDD(Resilient Distributed Dataset)叫做分布式数据集,是Spark中最基本的数据抽象,它代表一个不可变、可分区、里面的元素可并行计算的集合。
RDD五大特性
A list of partitions
一个分区列表,RDD中的数据都存在一个分区列表里面
A function for computing each split
作用在每一个分区中的函数
A list of dependencies on other RDDs
一个RDD依赖于其他多个RDD,这个点很重要,RDD的容错机制就是依据这个特性而来的
Optionally, a Partitioner for key-value RDDs (e.g. to saythat the RDD is hash-partitioned)
可选项,针对于kv类型的RDD才具有这个特性,作用是决定了数据的来源以及数据处理后的去向
Optionally, a list of preferred locations to compute each split on (e.g. block locations for an HDFS file)
可选项,数据本地性,数据位置最优
(1) 自动进行内存和磁盘切换
(2) 基于lineage的高效容错
(3) task如果失败会特定次数的重试
(4) stage如果失败会自动进行特定次数的重试,而且只会只计算失败的分片
(5) checkpoint【每次对RDD操作都会产生新的RDD,如果链条比较长,计算比较笨重,就把数据放在 硬盘中】和persist【内存或磁盘中对数据进行复用】(检查点、持久化)
(6) 数据调度弹性:DAG TASK 和资源管理无关
(7) 数据分片的高度弹性repartion
map、mapPartitions、foreach、foreachPartition
map:用于遍历RDD,将函数f应用于每一个元素,返回新的RDD(transformation算子)。
foreach:用于遍历RDD,将函数f应用于每一个元素,无返回值(action算子)。
mapPartitions:用于遍历操作RDD中的每一个分区,返回生成一个新的RDD(transformation 算子)。
foreachPartition: 用于遍历操作RDD中的每一个分区。无返回值(action算子)。
总结:一般使用mapPartitions或者foreachPartition算子比map和foreach更加高效,推荐使用。
在通过spark-submit提交任务时,可以通过添加配置参数来指定
--driver-class-path 外部jar包
--jars 外部jar包
1. driver端的内存溢出
可以增大driver的内存参数:spark.driver.memory (default 1g)
这个参数用来设置Driver的内存。在Spark程序中,SparkContext,DAGScheduler都是运行在Driver端的。对应rdd的Stage切分也是在Driver端运行,如果用户自己写的程序有过多的步骤,切分出过多的Stage,这部分信息消耗的是Driver的内存,这个时候就需要调大Driver的内存。2. map过程产生大量对象导致内存溢出
这种溢出的原因是在单个map中产生了大量的对象导致的,例如:rdd.map(x=>for(i <- 1 to 10000) yield i.toString),这个操作在rdd中,每个对象都产生了10000个对象,这肯定很容易产生内存溢出的问题。针对这种问题,在不增加内存的情况下,可以通过减少每个Task的大小,以便达到每个Task即使产生大量的对象Executor的内存也能够装得下。具体做法可以在会产生大量对象的map操作之前调用
repartition方法,分区成更小的块传入map。例如:rdd.repartition(10000).map(x=>for(i <- 1 to 10000) yield i.toString)。 面对这种问题注意,不能使用rdd.coalesce方法,这个方法只能减少分区,不能增加分区,不会有shuffle的过程。
3. 数据不平衡导致内存溢出
数据不平衡除了有可能导致内存溢出外,也有可能导致性能的问题,解决方法和上面说的类似,就是调用repartition重新分区。这里就不再累赘了。
4. shuffle后内存溢出
shuffle内存溢出的情况可以说都是shuffle后,单个文件过大导致的。在Spark中,join,reduceByKey这一类型的过程,都会有shuffle的过程,在shuffle的使用,需要传入一个partitioner,大部分Spark中的shuffle操作,默认的partitioner都是HashPatitioner,默认值是父RDD中最大的分区数,这个参数通过spark.default.parallelism控制(在spark-sql中用spark.sql.shuffle.partitions) ,spark.default.parallelism参数只对HashPartitioner有效,所以如果是别的Partitioner或者自己实现的Partitioner就不能使用spark.default.parallelism这个参数来控制shuffle的并发量了。如果是别的partitioner导致的shuffle内存溢出,就需要从partitioner的代码增加partitions的数量。standalone模式下资源分配不均匀导致内存溢出在standalone的模式下如果配置了--total-executor-cores 和--executor-memory 这两个参数,但是没有配置--executorcores这个参数的话,就有可能导致,每个Executor的memory是一样的,但是cores的数量不同,那么在cores数量多的Executor中,由于能够同时执行多个Task,就容易导致内存溢出的情况。这种情况的解决方法就是同时配置--executor-cores或者spark.executor.cores参数,确保Executor资源分配均匀。5. 使用rdd.persist(StorageLevel.MEMORY_AND_DISK_SER)代替rdd.cache()
rdd.cache()和rdd.persist(Storage.MEMORY_ONLY)是等价的,在内存不足的时候rdd.cache()的数据会丢失,再次使用的时候会重算,而
rdd.persist(StorageLevel.MEMORY_AND_DISK_SER)在内存不足的时候会存储在磁盘,避免重算,只是消耗点IO时间。
cache:缓存数据,默认是缓存在内存中,其本质还是调用persist
persist:缓存数据,有丰富的数据缓存策略。数据可以保存在内存也可以保存在磁盘中,使用的时候指定对应的缓存级别就可以了。
reduceByKey:按照key进行聚合,在shuffle之前有combine(预聚合)操作,返回结果是RDD[k,v]。
groupByKey: 按照key进行分组,直接进行shuffle。对数据进行全量拉取,不会进行数据的预聚合
开发指导:reduceByKey比groupByKey,建议使用。但是需要注意是否会影响业务逻辑。
1、基于Receiver的方式
这种方式使用Receiver来获取数据。Receiver是使用Kafka的高层次Consumer API来实现的。receiver从Kafka中获取的数据都是存储在Spark Executor的内存中的,然后Spark Streaming启动的job会去处理那些数据。
2、基于Direct的方式
这种新的不基于Receiver的直接方式,是在Spark 1.3中引入的,从而能够确保更加健壮的机制。替代掉使用Receiver来接收数据后,这种方式会周期性地查询Kafka,来获得每个topic+partition的最新的offset,从而定义每个batch的offset的范围。当处理数据的job启动时,就会使用Kafka的简单consumer api来获取Kafka指定offset范围的数据
Driver通过collect把集群中各个节点的内容收集过来汇总成结果,collect返回结果是Array类型的,collect把各个节点上的数据抓过来,抓过来数据是Array型类。Array中封装了RDD中的数据内容。
yarn-cluster模式
yarn-client模式
最大的区别就是Driver端的位置不一样。
yarn-cluster: Driver端运行在yarn集群中,与ApplicationMaster进程在一起。
yarn-client: Driver端运行在提交任务的客户端,与ApplicationMaster进程没关系,经常 用于进行测试
(1)shuffle产生海量的小文件在磁盘上,此时会产生大量耗时的、低效的IO操作;
(2)容易导致内存不够用,由于内存需要保存海量的文件操作句柄和临时缓存信息,如果数据处理规模比较大的化,容易出现OOM;
(3)容易出现数据倾斜,导致OOM。
(1)参数用于设置每个stage的默认task数量。这个参数极为重要,如果不设置可能会直接影响你的Spark作业性能;
(2)很多人都不会设置这个参数,会使得集群非常低效,你的cpu,内存再多,如果task始终为1,那也是浪费,spark官网建议task个数为CPU的核数*executor的个数的2~3倍。
(1)用于设置RDD持久化数据在Executor内存中能占的比例,默认是0.6,,默认Executor 60%的内存,可以用来保存持久化的RDD数据。根据你选择的不同的持久化策略,如果内存不够时,可能数据
就不会持久化,或者数据会写入磁盘;
(2)如果持久化操作比较多,可以提高spark.storage.memoryFraction参数,使得更多的持久化数据保存在内存中,提高数据的读取性能,如果shuffle的操作比较多,有很多的数据读写操作到JVM中,那么应该调小一点,节约出更多的内存给JVM,避免过多的JVM gc发生。在web ui中观察如果发现gc时间很长,可以设置spark.storage.memoryFraction更小一点。
(1)spark.shuffle.memoryFraction是shuffle调优中 重要参数,shuffle从上一个task拉去数据过来,要在Executor进行聚合操作,聚合操作时使用Executor内存的比例由该参数决定,默认是20%如果聚合时数据超过了该大小,那么就会spill到磁盘,极大降低性能;
(2)如果Spark作业中的RDD持久化操作较少,shuffle操作较多时,建议降低持久化操作的内存占比,提高shuffle操作的内存占比比例,避免shuffle过程中数据过多时内存不够用,必须溢写到磁盘上,降低了性能。此外,如果发现作业由于频繁的gc导致运行缓慢,意味着task执行用户代码的内存不够用,那么同样建议调低这个参数的值。
发现数据倾斜的时候,不要急于提高executor的资源,修改参数或是修改程序,首先要检查数据本身,是否存在异常数据。
### 1、数据问题造成的数据倾斜
- **找出异常的key**
- 如果任务长时间卡在最后最后1个(几个)任务,首先要对key进行抽样分析,判断是哪些key造成的。 选取key,对数据进行抽样,统计出现的次数,根据出现次数大小排序取出前几个。
- 比如: **df.select("key").sample(false,0.1).(k=>(k,1)).reduceBykey(+).map(k=>(k.2,k.1)).sortByKey(false).take(10)**
- 如果发现多数数据分布都较为平均,而个别数据比其他数据大上若干个数量级,则说明发生了数据倾斜。- 经过分析,倾斜的数据主要有以下三种情况:
- null(空值)或是一些无意义的信息之类的,大多是这个原因引起。
- 无效数据,大量重复的测试数据或是对结果影响不大的有效数据。
- 有效数据,业务导致的正常数据分布。
- **解决办法**
1. 第1,2种情况,直接对数据进行过滤即可(因为该数据对当前业务不会产生影响)。
2. 第3种情况则需要进行一些特殊操作,常见的有以下几种做法- 隔离执行,将异常的key过滤出来单独处理,最后与正常数据的处理结果进行union操作。
- 对key先添加随机值,进行操作后,去掉随机值,再进行一次操作。
- 使用reduceByKey 代替groupByKey(reduceByKey用于对每个key对应的多个value进行merge操作,最重要的是它能够在本地先进行merge操作,并且merge操作可以通过函数自定义.)- 使用map join。
- **案例**
**如果使用reduceByKey因为数据倾斜造成运行失败的问题。具体操作流程如下:**
- 将原始的 key 转化为 key + 随机值(例如Random.nextInt)
- 对数据进行 reduceByKey(func)
- 将 key + 随机值 转成 key
- 再对数据进行 reduceByKey(func)- **案例操作流程分析:**
- 假设说有倾斜的Key,我们给所有的Key加上一个随机数,然后进行reduceByKey操作;此时同一个Key会有不同的随机数前缀,在进行reduceByKey操作的时候原来的一个非常大的倾斜的Key就分而治之变成若干个更小的Key,不过此时结果和原来不一样,怎么破?进行map操作,目的是把随机数前缀去掉,然后再次进行reduceByKey操作。(当然,如果你很无聊,可以再次做随机数前缀),这样我们就可以把原本倾斜的Key通过分而治之方案分散开来,最后又进行了全局聚合
- **注意1:** 如果此时依旧存在问题,建议筛选出倾斜的数据单独处理。最后将这份数据与正常的数据进行union即可。
- **注意2:** 单独处理异常数据时,可以配合使用Map Join解决。### 2、spark使用不当造成的数据倾斜
- **提高shuffle并行度**
- dataFrame和sparkSql可以设置spark.sql.shuffle.partitions参数控制shuffle的并发度,默认为200。
- rdd操作可以设置spark.default.parallelism控制并发度,默认参数由不同的Cluster Manager控制。
- 局限性: 只是让每个task执行更少的不同的key。无法解决个别key特别大的情况造成的倾斜,如果某些key的大小非常大,即使一个task单独执行它,也会受到数据倾斜的困扰。
- 使用map join 代替reduce join
- 在小表不是特别大(取决于你的executor大小)的情况下使用,可以使程序避免shuffle的过程,自然也就没有数据倾斜的困扰了.(详细见http://blog.csdn.net/lsshlsw/article/details/50834858、http://blog.csdn.net/lsshlsw/article/details/48694893)
- 局限性: 因为是先将小数据发送到每个executor上,所以数据量不能太大。
一、资源调优
driver-memory
executor-memory
num-executors
executors-cores
spark.default.parallelism
参数说明:该参数用于设置每一个stage的默认task数量,这个参数极为重要,如果不设置可能会直接影响spark作业性能
调优建议:spark作业的默认task数量为500-1000个较为合适,很多时候就是不去设置这个参数,那么此时就会导致spark自己根据底层HDFS的block数量来设置task数量,默认是一个HDFS block对应一个task。通常来说,spark默认设置的数量是偏少的,如果task数量偏少的话,就会导致你前面设置好的executor参数都前功尽弃。试想一下无论你的executor进程有多少个,内存和cpu有多大,但是task只有一个或10个,那么90%的executor进程根本没有task执行,也就是说白白浪费了资源!因此官网建议的设置原则是,设置该参数num-executors*executor-cores的2~3倍较为合适。比如,executor的总cpu核数为300个,那么设置1000个task是可以的,此时可以充分的利用spark的集群资源。
spark.sql.shuffle.partitions
参数说明:该参数用来设置sparksql shuffle之后shuflle read task的个数
调优建议:默认是200,一般在大批量数据处理的场景下默认参数太小,需要调整,比如增加到400/500/600等
二、开发调优
RDD的重用和持久化
MEMORY_ONLY
MEMORY_ONLY_SER
MEMORY_AND_DISK_SER
广播变量的使用
尽量避免使用shuffle类算子:Broadcast + map 来代替 join
使用高性能的算子
使用reduceByKey/aggregateByKey替代groupByKey
使用mapPartitions替代普通map
使用foreachPartitions替代foreach
使用filter之后进行coalesce操作
使用repartitionAndSortWithinPartitions替代repartition与sort类操作
使用Kryo优化序列化性能:spark默认采用Java的序列化器,不够高效,使用KryoSerializer
使用fastutil优化数据格式:fastutil集合类,可以减少内存的占用,提供更快的存取速度
调节数据本地化等待时长:spark.loaclity.wait,默认是3s,可以适当增大
三、数据倾斜的调优
使用hive ETL预处理数据
过滤少数导致倾斜的key
提高shuffle操作的并行度
两阶段聚合(局部聚合+全局聚合)
将reduce join转为map join
采样倾斜key并拆分join操作
使用随机前缀和扩容RDD进行join
多种数据倾斜的解决方案综合的灵活使用
四、shuffle相关参数调优
spark.shuffle.filter.buffer
默认值:32k
参数说明:该参数用于设置shuffle write task的BufferOutputStream的buffer缓冲大小。将数据写到磁盘文件之前,会先写入到buffer缓冲中,待缓冲写满之后,才会溢写到磁盘。
调优建议:如果作业可用的内存资源较为充足的话,可以适当增加这个参数的大小(比如64k),从而减少shuffle write过程中溢写磁盘文件的次数,也就可以减少磁盘IO次数,进而提升性能。在实践中发现,合理调节该参数,性能会有1%~5%的提升。
spark.reducer.maxSizeInFlight
默认值:48M
参数说明:该参数用于设置shuffle read task的buffer缓冲大小。而这个buffer缓冲决定了每次能够拉取多少数据。
调优建议:如果作业可用的内存资源较为充足的话,可以适当增加这个参数的大小(比如96M),从而减少拉取数据的次数,也就可以减少网络传输的次数,进而提升性能。在实践中发现,合理调节该参数,性能会有1%~5%的提升。
spark.shuffle.io.maxRetries
默认值:3
参数说明:shuffle read task从shuffle write task所在节点拉取属于自己的数据时,如果因为网络异常导致拉取失败,是会自动进行重试的。该参数代表了可以重试的最大次数。如果在指定次数之内拉取还是没有成功,就可能会导致作业执行失败。
调优建议:对于那些包含了特别耗时的shuffle操作的作业,建议增加重试最大次数(比如60次),以避免由于JVM的full gc或者网络不稳定等因素导致的数据拉取失败。在实践中发现,对于针对超大数据量(数十亿~数百亿)的shuffle过程,调节该参数可以大幅度提升稳定性。
spark.shuffle.io.retryWait
默认值:5s
参数说明:具体解释如上,该参数代表了每次重试拉取数据的等待间隔,默认是5s。
调优建议:建议加大间隔时间(比如60s),以增加shuffle操作的稳定性。
spark.shuffle.memoryFunction
默认值:0.2
参数说明:该参数代表了executor内存中,分配给shuffle read task进行聚合操作的内存比例,默认是20%。
调优建议:在资源参数调优中讲解过这个参数。如果内存充足,而且很少使用持久化操作,建议提高这个比例,给shuffle read的聚合操作更多内存,以避免由于内存不足导致聚合过程中频繁读写磁盘。在实践中发现,合理调节该参数可以将性能提升10%左右。
spark.storage.memoryFunction
默认值:0.6
参数说明:该参数用于设置RDD持久化数据在executor内存中的占比,默认是60%。根据你选择的不同的持久化数据策略级别,如果内存不够的时候,可能数据就不会持久化或者数据会写入到内存中。
调优建议:如果spark任务中,有较多的RDD持久化数据操作,这个值可以适当的提高一些,保证持久化的数据能够容纳在内存中,如果spark任务中shuffle操作较多,那么这个参数的值适当降低一些比较合适,更多的内存空间留给shuffle操作。
spark.shuffle.manager
默认是:sort
参数说明:该参数用于设置ShuffleManager的类型,spark1.5之后,有三个可选项:hash、sort、tungsten-sort。HashShuffleManager是spark1.2以前的默认选项,但是spark1.2以及之后的版本默认的都是SortShuffleManager了。spark1.6之后把hash方式给移出了,tungsten-sort与sort类似,但是使用了tungsten计划中的堆外内存管理机制,内存使用效率更高。
调优建议:由于SortShuffleManager默认会对数据进行排序,因此如果你的业务逻辑中需要该排序机制的话,则使用默认的SortShuffleManager就可以;而如果你的业务逻辑中不需要对数据进行排序的话,那么建议参考后面的几个参数调优,通过bypass机制或优化的HashShuffleManager来避免排序操作,同时提供较好的磁盘读写性能。这里要注意的是,tungsten-sort要慎用,因为之前发现了一些相应的bug。
spark.shuffle.sort.bypassMergeThreshold
默认值:200
参数说明:当ShuffleManager为SortShuffleManager时,如果shuffle read task的数量小于这个阈值(默认是200),则shuffle write过程中不会进行排序操作,而是直接按照未经优化的HashShuffleManager的方式去写数据,但是最后会将每个task产生的所有临时磁盘文件都合并成一个文件,并会创建单独的索引文件。
调优建议:当你使用SortShuffleManager时,如果的确不需要排序操作,那么建议将整个参数调大一些,大于shuffle read task 的数量,那么此时就会启动bypass机制,map-side就不会进行排序了,减少了排序的性能开销。但是这种方式下,依然会产生大量的磁盘文件,因此shuffle write性能有待提高。
1)RDD
优点
编译时类型安全
编译时就能检查出类型错误
面向对象的编程风格
直接通过类名点的方式来操作数据
缺点
序列化和反序列化的性能开销
无论是集群间的通信, 还是 IO 操作都需要对对象的结构和数据进行序列化和反序列化 。
GC的性能开销,频繁的创建和销毁对象 , 势必会增加 GC2)DataFrame
DataFrame引入了 schema 和 off heap
schema : RDD每一行的数据 , 结构都是一样的,这个结构就存储在 schema 中。 Spark 通过 schema 就能够读懂数据 , 因此在通信和 IO 时就只需 要序列化和反序列化数据 , 而结构的部分就可以省略了。3)DataSet
DataSet结合了 RDD 和 DataFrame 的优点,并带来的一个新的概念 Encoder 。
当序列化数据时,Encoder 产生字节码与 off heap 进行交互,能够达到按需访问数据的效果,而不用反序列化整个对象。 Spark 还没有提供自定义 Encoder 的 API ,但是未来会加入。三者之间的转换:
append 在原有分区上进行追加,overwrite 在原有分区上进行全量刷新
coalesce 和repartition 都用于改变分区,coalesce 用于缩小分区且不会进行shuffle,repartition用于增大分区(提供并行度)会进行shuffle,在spark 中减少文件个数会使用coalesce 来减少分区来到这个目的。但是如果数据量过大,分区数过少会出现OOM 所以coalesce 缩小分区个数也需合理
1.11.4 cache 缓存级别
DataFrame 的cache 默认采用 MEMORY_AND_DISK 这和RDD 的默认方式不一样RDD cache 默认采用MEMORY_ONLY
缓存:(1)dataFrame.cache (2)sparkSession.catalog.cacheTable(“tableName”)
释放缓存:(1)dataFrame.unpersist (2)sparkSession.catalog.uncacheTable(“tableName”)
参数 spark.sql.shuffle.partitions 决定 默认并行度 200
kryo 序列化比java 序列化更快更紧凑,但spark 默认的序列化是java 序列化并不是spark 序列化,因为spark并不支持所有序列化类型,而且每次使用都必须进行注册。注册只针对于并不支持所有序列化类型,而且每次使用都必须进行注册。注册只针对于RDD。在。在DataFrames和和DataSet当中自动实现了当中自动实现了kryo序列化。序列化。
DataFrame.createTempView() 创建普通临时表
DataFrame.createGlobalTempView() DataFrame.createOrReplaceTempView() 创建全局临时表
原理:先将小表数据查询出来聚合到driver端,再广播到各个 executor端,使表与表 join时进行本地join,避免进行网络传输产生 shuffle。
使用场景:大表join小表 只能广播小表
spark.reducer.maxSizeInFilght 此参数为 reduce task能够拉取多少数据量的一个参数默认48MB,当集群资源足够时,增大此参数可减少 reduce拉取数据量的次数,从而达到优化shuffle的效果,一般调大为 96MB,资源够大可继续往上跳。
spark.shuffle.file.buffer 此参数为每个 shuffle文件输出流的内存缓冲区大小,调大此参数可以减少在创建 shuffle文件时进行磁盘搜索和系统调用的次数,默认参数为 32k 一般调大为64k。
SparkSession.udf.register 方法进行注册
join和 sql中的 inner join操作很相似,返回结果是前面一个集合和后面一个集合中匹配成功的,过滤掉关联不上的。
leftJoin类似于 SQL中的左外关联 left outer join,返回结果以第一个 RDD为主,关联不上的记录为空。
部分场景下可以使用 left semi join替代 left join
因为left semi join 是 in(keySet) 的关系, 遇到右表重复记录,左表会跳过 ,性能更高 ,而 left join 则会一直遍历。但是 left semi join 中最后 select 的结果中只许出现左表中的列名,因为右表只有 join key 参与关联计算了
kafka参数 auto.offset.reset 参数设置成 earliest 从最初始偏移量开始消费数据
1. 手动维护偏移量
2. 处理完业务数据后,再进行提交 偏移 量操作极端情况下,如在提交偏移量时断网或停电会造成spark程序第二次启动时重复消费问题,所以在涉及到金额或精确性非常高的场景会使用事物保证精准一次消费
把 spark.streaming.backpressure.enabled 参数设置为 ture,开启背压机制后 Spark Streaming会根据延迟动态去 kafka消费数据 ,上限由 spark.streaming.kafka.maxRatePerPartition 参数控制,所以两个参数一般会一起使用
Spark Streaming stage耗时由最慢的 task决定 ,所以数据倾斜时某个 task运行慢会导致整个Spark Streaming都运行非常慢。
把 spark.streaming.stopGracefullyOnShutdown 参数设置成 ture,Spark会在 JVM关闭时正常关闭 StreamingContext,而不是立马关闭
Kill 命令: yarn application -kill 后面跟 applicationid
Spark Streaming默认分区个数与所对接的 kafka topic分区个数一致 Spark Streaming里一般不会使用 repartition算子增大分区,因为 repartition会进行 shuffle增加耗时
一、基于Receiver的方式
这种方式使用Receiver 来获取数据。 Receiver 是使用 Kafka 的高层次 Consumer API来实现的。 receiver 从 Kafka 中获取的数据都是存储在 Spark Executor 的内存中的(如果突然数据暴增,大量 batch 堆积,很容易出现内存溢出的问题),然后 SparkStreaming 启动的 job 会去处理那些数据。然而,在默认的配置下,这种方式可能会因为底层的失败而丢失数据。如果要启用高可靠机制,让数据零丢失,就必须启用 Spark Streaming 的预写日志机制( Write Ahead Log WAL )。该机制会同步地将接收到的 Kafka 数据写入分布式文件系统(比如 HDFS )上的预写日志中。所以,即使底层节点出现了失败,也可以使用预写日志中的数据进行恢复。
二、基于Direct的方式
这种新的不基于Receiver 的直接方式,是在 Spark 1.3 中引入的,从而能够确保更加健壮的机制。替代掉使用 Receiver 来接收数据后,这种方式会周期性地查询 Kafka ,来获得每个 topic+partition 的最新的 offset ,从而定义每个 batch 的 offset 的范围。当处理数据的 job 启动时 ,就会使用 Kafka 的简单 consumer api 来获取 Kafka 指定 offset 范围的数据。优点如下:
简化并行读取:如果要读取多个 partition ,不需要创建多个输入 DStream 然后对它们进行 union 操作。 Spark 会创建跟 Kafka partition 一样多的 RDD partition ,并且会并行从 Kafka 中读取数据。所以在 Kafka partition 和 RDD partition 之间,有一个一对一的映射关系。
高性能:如果要保证零数据丢失,在基于 receiver 的方式中,需要开启 WA L 机制。这种方式其实效率低下,因为数据实际上被复制了两份, Kafka 自己本身就有高可靠的机制,会对数据复制一份,而这里又会复制一份到 WAL 中。而基于 direct 的方式,不依赖Receiver ,不需要开启 WAL 机制,只要 Kafka 中作了数据的复制,那么就可以通过 Kafka的副本进行恢复。一次且仅一次的事务机制。
三、对比:
基于receiver 的方式,是使用 Kafka 的高阶 API 来在 ZooKeeper 中保存消费过的offset 的。这是消费 Kafka 数据的传统方式。这种方式配合着 WAL 机制可以保证数据零丢失的高可靠性,但是却无法保证数据被处理一次且仅一次,可能会处理两次。因为 Spark和 ZooKeeper 之间可能是不同步的。
基于direct 的方式,使用 kafka 的简单 api Spark Streaming 自己就负责追踪消费的 offset ,并保存在 checkpoint 中。 Spark 自己一定是同步的,因此可以保证数据是消费一次且仅消费一次。
在实际生产环境中大都用Direct 方式
窗口函数就是在原来定义的 SparkStreaming 计算批次大小的基础上再次进行封装,每次计算多个批次的数据,同时还需要传递一个滑动步长的参数,用来设置当次计算任务完成之后下一次从什么地方开始计算。
图中time1 就是 SparkStreaming 计算批次大小,虚线框以及实线大框就是窗口的大小,必须为批次的整数倍。虚线框到大实线框的距离(相隔多少批次),就是滑动步长。
公司一:总用户量 1000万, 5台 64G内存的服务器。
公司二:总用户量 10亿, 1000台 64G内存的服务器。
1.公司一的数据分析师在做 join的时候发生了数据倾斜,会导致有几百万用户的相关数据集中到了一台服务器上,几百万的用户数据,说大也不大,正常字段量的数据的话 64G还是能轻松处理掉的。
2.公司二的数据分析师在做 join的时候也发生了数据倾斜,可能会有 1个亿的用户相关数据集中到了一台机器上了(相信我,这很常见)。这时候一台机器就很难搞定了,最后会很难算出结果。
1 hadoop中的数据倾斜 表现:
⚫ 有一个多几个 Reduce卡住 卡在 99.99%,一直不能结束。⚫ 各种 container报错 OOM
⚫ 异常的 Reducer读写的数据量极大,至少远远超过其它正常的 Reducer
⚫ 伴随着数据倾斜,会出现任务被 kill等各种诡异的表现。
2 hive中数 据倾斜一般都发生在 Sql中 group by和 join on上,而且和数据逻辑绑定比较深。
3 Spark中的数据倾斜
Spark中的数据倾斜 包括 Spark Streaming和 Spark Sql,表现主要有下面几种
⚫ Executor lost OOM Shuffle过程出错
⚫ Driver OOM
⚫ 单个 Executor执行时间特别久,整体任务卡在某个阶段不能结束
⚫ 正常运行的任务突然失败
我们以 Spark和 Hive的使用场景为例。
他们在做数据运算的时候会涉及到, count distinct、 group by、 join on等操作,这些都会触发 Shuffle动作。一旦触发 Shuffle,所有相同 key的值就会被拉到一个或几个 Reducer节点上,容易发生单点计算问题,导致数据倾斜。
一般来说,数据倾斜原因有以下几方面:1)key分布不均匀;
2)建表时考虑不周
我们举一个例子,就说数据默认值的设计吧,假设我们有两张表:user(用户信息表 userid register_ip
ip (IP表): ip register_user_cnt 这可能是两个不同的人开发的数据表。如果我们的数据规范不太完善的话,会出现一种情况:
user表中的 register_ip字段,如果获取不到这个信息,我们默认为 null;
但是在 ip表中,我们在统计这个值的时候,为了方便,我们把获取不到 ip的用户,统一认为他们的 ip为 0。
两边其实都没有错的,但是一旦我们做关联了,这个任务会在做关联的阶段,也就是 sql的 on的阶段卡死。3)业务数据激增
比如订单场景,我们在某一天在北京和上海两个城市多了强力的推广,结果可能是这两个城市的订单量增长了 10000%,其余城市的数据量不变。然后我们要统计不同城市的订单情况,这样,一做 group操作,可能直接就数据倾斜了。
很多数据倾斜的问题,都可以用和平台无关的方式解决,比如更好的数据预处理 异常值的过滤等 。 因此,解决数据倾斜的重点在于对数据设计和业务的理解,这两个搞清楚了,数据倾斜就解决了大部分了。
1)业务逻辑
我们从业务逻辑的层面上来优化数据倾斜,比如上面的两个城市做推广活动导致那两个城市数据量激增的例子,我们可以单独对这两个城市来做 count,单独做时可用两次MR,第一次打散计算,第二次再最终聚合计算。完成后和其它城市做整合。2)程序层面
比如说在 Hive中,经常遇到 count(distinct)操作,这样会导致最终只有一个 Reduce任务。
我们可以先 group by,再在外面包一层 count,就可以了。比如计算按用户名去重后的总用户量:
(1)优化前 只有一个 reduce,先去重再 count负担比较大:select name,count(distinct name)from user;
(2)优化后// 设置该任务的每个 job的 reducer个数为 3个。 Hive默认 -1,自动推断。
set mapred.reduce.tasks=3;// 启动两个 job,一个负责子查询 (可以有多个 reduce),另一个负责 count(1)
select count(1) from (select name from user group by name) tmp;3)调参方面
Hadoop和 Spark都自带了很多的参数和机制来调节数据倾斜,合理利用它们就能解决大部分问题。4)从业务和数据上解决数据倾斜
很多数据倾斜都是在数据的使用上造成的。我们举几个场景,并分别给出它们的解决方案。
⚫ 有损的方法:找到异常数据,比如 ip为 0的数据,过滤掉
⚫ 无损的方法:对分布不均匀的数据,单独计算
⚫ 先对 key做一层 hash,先将数据随机打散让它的并行度变大,再汇集
⚫ 数据预处理
Spark数据倾斜只会发生在 shuffle过程中。
这里给大家罗列一些常用的并且可能会触发 shuffle操作的算子: distinct、 groupByKey、reduceByKey、 aggregateByKey、 join、 cogroup、 repartition等。
出现数据倾斜时,可能就是你的代码中使用了这些算子中的某一个所导致的。