广播变量:通俗的讲(本人自己的理解,如有错误,请多指教)就是一旦处理数据量大,生成大量的Task.Driver需要将变量传递给每一个Task,这样就显得笨拙,低效率.而广播变量------->就是将Driver端的变量分发给executor一份,广播变量,是对Driver端变量的一个拷贝,这个拷贝是只读的,不能修改。这样executor启动的Task就会共享这个变量.节省了通信的成本和服务器的资源.
如何定义一个广播变量?代码如下:
sc.broadcast(变量)
注意:变量一旦定义为广播变量,那么这个变量就只能读,不能修改.
如何还原一个广播变量?代码如下:
broadcast.value
注意事项:
1、能不能将一个RDD使用广播变量广播出去?
不能,因为RDD是不存储数据的。可以将RDD的结果广播出去。
2、 广播变量只能在Driver端定义,不能在Executor端定义。
3、 在Driver端可以修改广播变量的值,在Executor端无法修改广播变量的值。
4、如果executor端用到了Driver的变量,如果不使用广播变量在Executor有多少task就有多少Driver端的变量副本。
5、如果Executor端用到了Driver的变量,如果使用广播变量在每个Executor中只有一份Driver端的变量副本。
案列:
需求:根据访问日志中的Ip地址,访问访客的省份信息.根据Ip
方案一(思路):
①:定义单列对象在静态代码块中加载规则,规则只会加载一次.
②:规则在一个Excutor中的内存中保存一份
③:使用二分查找,提高查找效率
缺点是:每一个Task都需要加载全部的Ip规则,如果Ip规则数据很大,就会很慢.
代码如下:
package day04
import Util.IpUtils
import cn._51doit.spark.day03.IpRulesLoader
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
import scala.collection.mutable.ArrayBuffer
/**
* 根据日志中的Ip查询位置信息
*/
object IpLocation {
def main(args: Array[String]): Unit = {
val isLocal = args(0).toBoolean
//创建SparkConf 然后创建SparkContext
val conf: SparkConf = new SparkConf().setAppName(this.getClass.getSimpleName)
if (isLocal) {
conf.setMaster("local[*]")
}
val sc: SparkContext = new SparkContext(conf)
//创建RDD 先读取日志数据
val lines: RDD[String] = sc.textFile(args(1))
val provinceAndOne: RDD[(String, Int)] = lines.map(line => {
//IpRulesLoader是在Excutor端被真正调用的
val allRules: ArrayBuffer[(Long, Long, String, String)] = IpRulesLoader.getAllRules()
val fields: Array[String] = line.split("[|]")
//获取IP地址
val ip: String = fields(1)
val ipNum: Long = IpUtils.ip2Long(ip)
//查找全部的Ip规则-->注意 应该在这之前就应该把Ip规则加载到exccutor中
val Index: Int = IpUtils.binarySearch(allRules, ipNum)
var province: String = "未知"
if (Index != -1) {
province = allRules(Index)._3
}
(province, 1)
})
//按照省份进行聚合
val result: RDD[(String, Int)] = provinceAndOne.reduceByKey(_ + _)
//将计算好的数据保存到MySQL
println(result.collect().toBuffer)
sc.stop()
}
}
加载Ip规则数据代码如下:
package cn._51doit.spark.day03
import java.io.{BufferedReader, InputStreamReader}
import java.net.URI
import org.apache.hadoop.conf.Configuration
import org.apache.hadoop.fs.{FSDataInputStream, FileSystem, Path}
import scala.collection.mutable.ArrayBuffer
object IpRulesLoader {
//在object中定义的定义的数据是静态的,在一个JVM进程中,只有一份
var IpRules = new ArrayBuffer[(Long, Long, String, String)]()
//加载IP规则数据,在Executor的类加载是执行一次
//静态代码块
{
//读取HDFS中的数据
val fileSystem = FileSystem.get(URI.create("hdfs://qiu01:9000"), new Configuration())
val inputStream = fileSystem.open(new Path("/ip/ip.txt"))
val bufferedReader = new BufferedReader(new InputStreamReader(inputStream))
var line: String = null
do {
line = bufferedReader.readLine()
if (line != null) {
//处理IP规则数据
val fields = line.split("[|]")
val startNum = fields(2).toLong
val endNum = fields(3).toLong
val province = fields(6)
val city = fields(7)
val t = (startNum, endNum, province, city)
IpRules += t
}
} while (line != null)
}
def getAllRules(): ArrayBuffer[(Long, Long, String, String)] = {
IpRules
}
}
二分查找,代码如下:
package Util
import scala.collection.mutable.ArrayBuffer
object IpUtils {
/**
* 将Ip地址转成十进制
*/
def ip2Long(ip: String): Long = {
val fragments = ip.split("[.]")
var ipNum = 0L
for (i <- 0 until fragments.length) {
ipNum = fragments(i).toLong | ipNum << 8L
}
ipNum
}
/**
* 二分查找
*/
def binarySearch(lines: ArrayBuffer[(Long, Long, String, String)], ip: Long): Int = {
var low = 0 //起始
var high = lines.length - 1 //结束
while (low <= high) {
val middle = (low + high) / 2
if ((ip >= lines(middle)._1) && (ip <= lines(middle)._2))
return middle
if (ip < lines(middle)._1)
high = middle - 1
else {
low = middle + 1
}
}
-1 //没有找到
}
def binarySearch(lines: Array[(Long, Long, String, String)], ip: Long): Int = {
var low = 0 //起始
var high = lines.length - 1 //结束
while (low <= high) {
val middle = (low + high) / 2
if ((ip >= lines(middle)._1) && (ip <= lines(middle)._2))
return middle
if (ip < lines(middle)._1)
high = middle - 1
else {
low = middle + 1
}
}
-1 //没有找到
}
}
方案二
***:我们可以使用广播变量的方法,把所有的Ip规则加载到Driver端,在切分成N(Excutor的个数,)个,分给每个Excutor,它们之间通过BT(种子)的方式相互传送数据.Excutor越多,传送的越快,提高效率.代码如下:
package day04
import Util.IpUtils
import org.apache.spark.broadcast.Broadcast
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object IpLocationAdv {
def main(args: Array[String]): Unit = {
val isLocal: Boolean = args(0).toBoolean
//创建SparkConf.然后创建SparkContext
val conf: SparkConf = new SparkConf().setAppName(this.getClass.getSimpleName)
if (isLocal) {
conf.setMaster("local[*]")
}
val sc: SparkContext = new SparkContext(conf)
//读取IP规则数据进行整理
val ipLines: RDD[String] = sc.textFile(args(1))
val ipRulesRDD: RDD[(Long, Long, String, String)] = ipLines.map(line => {
val fields: Array[String] = line.split("[|]")
val startNum: Long = fields(2).toLong
val endNum: Long = fields(3).toLong
val province: String = fields(6)
val city: String = fields(7)
(startNum, endNum, province, city)
}
)
//将全部的Ip规则收集到Driver端
val ipRulesInExecutor: Array[(Long, Long, String, String)] = ipRulesRDD.collect()
//将Driver端全部的ip规则在广播到Executor
//广播完成后,将Executor端的广播的数据的引用返回,以后可以通过这个引用获取到事先广播好的数据
//sc.broadcast是一个阻塞的方法,广播变量没有广播完,不会执行下面的逻辑
//定义一个广播变量 sc.broadcase(变量)
val ipRulesRef: Broadcast[Array[(Long, Long, String, String)]] = sc.broadcast(ipRulesInExecutor)
//处理IP日志数据
val logDD: RDD[String] = sc.textFile(args(2))
//处理要计算的数据并关联事先广播到Executor中的规则
val provinceAndOne: RDD[(String, Int)] = logDD.map(line => {
val fields: Array[String] = line.split("[|]")
val ip: String = fields(1)
val ipNum: Long = IpUtils.ip2Long(ip)
//获取实现广播到Executor的数据呢? 通过Driver的引用获取Executor广播好的数据
//调用二分查找
val index = IpUtils.binarySearch(ipRulesInExecutor, ipNum)
var province = "未知"
if (index >= 0) {
province = ipRulesInExecutor(index)._3
}
(province, 1)
})
//按照省份进行聚合
val result: RDD[(String, Int)] = provinceAndOne.reduceByKey(_ + _)
//建计算结果保存到HDFS中
// result.saveAsObjectFile(args(3))
val r: Array[(String, Int)] = result.collect()
println(r.toBuffer)
//释放资源
sc.stop()
/**
* 结果为:ArrayBuffer((陕西,1824), (河北,383), (云南,126), (未知,1), (重庆,868), (北京,1535))
*/
}
}