This commit is contained in:
罗祥 2019-05-27 18:01:20 +08:00
parent ec5c799eb1
commit 334493f7cb
3 changed files with 130 additions and 1 deletions

View File

@ -166,6 +166,8 @@ Deleting hdfs://hadoop001:8020/spark-streaming/checkpoint-1558945265000
## 三、输出操作
### 3.1 输出API
| Output Operation | Meaning |
| :------------------------------------------ | :----------------------------------------------------------- |
| **print**() | 在运行流应用程序的driver节点上打印DStream中每个批次的前十个元素。用于开发调试。 |
@ -175,8 +177,135 @@ Deleting hdfs://hadoop001:8020/spark-streaming/checkpoint-1558945265000
| **foreachRDD**(*func*) | 最通用的输出方式它将函数func应用于从流生成的每个RDD。此函数应将每个RDD中的数据推送到外部系统例如将RDD保存到文件或通过网络将其写入数据库。 |
### 3.1 foreachRDD
这里我们使用Redis作为客户端对文章开头示例程序进行改变把每一次词频统计的结果写入到Redis,利用Redis的`HINCRBY`命令来进行总次数的统计。相关依赖和实现代码如下:
```xml
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
```
实现代码如下:
```scala
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.DStream
import org.apache.spark.streaming.{Seconds, StreamingContext}
import redis.clients.jedis.Jedis
object NetworkWordCountToRedis {
def main(args: Array[String]) {
val sparkConf = new SparkConf().setAppName("NetworkWordCountToRedis").setMaster("local[2]")
val ssc = new StreamingContext(sparkConf, Seconds(5))
/*创建文本输入流,并进行词频统计*/
val lines = ssc.socketTextStream("hadoop001", 9999)
val pairs: DStream[(String, Int)] = lines.flatMap(_.split(" "))
.map(x => (x, 1)).reduceByKey(_ + _)
pairs.foreachRDD { rdd =>
rdd.foreachPartition { partitionOfRecords =>
var jedis: Jedis = null
try {
jedis = JedisPoolUtil.getConnection
partitionOfRecords.foreach(record => jedis.hincrBy("wordCount", record._1, record._2))
} catch {
case ex: Exception =>
ex.printStackTrace()
} finally {
if (jedis != null) jedis.close()
}
}
}
ssc.start()
ssc.awaitTermination()
}
}
```
其中`JedisPoolUtil`的代码如下:
```java
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
public class JedisPoolUtil {
/* 声明为volatile防止指令重排序 */
private static volatile JedisPool jedisPool = null;
private static final String HOST = "localhost";
private static final int PORT = 6379;
/* 双重检查锁实现懒汉式单例 */
public static Jedis getConnection() {
if (jedisPool == null) {
synchronized (JedisPoolUtil.class) {
if (jedisPool == null) {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(30);
config.setMaxIdle(10);
jedisPool = new JedisPool(config, HOST, PORT);
}
}
}
return jedisPool.getResource();
}
}
```
### 3.3 代码说明
这里将上面输出操作的代码单独抽取出来,并去除异常判断的部分,精简后的代码如下:
```scala
pairs.foreachRDD { rdd =>
rdd.foreachPartition { partitionOfRecords =>
val jedis = JedisPoolUtil.getConnection
partitionOfRecords.foreach(record => jedis.hincrBy("wordCount", record._1, record._2))
jedis.close()
}
}
```
这里可以看到一共使用了三次循环分别是循环RDD循环分区循环每条记录上面我们的代码是在循环分区的时候获取连接也就是为每一个分区获取一个连接。但是这里大家可能会有疑问为什么不在循环RDD的时候为每一个RDD获取一个连接这样所需要的连接数更少。实际上这是不可以的如果按照这种情况进行改写如下
```scala
pairs.foreachRDD { rdd =>
val jedis = JedisPoolUtil.getConnection
rdd.foreachPartition { partitionOfRecords =>
partitionOfRecords.foreach(record => jedis.hincrBy("wordCount", record._1, record._2))
}
jedis.close()
}
```
此时在执行时候就会抛出`Caused by: java.io.NotSerializableException: redis.clients.jedis.Jedis`,这是因为
第二个需要注意的是ConnectionPool最好是一个静态惰性初始化连接池 。这是因为Spark的转换操作本生就是惰性的且没有数据流时是不会触发写出操作故出于性能考虑连接池应该是惰性静态的所以上面`JedisPool`在初始化时采用了懒汉式单例进行初始化。
### 3.3 启动测试
```shell
[root@hadoop001 ~]# nc -lk 9999
hello world hello spark hive hive hadoop
storm storm flink azkaban
hello world hello spark hive hive hadoop
storm storm flink azkaban
```
使用Redis Manager查看写入结果(如下图),可以看到与使用`updateStateByKey`算子得到的计算结果相同。
![spark-streaming-word-count-v2](D:\BigData-Notes\pictures\spark-streaming-word-count-v3.png)

View File

@ -52,7 +52,7 @@ val addMore = (x: Int) => x + more
**2. Spark中的闭包**
在实际计算时Spark会将对RDD操作分解为TaskTask运行在Worker Noode上。在执行之前Spark会对任务进行闭包如果闭包内涉及到自由变量则程序会进行拷贝并将副本变量放在闭包中之后闭包被序列化并发送给每个执行者。因此当在foreach函数中引用`counter`它将不再是Driver节点上的`counter`,而是闭包中的副本`counter`,默认情况下,副本`counter`更新后的值不会回传到Driver所以计数器的最终值仍然为零。
在实际计算时Spark会将对RDD操作分解为TaskTask运行在Worker Node上。在执行之前Spark会对任务进行闭包如果闭包内涉及到自由变量则程序会进行拷贝并将副本变量放在闭包中之后闭包被序列化并发送给每个执行者。因此当在foreach函数中引用`counter`它将不再是Driver节点上的`counter`,而是闭包中的副本`counter`,默认情况下,副本`counter`更新后的值不会回传到Driver所以计数器的最终值仍然为零。
需要注意的是在Local模式下**有可能**执行foreach的Worker Node与Diver处在相同的JVM并引用相同的原始`counter`,这时候更新可能是正确的,但是在集群模式下却不行。所以在遇到此类问题时应优先使用累加器。

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB