读数据流程:
读文件
下载:
1.请求下载第一个块,
2.NameNode检查路径是否存在,不存在抛异常,存在返回DataNode列表
3.返回DataNode列表[DN1,DN2,DN3]
4.建立连接,下载第一个数据块
注:
1下载后面的数据块重复1~4步骤,client对数据块进行连接,形成一个完整的文件.
2.在传输的过程中,也是以packet形式进行传输,默认大小64KB
内部表不会被 external 修饰,而被 external 修饰的就是外部表
区别:
静态分区与动态分区的主要区别在于静态分区是手动指定,而动态分区是通过数据来进行判断。
动态分区可以通过下面的设置来打开:
//设置为true表示开启动态分区功能(默认为false)
set hive.exec.dynamic.partition=true;
//设置为nonstrict,表示允许所有分区都是动态的(默认为strict)
set hive.exec.dynamic.partition.mode=nonstrict;
动态分区:
//注意输入字段的最后面必须是动态分区字段。需要在select中查出分区的字段
insert [overwrite] table tbl_name partition(pt, if_online)
select field1, field2, ..., pt, if_online
from tbl
where xxx;
静态分区:
//不需要在select中查出分区的字段,已指定值
insert [overwrite] table tbl_name partition(pt=20121023, if_online=1)
select field1, field2, ..., fieldn
from tbl
where xxx;
注意分区细节:
(1)尽量不要用动态分区,因为动态分区的时候,将会为每一个分区分配reducer数量,当分区数量多的时候,reducer数量将会增加,对服务器是一种灾难。
(2)动态分区和静态分区的区别,静态分区不管有没有数据都将会创建该分区,动态分区是有结果集将创建,否则不创建。
(3)设置hive动态分区的严格模式:
hive提供我们一个严格模式:为了阻止用户不小心提交恶意hql
hive.mapred.mode=nostrict : strict
如果该模式值为strict,将会阻止以下三种查询:
(1)对分区表查询,where中过滤字段不是分区字段。
(2)笛卡尔积join查询,join查询语句,不带on条件或者where条件。
(3)对order by查询,有order by的查询不带limit语句。
set hive.exec.mode.local.auto=true; --default false
set hive.exec.mode.local.auto.inputbytes.max=50000000
set hive.exec.mode.local.auto.input.files.max=5 --default 4
hive.exec.parallel 值为 true
,就可以开启并发执行。不过,在共享集群中,需要注意下,如果 job 中并行阶段增多,那么集群利用率就会增加。set hive.exec.parallel=true;//打开任务并行执行
set hive.exec.parallel.thread.number=16;//同一个 sql 允许最大并行度,默认为 8
<property>
<name>mapred.job.reuse.jvm.num.tasks</name>
<value>10</value>
</property>
严格模式 :可以防止用户执行那些可能产生意想不到的不好的影响查询
合理设置map和reduce的数量
hive通过将查询划分为一个或者多个MR任务达到并行化的目的,每个任务都可能具有多个mapper和reducer任务,其中一些是可以并行执行的,确定最佳的mapper个数和reducer个数取决于多个变量,例如输入的数据量的大小以及对这些数据操作的类型等。
设置 太多 的mapper与reducer个数,就会导致启动阶段,调度与运行job的过程中产生过多的开销;
设置 太少 ,那么就可能没有充分利用好集群的并行性
hive会根据输入的数据量来分配reducer的个数,我们可以通过参数hive.exec.reducers.bytes.per.reducer
来设置每个reducer的数据量大小,默认是1G,将该值调大,可以减少reducer的数量,调小,可以增加 reducer的数量。
Reducer设置的原则:
每个Reduce处理的数据默认是256MB:hive.exec.reducers.bytes.per.reducer=256000000
每个任务最大的reduce数,默认为1009:hive.exec.reducers.max=1009
计算reduce数的公式:N=min(参数2,总输入数据量/参数1)
设置Reducer的数量:set mapreduce.job.reduces=n
Fetch抓取
explain执行计划: 通过查看执行计划来调整sql语句
防止小文件过多造成资源的多度占用以及影响查询效率
原因:小文件在HDFS中存储本身就会占用过多的内存空间,那么对于MR查询过程中过多的小文件又会造成启动过多的Mapper Task, 每个Mapper都是一个后台线程,会占用JVM的空间
比如:
在Hive中,动态分区会造成在插入数据过程中,生成过多零碎的小文件(请回忆昨天讲的动态分区的逻辑)
不合理的Reducer Task数量的设置也会造成小文件的生成,因为最终Reducer是将数据落地到HDFS中的
解决方案:
①在数据源头HDFS中控制小文件产生的个数,比如:采用Sequencefile作为表存储格式,不要用textfile,在一定程度上可以减少小文件(常见于在流计算的时候采用Sequencefile格式进行存储)
②减少reduce的数量(可以使用参数进行控制)
③慎重使用动态分区,最好在分区中指定分区字段的val值
④最好数据的校验工作,比如通过脚本方式检测hive表的文件数量,并进行文件合并
⑤合并多个文件数据到一个文件中,重新构建表
rowkey的设计原则是根据ASCII字典顺序进行全局排序的,如果rowkey的设计是一序列连续有序的数字,那么hbase region sever就会认为这些数据是在一块的,他就会把这一类型的有序数据全部都放到一个或少数几个节点上,造成少数region server的读/写请求过多、负载过大,而其他region server负载却很小,就造成了“热点”现象
例如将上述的原始Rowkey经过hash处理,此处我们采用md5散列算法取前4位做前缀,结果如下:
9bf0-abc001 (abc001在md5后是9bf049097142c168c38a94c626eddf3d,取前4位是9bf0)
7006-abc002
95e6-abc003
若以前4个字符作为不同分区的起止,上面几个Rowkey数据会分布在3个region中。实际应用场景是当数据量越来越大的时候,
这种设计会使得分区之间更加均衡。
如果Rowkey是数字类型的,也可以考虑Mod方法。
读:
通过rowkey可以快速地位到在那个region上,位置信息保存在hbase的meta表里
读取速度快是因为它使用了LSM树型结构
HBase读取首先会在缓存(BlockCache)中查找,它采用了LRU(最近最少使用算法),如果缓存中没找到,会从内存中的MemStore中查找,只有这两个地方都找不到时,才会加载HFile中的内容
HBase会将数据保存到内存中,在内存中的数据是有序的,如果内存空间满了,会刷写到HFile中,而在HFile中保存的内容也是有序的
实时查询,可以认为是从内存中查询,一般响应时间在1秒内。
写:
HBase的机制是数据先写入到内存中,当数据量达到一定的量(如128M),再写入磁盘中, 在内存中,是不进行数据的更新或合并操作的,只增加数据,这使得用户的写操作只要进入内存中就可以立即返回,保证了HBase I/O的高性能。
偏函数数学概念,它接受一个类型为A的参数,返回一个类型为B的结果
val pf:PartialFunction[Int,String] = {
case 1 => "one"
case 2 => "two"
case 3 => "three"
}
scala> pf(1)
res0:String = one
把一个接受多参数的函数转化成接受其中几个单一参数的函数
不可变,可以包含不同的数据类型,最大长度22
scala源于java,最终被编译成.class文件运行在JVM虚拟机中,本质上还是java,可以互相调用API
scala 在面对编译出现错误时,提供了一个由编译器自我修复的机制,编译器试图去寻找一个隐式的implicit 转换方法,转换出正确的类型,完成编译
trait t(i: Int){}
,这种声明是错误的)class 类似Java中的class
case class 被称为样例类,是一种也输的类,常被用于模式匹配
具体区别:
什么是数据倾斜?
spark中数据倾是指在数据处理的时候,由于spark单个patition中的数据分布不均,导致大量的数据集中不到一台或某几台计算节点上,导致处理速度远低于平均计算速度,数据集中的几台机器很忙,而其他几台数据很少的机器很闲,这样影响整个集群计算性能。
数据倾斜的产生?
在spark中当一个RDD的数据需要被多个子RDD所使用的时候,我们需要进行shuffle将数据打散,把数据均匀的分配给子RDD进行并行计算,Shuffle过程中spark默认使用 HashPartitioner 对数据进行分区,在这个过程中可能由于我们的数据分布不均,我们在进行hash取摸的时候,并行度设置不足,导致多数据分配到一个task上,导致倾斜,或者就是相同key的数据hash取摸之后就是比较大,分配同一个task导致数据倾斜等,对于这行情况我们分以下场景进行解决
一般产生数据倾斜有两种表现形式:
JVM Out Of Memory
,内存溢出了,task failed,反复执行总是会挂掉,这时就基本可以判断是由数据倾斜引起的,因为某一个分区大量数据堆积,task每处理一条数据,就要创建大量的对象,导致内存溢出了。首先要定位数据倾斜
可以通过Spark日志服务器xxxxx:18088者yarn的UI进入到应用xxxx:8088,进入相应的Spark UI界面,查看stage,如果看到某一个stage执行的时间很长,就可以判定数据倾斜的这个stage
这时可以去log日志查看哪一行代码,导致了OOM异常;或者查看log中,执行到了第几个stage,从而判断是哪个suffle算子,一般常规的suffle算子如 reduceByKey、countByKey、groupByKey、join 等,根据代码逻辑判断是否会出现数据倾斜
比较通用的方法,提高reduce并行度
比如说,rduceBykey中有一个shuffle read task的值默认为200,一次用两百个task来处理任务,那么我设置reduceBYkey(1000),这个时候task的数量就多了,然后分配到每个task中的key就少了,从而减少每个task处理的数据量,避免oom。
但是他有一点的局限性,map 端不断地写入数据,reduce task 不断地从指定位置读取数据,如果 task 很多,读取的速度就会增加,但是每个 key 对应的 reduce 处理的总量没变,所以它并没有从根本上解决数据倾斜的问题,只是尽量去减少 每个task中的key的数据量,适用于 较多 key 对应的数据量都很大的问题;
对于 聚合类的shuffle 操作,可以使用随机 key 进行双重聚合
由于一个 key 对应的数据量太大,我先给这个 key 加个随机数,num_key,强行把一个 key 变成 多个 key,这样每个 key 的数据量减小,然后按 num_key 进行聚合,聚合之后,把 num_key 再转回 key,然后对 key 再次聚合;
这种方法适合于 reduceByKey、groupByKey 等算子,不适合 join 算子
对于join算子,将 reduce join 变成 map join
正常情况下,join 会产生 shuffle 过程,而且是 reduce join,即先将相同 key 对应的 value 汇聚到一个 reduce task 中,再进行 join
这种方法是有假定的前提的条件的,比如有两个rdd进行join操作,其中一个rdd的数据量不是很大,比如低于1个G的情况,就可以采用 广播小 RDD + map 大 RDD 实现 join 功能,
具体操作是就是选择两个rdd中那个比较数据量小的,然后我们把它拉到driver端,再然后通过广播变量的方式给他广播出去,这个时候再进行join 的话,因为数据都是在同一Executor中,此时再对另外一个map类算子执行map操作,连接两个rdd中key相同的值,这当中不会产生shuffle的过程,也就避免了数据倾斜,这时数据就可以按照需要的方式去处理
object MapJoinTest {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local").setAppName("WordCount")
val sc = new SparkContext(conf)
val lista=Array(
Tuple2("001","令狐冲"),
Tuple2("002","任盈盈")
)
//数据量小一点
val listb=Array(
Tuple2("001","一班"),
Tuple2("002","二班")
)
val listaRDD = sc.parallelize(lista)
val listbRDD = sc.parallelize(listb)
//val result: RDD[(String, (String, String))] = listaRDD.join(listbRDD)
//设置广播变量
val listbBoradcast = sc.broadcast(listbRDD.collect())
listaRDD.map( tuple =>{
val key = tuple._1
val name = tuple._2
val map = listbBoradcast.value.toMap
val className = map.get(key)
(key,(name,className))
}).foreach( tuple =>{
println("班级号"+tuple._1 + " 姓名:"+tuple._2._1 + " 班级名:"+tuple._2._2.get)
})
}
}
当然了,这种方法也是有缺陷的,比如两个rdd都非常大,比如超过了10个G,这个时候我们就不能用这种方法了,因为数据量太大了,广播变量还是需要太大的消耗,这时就不合适了。
产生的原因?
解决办法?
宽依赖往往对应shuffle操作,需要在运行过程中将同一个父RDD的分区传入到不同的子RDD分区中,中间可能涉及多个节点之间的数据传输;
而窄依赖是指每个父RDD的分区只会传入到一个子RDD分区中,通常可以在一个节点内完成转换
当RDD分区丢失时,对于窄依赖来说,由于父RDD的一个分区只对应一个子RDD分区,这样只需要重新计算与子RDD分区对应的父RDD分区就行。这个计算对数据的利用是100%的
当RDD分区丢失时,对于宽依赖来说,重算的父RDD分区只有一部分数据是对应丢失的子RDD分区的,另一部分就造成了多余的计算。宽依赖中的子RDD分区通常来自多个父RDD分区,极端情况下,所有父RDD都有可能重新计算。
窄依赖的函数有:map, filter, union, join(父RDD是hash-partitioned ), mapPartitions, mapValues
宽依赖的函数有:groupByKey, join(父RDD不是hash-partitioned ), partitionBy
如果要对一个RDD进行持久化,只要对这个RDD调用cache()和persist()即可
val rdd1 = sc.textFile("hdfs://192.168.0.1:9000/hello.txt").cache()
rdd1.map(...)
rdd1.reduce(...)
val rdd1 = sc.textFile("hdfs://192.168.0.1:9000/hello.txt").persist(StorageLevel.MEMORY_AND_DISK_SER)
rdd1.map(...)
rdd1.reduce(...)
spark架构主要由以下组件构成:
rdd有多少个partition,就会有多少个task,因为一个task只处理一个partition上的数据
Master,Worker 是物理节点
Driver,Executor 是进程
spark-submit --master yarn --deploy-mode cluster --class org.apache.spark.examples.SparkPi …/examples/jars/spark examples_2.11-2.3.1.jar 10000
执行流程
spark-submit --master yarn --deploy-mode client --class org.apache.spark.examples.SparkPi …/examples/jars/spark examples_2.11-2.3.1.jar 10000
Executor-cores ---- 默认1 建议2-5 使用3
Num-executors ---- 启动executors的数量,默认2
Executor-memory ---- executor内存大小 默认1G
Driver-cores ---- driver使用内核数, 默认1
Driver-memory ---driver内存大小,默认512M
两者都是用mr模型来进行并行计算
RDD是 弹性分布式数据集,代表一个不可变、可分区、并行计算的元素集合
RDD五大特性:
场景:
checkpoint的意思就是建立检查点,类似于快照,比如说spark某次计算流程DAG特别长,服务器需要将整个DAG计算完成得出结果,但是在这当代中突然出现数据丢失了,spark又会根据RDD的依赖关系从头到尾计算一遍,这样就很耗时间。
这时可以通过中间的计算结果通过 cache 或者 persist 放到内存或者磁盘中,但是这样也不能保证数据完全不会丢失,存储的这个内存出问题了或者磁盘坏了,也会导致spark从头再根据RDD计算一遍,
这时更保险的措施就是,设置checkpoint,其中checkpoint的作用就是将DAG中比较重要的中间数据做一个检查点将结果存储到一个高可用的地方(通常这个地方就是HDFS里面)
原理:
Hashshuffle sortshuffle(默认)
使用广播变量,每个Executor的内存中,只留一份副本,不是对每个task都传输一次大变量,省去很多传输时间。
使用场景: 使用map join 代替reduce join 把小的数据集广播到各个节点上,节省昂贵的shuffle操作
Driver上有一张数据量小的表,其他节点上的task都需要lookup这张表,那么driver可以先把这张表copy到这些节点,这样task就可以在本地查表了
Spark作为分布式的计算框架,最为影响其执行效率的地方就是频繁的网络传输。所以一般的,在不存在数据倾斜的情况下,想要提高Spark job的执行效率,就尽量 减少job的shuffle过程 (减少job的stage),或者减小shuffle带来的影响。
RDD不支持sparksql操作,DataFrame与Dataset均支持sparksql的操作
DataFrame关心的是数据的结构,Dataset关心的是数据的类型
DataFrame与RDD和Dataset不同,DataFrame每一行的类型固定为Row,只有通过解析才能获取各个字段的值
DataFrame与Dataset一般不和spark ml同时使用
DataFrame与Dataset支持一些特别方便的保存方式,如csv,可以带上表头
相互转换:
①:RDD -> Dataset
val ds = rdd.toDS()
②:RDD -> DataFrame
val df=rdd.toDF()
③:Dataset -> RDD
val rdd = ds.rdd
④: Dataset -> DataFrame
val df = ds.toDF()
⑤:DataFrame -> RDD
val rdd = df.rdd
⑥:DataFrame -> Dataset
val ds = df.as[T]
Catalyst 的目标是将逻辑计划转化为物理计划,执行过程可简单概述为:
SQL语句翻译成语法树 —> 生成逻辑计划 —> 优化逻辑计划 —> 在投影上检查过滤器 —> 检查过滤器是否可以下压 —>合并project —> 生成物理计划
Catalyst优化器常见的三种优化规则:谓词下推、常量累加、列剪枝
RegisterTempTable不是action算子,不发生缓存
Join和sql中的inner join相似,返回结果是前面一个集合和后面一个集合中匹配成功的,过滤掉关联不上的
leftJoin类似于sql中的left outer join,返回结果是以第一个RDD为主,关联不上的记录为空
查看日志,先分析看看报什么错误
常见的报错有:心跳超时、KafkaUtil类在消费消息时发生OOM、netty请求无响应等等,节点被关闭的直接原因大都是RECEIVED SIGNAL TERM,这一系列的报错都指向了内存占用的问题。
先分析原因,其次要想重启的话,可以用一些 azkaban,oozie这些任务调度工具,他可以实时的监控应用程序的状态、任务提交、应用程序实时重启等,也可以通过原始的通过脚本来监控应用的状态。
例如可以写一个脚本,让他每隔10分钟检查一次状态,如果程序不在运行,则重启
#!/bin/bash
# check spark app state
# if not running, restart spark app
#
# */10 * * * * /opt/spark/autorestart-sparkapp.sh 2>&1
#
basePath=$(pwd)
proxyUser="用户名,提交程序的应用名,免得把别人的程序kill掉了"
# 下面规定多几个匹配条件,就是匹配误杀的。
applicationType="SPARK"
appStatus="RUNNING"
# 日志临时目录
logDir=/tmp/aaaaaaaalog
initLockFile=${basePath}/autoInitSparkApp.lock
isNeedInit=false
nowDate=$(date "+%Y%m%d")
nowTime=$(date "+%H%M")
# wait app seconds
maxStartAppWait=600
if [ ! -e ${initLockFile} ]&&[[ ${nowTime} < "0200" ]]; then
isNeedInit=true
elif [ -e ${initLockFile} ];then
initDate=$(cat ${initLockFile})
if [ X${initDate} != X${nowDate} ]&&[[ ${nowTime} < "0200" ]]; then
isNeedInit=true
fi
fi
if [ ! -d "$logDir" ] ; then
mkdir $logDir
fi
# 用于临时存储spark 应用列表
jobListFile=/tmp/aaalog/jobList.txt
# aaaaaa之类代表应用名称,匹配误杀,多个判断条件安全一点
allAppNames=("aaaaaa" "bbbbbb" "cccccc")
yarn application -list 2>/dev/null|awk '{print $0;}'|grep -E "aaaaaa|bbbbbb|cccccc" > ${jobListFile}
declare isRunning=false
for idx in $(seq 0 ${#allAppNames[@]}) ;
do
appName=${allAppNames[$idx]}
isRunning=false;
jobId=""
if [ -z $appName ];then
continue;
fi
while read line
do
jobId=$(echo $line|awk '{print $1;}');
jobName=$(echo $line|awk '{print $2;}');
appType=$(echo $line|awk '{print $3;}');
user=$(echo $line|awk '{print $4;}');
queue=$(echo $line|awk '{print $5;}');
jobStatus=$(echo $line|awk '{print $6;}');
if [ ! -z $appName ]&&[ "$appName" == "$jobName" ]&&[ "$proxyUser" = "$user" ]&&[ "$appType" = "$applicationType" ]&&[ "$appStatus" = "$jobStatus" ];then
isRunning=true
break;
elif [ ! -z $appName ]&&[ "$appName" == "$jobName" ]&&[ "$proxyUser" = "$user" ]&&[ "$appType" = "$applicationType" ];then
isRunning=false
break;
else
jobId=""
jobName=""
jobStatus=""
isRunning=false
fi
done < $jobListFile
if [ $isRunning = true ];then
echo "Spark application "$appName" is running!"
if [ ${isNeedInit} = true ]&&[ ! -z $jobId ];then
yarn application -kill $jobId
fi
jobId=""
else
finishTime=0;
timeDiff=0;
if [ ! -z $jobId ];then
finishTime=`yarn application -status $jobId 2>/dev/null | grep "Finish-Time" | awk '{print $NF}'`
if [ "$finishTime" -eq 0 ]; then
timeDiffExpr=`date +%s`-`yarn application -status $jobId 2>/dev/null | grep "Start-Time" | awk '{print $NF}' | sed 's!$!/1000!'`
timeDiff=`echo $timeDiffExpr|bc`
# wait for $maxStartAppWait time maybe allays accept status
if [ "$timeDiff" -gt "$maxStartAppWait" ];then
yarn application -kill $jobId
sleep 15s
else
continue;
fi
fi
fi
if [ x"$appName" == x"${allAppNames[0]}" ];then
echo "Spark Submit $appName to Cluster!!"
# aaaaa 的应用提交脚本
nohup sh $basePath/run-aaaaaa-cluster.sh 2>&1 >/dev/null &
elif [ x"$appName" == x"${allAppNames[1]}" ];then
echo "Spark Submit $appName to Cluster!!"
# bbbbbb的应用提交脚本
nohup sh $basePath/run-bbbbbb-cluster.sh 2>&1 >/dev/null &
elif [ x"$appName" == x"${allAppNames[2]}" ];then
echo "Spark Submit $appName to Cluster!!"
# cccccc的应用提交脚本
nohup sh $basePath/run-cccccc-cluster.sh 2>&1 >/dev/null &
fi
sleep 30s
fi
done
if [ ${isNeedInit} = true ]; then
# delete checkpoint directory
hadoop fs -rm -r -f /tmp/aaaaaa/checkpoint
hadoop fs -rm -r -f /tmp/bbbbbb/checkpoint
echo ${nowDate} > ${initLockFile}
fi
程序挂掉了,防止数据丢失,就是可以用CheckPoint,重启后根据checkpoin进行恢复
Receiver的方式
Receiver方式为确保零数据丢失,必须在Spark Streaming中另外启用预写日志(Write Ahead Logs)。这将同步保存所有收到的Kafka数据到分布式文件系统(例如HDFS)上,以便在发生故障时可以恢复所有数据。
Direct的方式
Direct方式依靠checkpoint机制来保证。每次streaming 消费了kafka的数据后,将消费的kafka offsets更新到checkpoint。当你的程序挂掉或者升级的时候,就可以接着上次的读取,实现数据的零丢失。
窗口函数就是在原来定义的SparkStreaming计算批次大小的基础上再次进行封装,每次计算多个批次的数据,同时还需要传递一个滑动步长的参数,用来设置当次计算任务完成之后下一次从什么地方开始计算。
他有两个参数,窗口长度和滑动间隔。比如说我设置窗口长度为8s,滑动间隔为4s,这样就是表示每隔4s计算最近8s的数据。并且 窗口长度和滑动间隔这两个参数都必须是批处理间隔的整数倍。
Spark Streaming 中 checkpoint 两种类型的数据:
Metadata(元数据) checkpointing : 保存定义了 Streaming 计算逻辑至类似 HDFS 的支持容错的存储系统。用来恢复 driver,元数据包括:
配置 - 用于创建该 streaming application 的所有配置
DStream 操作 - DStream 一些列的操作
未完成的 batches - 那些提交了 job 但尚未执行或未完成的 batches
Data checkpointing : 保存已生成的RDDs至可靠的存储,所有接收的数据通过receivers写入HDFS或者S3中checkpoint目录,这样当driver失败后,executor中数据丢失后,可以通过checkpoint恢复
激活 checkpoint ?
启用 checkpoint,需要设置一个支持容错 的、可靠的文件系统(比如 HDFS)目录来保存 checkpoint 数据。通过调用 streamingContext.checkpoint(checkpointDirectory) 来完成。另外,如果你想让你的 application 能从 driver 失败中恢复,你的 application 要满足:
1.说明
SparkStreaming的一般是7天24小时不停息的运行,而在运行的时候,中间会有很多的状态,而有些状态我们需要一些操作,比如累计,更新或者其他的操作。那么如何将这些独立的状态联系起来就成了一种迫切的需求。
2.介绍
UpdateStateByKey的主要功能:
1、为Spark Streaming中每一个Key维护一份state状态,state类型可以是任意类型的, 可以是一个自定义的对象,那么更新函数也可以是自定义的。
2、通过更新函数对该key的状态不断更新,对于每个新的batch而言,Spark Streaming会在使用updateStateByKey的时候为已经存在的key进行state的状态更新。
注意
使用到updateStateByKey要开启checkpoint机制和功能。
多久会将内存中的数据写入到磁盘一份?
如果batchInterval设置的时间小于10秒,那么10秒写入磁盘一份。如果batchInterval设置的时间大于10秒,那么就会batchInterval时间间隔写入磁盘一份
3.代码说明
这里是以单词统计为例
import org.apache.hadoop.mapred.lib.HashPartitioner
import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}
object Streaming_demo {
def main(args: Array[String]): Unit = {
// 创建conf
val conf = new SparkConf().setMaster("local[2]").setAppName("streaming_demo")
// 创建SparkStreamingContext
val ssc = new StreamingContext(conf, Seconds(3))
// 设置日志
ssc.sparkContext.setLogLevel("WARN")
// 设置检查点,checkpoint
ssc.checkpoint("/Users/ricky/Desktop/chekpoint")
// 使用Socket流产生数据
val lines = ssc.socketTextStream("localhost",9999)
// 切分数据
val words = lines.flatMap(_.split(" "))
// 遍历数据
val word = words.map(word=>{
(word,1)
});
// 使用UpdateStateByKey进行更新
val result = word.updateStateByKey((seq:Seq[Int],option:Option[Int])=>{
// 初始化一个变量
var value = 0;
// 该变量用于更新,加上上一个状态的值,这里隐含一个判断,如果有上一个状态就获取,如果没有就赋值为0
value += option.getOrElse(0)
// 遍历当前的序列,序列里面每一个元素都是当前批次的数据计算结果,累加上一次的计算结果
for(elem <- seq){
value +=elem
}
// 返回一个Option对象
Option(value)
})
// 累加,打印
val wordcount1 = result.reduceByKey(_ + _)
wordcount1.print()
// 启动SparkStreaming,设置阻塞
ssc.start()
ssc.awaitTermination()
}
}
channel
File channel相对于memory channel,无数据丢失风险,存储容量大。
File channel 数据存储路径可以配置多磁盘文件路径,通过磁盘并行写入提高file channel性能。配置maxfilesize设置数据文件大小,当写入的文件大小达到上限,flume会重新创建新的文件存储写入event。配置dataDirs指向多个路径,每个路径对应不同的硬盘,增大Flume吞吐量。
Memory channel:读取速度快,存储量小,flume进程挂掉,服务器重启会导致数据丢失
Kafka channel :容量大,容错能力强,在日志收集层只配置Source组件和Kafka组件,不配置Sink组件,减少日志收集层启动的进程数,有效降低服务器内存、磁盘等资源的使用;
日志汇聚层,只配置kafka channel和sink,不需要配置Source
Sink
Hdfs sink:长期存储大量数据,写入hdfs文件
Kafka sink:flume通过kafkasink 将event写入kafka主题,其他应用通过订阅主题消费数据。
Source将event写入channel之前可以使用拦截器对event进行各种形式的处理,source和channel之间可以有多个拦截器,不同拦截器使用不同规则处理event,包括时间、主机、UUID、正则表达式等多种形式的拦截器
提高整个系统的容错能力和稳定性。设置sink组,同一个sink组有多个sink,不同sink之间可以配置成负载均衡或者故障转移
数据传输量很大的情况下,用来接收数据做消峰处理,解耦生产者和消费者,缓存数据
① 消息丢失解决方案:
acks = all
,即需要相应的所有处于ISR的分区都确认收到该消息后,才算数据发送成功② 消息重复解决方案
消息使用唯一id标识
生产者 ack = all
代表至少成功发送一次
消费者 offset手动提交,业务逻辑处理成功后,提交 offset
选择唯一主键存储到redis中,先查询是否存在,存在不处理;不存在,先插入redis中,在进行业务处理
保证数据完整性:
在Spark Streaming中 开启预写日志(Write Ahead Logs),同步保存所有收到的Kafka数据到分布式文件系统(例如HDFS)上,以便在发生故障时可以恢复所有数据。
Direct 方式
使用 checkpoint 机制,每次streaming 消费了kafka的数据后,将消费的kafka offsets更新到checkpoint。当你的程序挂掉或者升级的时候,就可以接着上次的读取,实现数据的零丢失。
两种情况可能出现重复消费:
当ack=-1时,如果在follower同步完成后,broker发送ack之前,leader发生故障,导致没有返回ack给Producer,由于失败重试机制,又会给新选举出来的leader发送数据,造成数据重复。
(手动管理offset时,先消费后提交offset)消费者消费后没有commit offset(程序崩溃/强行kill/消费耗时/自动提交偏移情况下unscrible)
(手动管理offset时,先提交offset后消费)先提交offset,后消费,有可能造成数据的重复
如果先提交offset,后消费,可能会出现数据漏消费问题。比如,要消费0,1,2,我先提交offset ,此时__consumer_offsets的值为4,但等我提交完offset之后,还没有消费之前,消费者挂掉了,这时等消费者重新活过来后,读取的__consumer_offsets值为4,就会从4开始消费,导致消息0,1,2出现漏消费问题。
当ack=0时,producer不等待broker的ack,这一操作提供了一个最低的延迟,broker一接收到还没有写入磁盘就已经返回,当broker故障时有可能丢失数据;
当ack=1时,producer等待broker的ack,partition的leader落盘成功后返回ack,如果在follower同步成功之前leader故障,而由于已经返回了ack,系统默认新选举的leader已经有了数据,从而不会进行失败重试,那么将会丢失数据
保证数据不重复消费:
kafka提供了两套comsumer API: The high-level Consumer API 和 The Simple Consumer API
The high-level Consumer API : 提供了一个从kafka消费数据的高层抽象,有 consumer group 的语义,一个消息只能被group内的一个consumer所消费,且consumer消费消息时不关注 offset,最后一个offset由zookeeper保存
注: The high-level Consumer API 可以使 多线程 的应用,需要注意的是:
(1)如果消费线程大于patition数量,则有些线程接收不到消息
(2)如果patition数量大于线程数,则有些线程无法收到多个patition的消息
The Simple Consumer API : 需要开发人员更多的关注细节,如果想要对 partition 有更多的控制权,应该使用The Simple Consumer API ,它可以实现:
多次读取一个消息
只消费一个partition中的部分消息
使用事务来保证一个消息仅被消费一次
注: 使用 he Simple Consumer API 需要做一些额外工作:
(1)必须在应用程序中跟踪offset,从而确定下一条应该消费哪条消息
(2)应用程序需要通过程序获取每个partition的leader是谁
(3)需要处理leader的变更
总的来说,The Simple Consumer API 相较于 The high-level Consumer API 需要手动维护
①保证数据一致性:
设置 acks = all
对于leader新收到的msg,client不能立即消费,等待消息被所有 ISR 中的副本 replica 同步后,更新 HW-HighWaterMark,此时消息才能被client消费,这样就保证了如果leader fail,该消息仍然可以从新选举的leader中获取
②保证数据可靠性
当producer向leader发送数据时,可以通过 acks 参数设置数据可靠性的级别
acks = 0
:不论写入成功与否,server不需要给 producer 发送response,如果发生异常,server会终止连接,出发producer更新meta数据acks = 1
:leader写入成功后即发送response,此种情况下如果leader fail ,会丢失数据acks = -1
:等待所有 ISR 接收到消息再给 producer 发送 response ,这样安全性最强应该措施:
spark.streaming.concurrentJobs=10
:提高Job并发数sparl.streaming.kafka.maxRatePerPartition = 20000
:设置每秒每个分区最大获取日志数,控制处理数据量,保证数据处理均匀spark.streaming.kafka.maxRetries = 50
:获取topic 分区leaders 及最新offsets时,调大重试次数kafka的acks参数有一个非常重要的作用。如果
写:
读:
保证数据一致性,只与leader进行数据交互,其他follower从leader中备份数据
kafka中有两种 保留策略 :
Broker服务器上(0.9版本之后),默认将消费的offset迁入到kafka _consumer_offsets Topic中
kafka只能保证一个partition中的消息被某个 consumer 消费时时顺序的,实时上,从topic的角度来说,当有多个partition时,消息仍然不是全局有序的,也就是说,要想做到全局有序,需要设置成只有一个partition
分区数并不是越多越好,一般分区数不要超过集群机器数量。分区数越多占用内存越大(ISR等),一个节点集中的分区也就越多,当它宕机的时候,对系统的影响也就越大
结合项目来说,一般数据量100G以内,10台机器,3台kafka节点,分区数3-10左右
副本数的设定,一般设置成2-3个,很多企业设置成2个
kafka内部存在两种默认的分区分配策略: Range 和 RoundRobin
假设每天总数量100G,每天产生1亿条日志,100000 万 / 24/60/60 = 1150条/每分钟
如果是kafka消费能力不足,则可以考虑增加topic的分区数,并且同时提升消费组的消费者数量,消费者数 = 分区数
如果是下游的数据处理不及时:提高每批次拉取的数量。批次拉取数据过少(拉取数据处理时间 < 生产速度),处理的数据小于生产的数据,也会造成数据积压
开源的监控器有: KafkaManager、 KafkaMonitor
可以将这个大文件拆分,然后内部排序,快速排序,最后在归并
计算每个训练样例到待分类样品的距离,取与样例距离最近的样品,则待分类的样例就属于这个样品。
算法步骤为:
计算未知实例到所有已知实例的距离;
选择参数 K;
根据多数表决( majority-voting )规则,将未知实例归类为样本中最多数的类别。
距离的衡量方法:欧氏距离,这种测量方式就是简单的平面几何中两点之间的直线距离。
将一组对象集合按他们的相似程度划分成多个类或者簇
回归就是用两个变量来拟合一条最佳的直线,可以用一个属性来预测另外一个属性。