spark structured streaming 教程03(窗口函数)

上一篇structured streaming的博客,我们用structured streaming 解析了推送到kafka的用户访问日志,这篇博客我们利用窗口函数,根据用户日志统计每分钟的pv,顺便写清楚一下窗口函数的原理

1数据源

上篇博客也写过了,推送到kafka的每条用户访问日志数据源是这样的

{
	"uid": "ef16382c8acce8ec",
	"timestamp": 1594983278059,
	"agent": "Mozilla/5.0 (Linux; Android 10; Redmi K30 5G Build/QKQ1.191222.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/80.0.3987.99 Mobile Safari/537.36"
}

这里我补充一下将这条日志推送到kafka的代码,模拟一下用户访问日志(groovy代码)

package top1024b.etl

import groovy.transform.CompileStatic
import org.apache.kafka.clients.producer.KafkaProducer
import org.apache.kafka.clients.producer.ProducerRecord

@CompileStatic
class TestKafkaSource {
    private String kafkaServers = "192.168.0.1:9092"
    private String topic = "user_log"

    KafkaProducer getKafka() {
        Properties props = new Properties();
        props.put("bootstrap.servers", kafkaServers);
        props.put("acks", "all");
        props.put("delivery.timeout.ms", 30000);
        props.put("batch.size", 16384);
        props.put("linger.ms", 1);
        props.put("buffer.memory", 33554432);
        props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

        new KafkaProducer<>(props)
    }

    String getJson() {
        """
            {
              "uid": "${new Random().nextInt(100)}",
              "timestamp": ${System.currentTimeMillis()},
              "agent": "Mozilla/5.0 (Linux; Android 8.0; MI 6 Build/OPR1.170623.027; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/57.0.2987.132 MQQBrowser/6.2 TBS/044034 Mobile Safari/537.36 MicroMessenger/6.6.7.1321(0x26060736) NetType/4G Language/zh_CN"
            }
        """.toString().trim()
    }


    void run() {
        KafkaProducer producer = getKafka()
        10.times {
            producer.send(new ProducerRecord<>(topic, null, getJson()))
        }
        producer.close()
    }

    static void main(String[] args) {
        new TestKafkaSource().run()
    }
}

以上代码你需要改动的是这里

  • private String kafkaServers = “192.168.0.1:9092” 》 改成你的kafka的ip和端口,多台kafka用逗号隔开
  • private String topic = “user_log” 》 改成你要发送的topic的名称

以上代码运行一次,会随机生成10条数据推送到kafka,每条数据作为一次访问记录,用户id是100以内的数字随机的,时间戳是取程序运行的时间

2统计每分钟的pv

先上代码(groovy)

package top1024b.etl

import groovy.transform.CompileStatic
import org.apache.spark.SparkConf
import org.apache.spark.sql.Dataset
import org.apache.spark.sql.functions
import org.apache.spark.sql.Row
import org.apache.spark.sql.SparkSession

import org.apache.spark.sql.streaming.StreamingQuery

@CompileStatic
class Test03 {
    private static String kafkaServers = "192.168.0.1:9092"
    private static String topic = "user_log"

    static void main(String[] args) throws Exception {
        SparkSession spark = SparkSession
                .builder().config(new SparkConf()
                .setMaster("local[*]")
                .set("spark.sql.shuffle.partitions", "1"))
                .appName("JavaStructuredNetworkWordCount")
                .getOrCreate()

        Dataset<Row> df = spark
                .readStream()
                .format("kafka")
                .option("kafka.bootstrap.servers", kafkaServers)
                .option("subscribe", topic)
                .option("startingOffsets", "latest")
                .load()

        DataSetSql ds = new DataSetSql(spark, df)

        String sql = """
            SELECT
                get_json_object ( VALUE, '\$.uid' ) as uid,
                cast (get_json_object ( VALUE, '\$.timestamp' )/1000 as timestamp) as timestamp,
                get_json_object ( VALUE, '\$.agent' ) as agent
            FROM
                t
        """.toString().trim()

        df = ds
                .exe("select CAST(value AS STRING) from t")
                .exe(sql)
                .get()

		df = df.groupBy(
                functions.window(df.col("timestamp"), "1 minutes", "30 seconds")
        ).count()
        
		ds = new DataSetSql(spark, df)
		df = ds.exe("select date_format(window.start,'HH:mm:ss') as start,date_format(window.end,'HH:mm:ss') as end,count from t").get()

        StreamingQuery query = df.writeStream()
                .outputMode("update")
                .format("console")
                .start()

        query.awaitTermination()
    }
}

在上面这部分代码中,写窗口函数的是这部分代码:

		df = df.groupBy(
                functions.window(df.col("timestamp"), "1 minutes", "30 seconds")
        ).count()

这段代码,我们groupBy的列是一个窗口函数返回来的 :
functions.window(df.col(“timestamp”), “1 minutes”, “30 seconds”)
窗口函数的入参有3个

  1. Column : 这里我们用的是df.col(“timestamp”) ,它就是我们每条访问记录发生的时间,注意选择的列在dataset里面必须是Timestamp类型的,你应该有注意到我sql里有对该字段转过一次类型的
 cast (get_json_object ( VALUE, '\$.timestamp' )/1000 as timestamp) as timestamp,
  1. windowDuration: 这里我们用的是1分钟 ,它就是窗口的时长
  2. slideDuration: 这里我们用的是30秒,它就是滑动的时长

看到这几个参数还是有点懵,我们先看代码运行的效果,再从效果中总结原理

步骤1

启动好代码后,在 15:16:47 这个时间 我们推送了10条数据到kafka,然后看到spark程序输出如下

+--------+--------+---+
|   start|     end| pv|
+--------+--------+---+
|15:16:00|15:17:00| 10|
|15:16:30|15:17:30| 10|
+--------+--------+---+

输出结果告诉我们,15:16:00~15:17:00这段时间的pv是10,15:16:30 ~ 15:17:30 这段时间的pv也是10

  1. 我们看到这2条记录的start和end的间隔都是1分钟,这是因为我们设置了windowDuration为1分钟
  2. 我们看到输出的2条记录的start一个是15:16:00,一个是15:16:30,相差了30秒,这是因为我们设置了slideDuration为30秒的原因
  3. 为什么输出结果有且只有2条记录呢?
    因为我们是 15:16:47推的数据,而我们的滑动时间为30秒,所以计算出第一条数据的end为15:17:00(15:16:47对30秒取整),又根据窗口时间1分钟计算出start为15:16:00,15:16:47在15:16:00-15:17:00这个区间内,所以有了第一条数据;
    然后我们向后滑动30秒(滑动时间是30秒),到了15:16:30,那这条新纪录的区间就必然是15:16:30- 15:17:30 ,我们推送时间是15:16:47 ,也在第二条数据的区间里面,所以第二条数据就有了;
    然后我们再向后滑动30秒,这个时候区间就是15:17:00-15:18:00,很显然我们推送的时间不在第三条数据的区间里面,所以没有第三条数据
    因此,只打印了2条数据
步骤2

然后在 15:17:11 这个时间 我们又推送了10条数据到kafka,然后看到spark程序输出如下

+--------+--------+---+
|   start|     end| pv|
+--------+--------+---+
|15:16:30|15:17:30| 20|
|15:17:00|15:18:00| 10|
+--------+--------+---+

步骤1的时候是在15:16:47推了10条,步骤2在 15:17:11 推了10条,所以15:16:30-15:17:30这个区间的pv是20,而15:17:00-15:18:00这个区间的pv目前只有15:17:11 推了的这10条,所以是这样的结果

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页