Spark RDD案例(四)IP和地理位置映射统计
1. 背景
- Spark作为大数据分析引擎,本身可以做离线和准实时数据处理
- Spark抽象出的操作对象如RDD、dataSet、dataFrame、DStream等都是高层级的抽象,屏蔽了分布式数据处理代码细节,操作分布式数据和处理就像使用scala集合接口一样便利。这样可以很大降低编程使用和理解门槛。
- 在实际生产中,大数据处理面临的业务需求和正常java 业务需求一样,都是基于数据做处理。不同的是正常java业务数据相对较少,如mysql中适合存储的数据是小而美的如500万行数据及以下,而大数据存储500万行才达到海量数据存储的门槛。
- 实际生产中,大数据和小批量Java数据处理需求往往类似,如需要根据已知规则确定手中数据的对应信息,涉及到一个查找过程。在sql中一般是join的方式来将信息匹配映射,但是在spark中一般采用逐条处理并从已知规则中匹配方式
- 当所需要查找的规则数据集合相对小时,一般采用广播变量将数据分发到各个executor上,这样类似于mapreduce的分布式缓存,可以降低大量的数据传输和处理损耗。
2. 案例
- 需求
需要将日志中ip地址提取出来,查找出ip对应的省信息,并作次数统计 - 数据
- ip规则数据
1.0.1.0|1.0.3.255|16777472|16778239|亚洲|中国|福建|福州||电信|350100|China|CN|119.306239|26.075302
1.0.8.0|1.0.15.255|16779264|16781311|亚洲|中国|广东|广州||电信|440100|China|CN|113.280637|23.125178
1.0.32.0|1.0.63.255|16785408|16793599|亚洲|中国|广东|广州||电信|440100|China|CN|113.280637|23.125178
1.1.0.0|1.1.0.255|16842752|16843007|亚洲|中国|福建|福州||电信|350100|China|CN|119.306239|26.075302
1.1.2.0|1.1.7.255|16843264|16844799|亚洲|中国|福建|福州||电信|350100|China|CN|119.306239|26.075302
1.1.8.0|1.1.63.255|16844800|16859135|亚洲|中国|广东|广州||电信|440100|China|CN|113.280637|23.125178
1.2.0.0|1.2.1.255|16908288|16908799|亚洲|中国|福建|福州||电信|350100|China|CN|119.306239|26.075302
1.2.2.0|1.2.2.255|16908800|16909055|亚洲|中国|北京|北京|海淀|北龙中网|110108|China|CN|116.29812|39.95931
1.2.4.0|1.2.4.255|16909312|16909567|亚洲|中国|北京|北京||中国互联网信息中心|110100|China|CN|116.405285|39.904989
1.2.5.0|1.2.7.255|16909568|16910335|亚洲|中国|福建|福州||电信|350100|China|CN|119.306239|26.075302
1.2.8.0|1.2.8.255|16910336|16910591|亚洲|中国|北京|北京||中国互联网信息中心|110100|China|CN|116.405285|39.904989
1.2.9.0|1.2.127.255|16910592|16941055|亚洲|中国|广东|广州||电信|440100|China|CN|113.280637|23.125178
1.3.0.0|1.3.255.255|16973824|17039359|亚洲|中国|广东|广州||电信|440100|China|CN|113.280637|23.125178
1.4.1.0|1.4.3.255|17039616|17040383|亚洲|中国|福建|福州||电信|350100|China|CN|119.306239|26.075302
1.4.4.0|1.4.4.255|17040384|17040639|亚洲|中国|北京|北京|海淀|北龙中网|110108|China|CN|116.29812|39.95931
1.4.5.0|1.4.7.255|17040640|17041407|亚洲|中国|福建|福州||电信|350100|China|CN|119.306239|26.075302
1.4.8.0|1.4.127.255|17041408|17072127|亚洲|中国|广东|广州||电信|440100|China|CN|113.280637|23.125178
1.8.0.0|1.8.255.255|17301504|17367039|亚洲|中国|北京|北京|海淀|北龙中网|110108|China|CN|116.29812|39.95931
- 日志数据
20090121000132095572000|125.213.100.123|show.51.com|/shoplist.php?phpfile=shoplist2.php&style=1&sex=137|Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; Mozilla/4.0(Compatible Mozilla/4.0(Compatible-EmbeddedWB 14.59 http://bsalsa.com/ EmbeddedWB- 14.59 from: http://bsalsa.com/ )|http://show.51.com/main.php|
20090121000132124542000|117.101.215.133|www.jiayuan.com|/19245971|Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; TencentTraveler 4.0)|http://photo.jiayuan.com/index.php?uidhash=d1c3b69e9b8355a5204474c749fb76ef|__tkist=0; myloc=50%7C5008; myage=2009; PROFILE=14469674%3A%E8%8B%A6%E6%B6%A9%E5%92%96%E5%95%A1%3Am%3Aphotos2.love21cn.com%2F45%2F1b%2F388111afac8195cc5d91ea286cdd%3A1%3A%3Ahttp%3A%2F%2Fimages.love21cn.com%2Fw4%2Fglobal%2Fi%2Fhykj_m.jpg; last_login_time=1232454068; SESSION_HASH=8176b100a84c9a095315f916d7fcbcf10021e3af; RAW_HASH=008a1bc48ff9ebafa3d5b4815edd04e9e7978050; COMMON_HASH=45388111afac8195cc5d91ea286cdd1b; pop_1232093956=1232468896968; pop_time=1232466715734; pop_1232245908=1232469069390; pop_1219903726=1232477601937; LOVESESSID=98b54794575bf547ea4b55e07efa2e9e; main_search:14469674=%7C%7C%7C00; registeruid=14469674; REG_URL_COOKIE=http%3A%2F%2Fphoto.jiayuan.com%2Fshowphoto.php%3Fuid_hash%3D0319bc5e33ba35755c30a9d88aaf46dc%26total%3D6%26p%3D5; click_count=0%2C3363619
20090121000132406516000|117.101.222.68|gg.xiaonei.com|/view.jsp?p=389|Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; CIBA)|http://home.xiaonei.com/Home.do?id=229670724|_r01_=1; __utma=204579609.31669176.1231940225.1232462740.1232467011.145; __utmz=204579609.1231940225.1.1.utmccn=(direct)
- 环境准备
- idea 202
- mave 3.6.3
- pom文件
<!-- 定义了一些常量 -->
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<scala.version>2.12.10</scala.version>
<spark.version>3.0.1</spark.version>
<hbase.version>2.2.5</hbase.version>
<hadoop.version>3.2.1</hadoop.version>
<encoding>UTF-8</encoding>
</properties>
<dependencies>
<!-- 导入scala的依赖 -->
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
<version>${scala.version}</version>
<!-- 编译时会引入依赖,打包是不引入依赖 -->
<!-- <scope>provided</scope>-->
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.12</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-core_2.12</artifactId>
<version>${spark.version}</version>
<!-- 编译时会引入依赖,打包是不引入依赖 -->
<!-- <scope>provided</scope>-->
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.73</version>
</dependency>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<!-- 编译scala的插件 -->
<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
<version>3.2.2</version>
</plugin>
<!-- 编译java的插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
<executions>
<execution>
<id>scala-compile-first</id>
<phase>process-resources</phase>
<goals>
<goal>add-source</goal>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>scala-test-compile</id>
<phase>process-test-resources</phase>
<goals>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- 打jar插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.4.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
- jdk 1.8
- scala 2.12.12
- 代码
package com.doit.day07
import org.apache.spark.broadcast.Broadcast
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
import scala.collection.mutable.ArrayBuffer
/*
* 有一堆log日志文件,其中包含ip地址
* 有一个ip地址和位置信息规则文件
*
* 需求是希望可以统计出每条日志ip的实际位置信息,以及对应次数。地理位置精确到省份即可
* */
object IPLocation1 {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("IPLocation1")
.setMaster("local[*]")
val sc = new SparkContext(conf)
/*
* 1.2.0.0|1.2.1.255|16908288|16908799|亚洲|中国|福建|福州||电信|350100|China|CN|119.306239|26.075302
* */
// 先读取ip规则文件,转换,
val rdd1: RDD[String] = sc.textFile("E:\\DOITLearning\\12.Spark\\ip_location_dict.txt")
// 对ip规则数据做转换,注意要去重,防止重复数据
val ipRulesInDriver: Array[(Long, Long, String, String)] = rdd1.map(line => {
val strings: Array[String] = line.split("\\|")
val startIP = strings(2).toLong
val endIP = strings(3).toLong
val provinceStr: String = strings(6)
// 为了方便排序和查找,转为long类型,然后是省份信息
(startIP, endIP, provinceStr, strings(7))
}).sortBy(_._1).collect()
println("ipRulesInDriver: " + ipRulesInDriver.toBuffer)
// 将去重,排序之后的ip规则信息设置为广播变量
val broadcastRefInDriver: Broadcast[Array[(Long, Long, String, String)]] = sc.broadcast(ipRulesInDriver)
/*
* 20090121000132095572000|125.213.100.123|show.51.com
* |/shoplist.php?phpfile=shoplist2.php&style=1&sex=137
* |Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; Mozilla/4.0(Compatible Mozilla/4.0(Compatible-EmbeddedWB 14.59 http://bsalsa.com/ EmbeddedWB- 14.59 from: http://bsalsa.com/ )
* |http://show.51.com/main.php|
* */
// 读取日志文件,然后将日志文件中IP解析出来,通过广播变量的ip规则信息,查找到ip对象的省信息
val logRDD: RDD[String] = sc.textFile("E:\\DOITLearning\\12.Spark\\ipaccess.log")
val resRDD: RDD[(String, Int)] = logRDD.map(f = line => {
val strings: Array[String] = line.split("\\|")
val ipStr: String = strings(1)
val ipNumber: Long = IPUtils.ip2Long(ipStr)
val ipRulesInExecutor: Array[(Long, Long, String, String)] = broadcastRefInDriver.value
// 查找这个ip对应的省
val ipIndex: Int = IPUtils.binarySearchIpAndLocation(ipRulesInExecutor, ipNumber)
// 说明找到了
var ipProvince: String = "null"
if (ipIndex > -1) {
ipProvince = ipRulesInExecutor(ipIndex)._3
}
(ipProvince, 1)
}).reduceByKey(_ + _)
println("resRDD: " + resRDD.collect().toBuffer)
sc.stop()
}
}
object IPUtils {
def ip2Long(ip: String): Long = {
val ipFragments: Array[String] = ip.split("\\.")
var ipNumber = 0L
// 其实就可以把ip地址看成是由8位bit组成的数字,刚好最大是255,
// 这时候可以将最开始位置的数字左移24位,第二位左移16位,第三位左移8位,最后一个数字不移动,最后组成起来就是一个32位正整数的bit形式
// return (Long.parseLong(ipFragments[0]) << 24) + (Long.parseLong(ipFragments[1]) << 16) + (Long.parseLong(ipFragments[2]) << 8) + Long.parseLong(ipFragments[3]);
for (i <- 0 until ipFragments.length) {
// 这里相当于第一个ip段循环向左移动了三次,第二个移动了2次,第三个移动了1次,每次都是8bit位
ipNumber = ipFragments(i).toLong | ipNumber << 8L
}
ipNumber
}
// 二分查找,注意前提是数据需要有序,默认是升序排列,本实现也是升序排列
def binarySearchIpAndLocation(infoArray: Array[(Long, Long, String, String)], ip: Long): Int = {
var lowIndex = 0
var highIndex = infoArray.length - 1
while (lowIndex <= highIndex) {
// 中间位置的数组序号
val middleIndex = (lowIndex + highIndex) / 2
// 如果要查找的ip地址刚好在middle序号对应的位置上,则ip段起始整数,ip段结束整数会刚好把这个ip包含起来
if ((ip >= infoArray(middleIndex)._1) && (ip <= infoArray(middleIndex)._2)) {
return middleIndex
} else if (ip < infoArray(middleIndex)._1) {
// 数据在上半部分
highIndex = middleIndex - 1
} else {
// 数据在下半部分
lowIndex = middleIndex + 1
}
}
// 没有找到
-1
}
def binarySearchIpAndLocation(infoArray: ArrayBuffer[(Long, Long, String, String)], ip: Long): Int = {
var lowIndex = 0
var highIndex = infoArray.length - 1
while (lowIndex <= highIndex) {
// 中间位置的数组序号
val middleIndex = (lowIndex + highIndex) / 2
// 如果要查找的ip地址刚好在middle序号对应的位置上,则ip段起始整数,ip段结束整数会刚好把这个ip包含起来
if ((ip >= infoArray(middleIndex)._1) && (ip <= infoArray(middleIndex)._2)) {
return middleIndex
} else if (ip < infoArray(middleIndex)._1) {
// 数据在上半部分
highIndex = middleIndex - 1
} else {
// 数据在下半部分
lowIndex = middleIndex + 1
}
}
// 没有找到
-1
}
}
注意,二分查找的代码,找到middle index符合条件时,记得return 返回,需要让数据返回并且结束方法,否则会一直卡住
整个查找过程中,首先加载ip和省市对应的字典信息,并且采集回driver端,然后使用广播变量方式分发到各个executor端
加载日志数据之后,map方法逐条处理,查找,这时候就发生了闭包,广播变量被闭包捕获,然后map内部代码会以task方式被序列化之后,发送executor执行。
具体源码如下

1780



被折叠的 条评论
为什么被折叠?



