实时处理(流处理)

结论

Spark和Flink的数据源最好都是Kafka等消息队列,这样才能更好的保证Exactly-Once(精准一次);

作为流处理框架,Flink是当前最优秀的实时处理框架,并处于飞速发展的状态中;

Spark社区活跃度高,生态圈庞大,Spark-Streaming技术成熟稳定,且Spark是批处理框架中使用最为广泛的框架,如果需要批处理的情况下,批处理和流处理都是用Spark,可以大大减少框架的学习成本,并且不需要不同的框架,因此在没有特殊情况时,推荐使用Spark-Streaming作为流处理的框架。

Spark框架可以作用于几乎所有大数据的任务。

如果需要完美的话,以下一些情况推荐使用Flink:

1、对数据要求是毫秒级的实时处理场景;

2、必须按照事件时间语义的流处理场景;

数据源(实时数据)

Spark:自定义数据输入,最好是Kafka等消息队列;

Flink:自定义数据输入,最好是Kafka等消息队列;

计算引擎

Spark:

Spark Streaming,通过Java或者scala编写计算代码,属于微批次处理,运行的时候需要指定批处理的时间,每次运行 job 时处理一个批次的数据,实时性还不够;

实际上每个job都可以理解为一个Spark-core任务,每次处理一个job都是先缓存数据,然后进行批处理的;

Spark Streaming 只支持处理时间,Structured streaming(Spark的结构化微批次处理框架,还停留在beta阶段,因此官方声明,仅供用户学习、实验和测试) 支持处理时间和事件时间,同时支持 watermark 机制处理滞后数据;因此Streaming本身没办法处理事件时间语义的数据,且对无法针对乱序数据和滞后进行处理;

SparkStreming通过checkPoint机制与Kafka保证数据输入的Exactly-Once,数据写出的Exactly-Once处理需要自定义;

SparkStreaming只支持基于时间的窗口操作(处理时间或者事件时间);

SparkStreaming社区活跃度高,技术成熟稳定。

Flink

Flink ,通过Java或者scala编写计算代码,是基于事件驱动的,事件可以理解为消息。事件驱动的应用程序是一种状态应用程序,它会从一个或者多个流中注入事件,通过触发计算更新状态,或外部动作对注入的事件作出反应;

事件数据会在整个Flink任务中流动处理,一个处理完成之后立马处理下一个;

Flink 支持三种时间机制:事件时间,注入时间,处理时间,同时支持 watermark 机制处理滞后数据;支持对乱序数据和滞后数据进行处理;

Flink通过checkPoint机制与Kafka保证数据输入的Exactly-Once,也可以Kafka作为输出就可以保证输出的Exactly-Once;

Flink支持的窗口操作非常灵活,不仅支持时间窗口,还支持基于数据本身的窗口(另外还支持基于time、count、session,以及data-driven的窗口操作),可以自由定义想要的窗口操作;

Flink的每个任务都会有对应的webUI查看,并且可以通过webUI对应的api接口,在我们自己的程序中获取到对应任务的一些信息,非常方便对任务的管理。

Flink处于高速发展阶段,社区活跃度不如SparkStreaming。

数据输出

Spark:Spark-Streaming可以选择输出到文件或者自定义数据输出,通过转换成DataFrame数据结构可以很方便的将数据输出到数据库或是其他结构化文件中

Flink:支持输出到文件、Redis、Netty、Kafka等,也可以自定义数据输出。

执行效率

Spark:基于内存的计算,属于微批次低延迟的模拟流处理;

Flink:基于内存的计算,真正的流处理。

任务提交

Spark

Spark的提交方法jar包+Shell命令、jar包+Spark-launcher、ssh命令

jar包+Shell命令、jar包+Spark-launcher方法均需要依赖小程序,后者通过launcher能够更好的对Spark任务进行管理;

ssh命令直接提交方法不依赖小程序,但是对于任务的管理更加不方便;

Spark 提供了以Spark-launcher 作为JAVA API编程的方式提交,这种方式不需要使用命令行,能够实现与Spring整合,让应用在tomcat中运行;Spark-launcher值支持将任务运行,日志分开输出,便于问题的回溯,可以自定义监听器,当信息或者状态变更时,进行相关操作,支持暂停、停止、断连、获得AppId、获得任务State等多种功能。

可以通过第三方组件对Spark-Streaming任务进行优雅退出。

Flink

Flink的提交方法包括jar包+Shell命令、jar包+Flink-Yarn、ssh命令

ar包+Shell命令、jar包+Spark-launcher方法均需要依赖小程序,后者通过launcher能够更好的对Spark任务进行管理;

ssh命令直接提交方法不依赖小程序,但是对于任务的管理更加不方便;

Flink-Yarn方式可以通过JAVA API远程进行任务的提交,不需要依赖小程序和命令行,能够实现与Spring整合,让应用在tomcat中运行;相比通过命令行方式,能够直接通过api获取到webUI地址以及applicationId,可以更加方便的进行任务的管理。

可以通过yarn方式,或者webUI的api接口来退出Flink任务。

实时处理

测试环境

Kafka作为数据源,数据结果均输出到数据库中。

Hadoop的Yarn作为任务运行的资源调度管理器(spark on yarn 和 flink on yarn)。

数据获取与采集

实时数据可以通过以下方式进行采集:

1、可以通过Flume监控日志文件的方式,然后数据既传给HDFS进行保存,又将数据发送给Kafka;

2、通过Kafka消息队列进行数据的获取,Flume作为Kafka的其中一个消费者将数据保存到HDFS保存;

本次实验只涉及Kafka的部分,Kafka的topic为streamTest,然后以控制台输入作为Kafka的数据生产者

## 创建topic
kafka-topics.sh --zookeeper hadoop113:2181 --create --topic streamTest --replication-factor 2 --partitions 2

## 控制台生产者
kafka-console-producer.sh --broker-list localhost:9092 --topic streamTest

job提交与运行

Spark-Streaming

object WordCountByStream {

    //初始化连接池
    var dataSource: DataSource = init()

    def main(args: Array[String]): Unit = {

        // 创建 SparkConf
        val sparkConf: SparkConf = new SparkConf().setAppName("ReceiverWordCount").setMaster("yarn")

        //创建 StreamingContext
        val ssc = new StreamingContext(sparkConf, Seconds(3))

        // 定义 Kafka 参数
        val kafkaPara: Map[String, Object] = Map[String, Object](
            ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> "hadoop113:9092,hadoop114:9092,hadoop115:9092",
            ConsumerConfig.GROUP_ID_CONFIG -> "sparkStreamTestGroup",
            ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG -> "org.apache.kafka.common.serialization.StringDeserializer",
            ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG -> "org.apache.kafka.common.serialization.StringDeserializer",
            ConsumerConfig.AUTO_OFFSET_RESET_CONFIG -> "latest"
        )

        // 读取 Kafka 数据创建 DStream
        val kafkaDStream: InputDStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream[String, String](
            ssc,
            LocationStrategies.PreferConsistent,
            ConsumerStrategies.Subscribe[String, String](Set("streamTest"), kafkaPara))

        val wordCountDS: DStream[(String, Long)] = kafkaDStream.flatMap(
            kafkaData => {
                val data = kafkaData.value()
                val datas: Array[String] = data.split(" ")
                datas.map(str => str -> 1L)
            }
        ).reduceByKey(_ + _)

        // 输出
        wordCountDS.foreachRDD(
            rdd => {
                rdd.foreachPartition(
                    iter => {
                        // 此处是放入数据库中,可以在这里设置放到其他地方,如redis等
                        val conn = getConnection
                        val pstat = conn.prepareStatement(
                            """
                              | insert into spark_streaming_test
                              | (word, cnt)
                              | values
                              | (?, ?)
                              | on duplicate key
                              | update cnt = cnt + ?
                              |""".stripMargin)
                        iter.foreach {
                            case (word, count) => {

                                println(s"$word, $count")

                                pstat.setString(1, word)
                                pstat.setLong(2, count)
                                pstat.setLong(3, count)

                                pstat.executeUpdate()
                            }
                        }
                        pstat.close()
                        conn.close()
                    }
                )
            }
        )

        // 开启任务
        ssc.start()

        // 优雅退出
        stopHandle(ssc)
        // 这个是批量处理的退出,用Ctrl+C来退出
        //  ssc.awaitTermination()
    }

    def stopHandle(ssc: StreamingContext): Unit = {
        // 优雅的关闭
        // 计算节点不再接受新的数据,而是将现有的数据处理完毕,然后关闭
        // mysql、redis、zk、hdfs等
        var needStop = false;
        while (true) {
            // 判断是否需要关闭
            if (needStop) {
                if (ssc.getState() == StreamingContextState.ACTIVE) {
                    ssc.stop(true, true)
                    System.exit(0)
                }
            }
            Thread.sleep(10000)
            needStop = true
        }
    }

    //初始化连接池方法
    def init(): DataSource = {

        val properties = new Properties()
        properties.setProperty("driverClassName", "com.mysql.jdbc.Driver")
        properties.setProperty("url", "jdbc:mysql://10.10.10.38:13306/stream_test?useUnicode=true&characterEncoding=UTF-8")
        properties.setProperty("username", "root")
        properties.setProperty("password", "123456")
        properties.setProperty("maxActive", "50")
        DruidDataSourceFactory.createDataSource(properties)
    }

    //获取 MySQL 连接
    def getConnection: Connection = {

        dataSource.getConnection
    }
}

那么可以和Spark的方法完全一样,将以上代码达成jar包,然后提交运行即可。

/opt/module/spark-yarn/bin/spark-submit --class com.starnet.server.bigdata.spark.stream.wordcount.WordCountByStream  --master yarn  --deploy-mode cluster /home/bd/SPARK/spark-test-1.0.0.jar

或者是通过Spark-launcher的方式进行提交。

在Yarn上运行时,可以提前将所使用的jar包全部上传到HDFS然后进行jar路径的配置,可以使每次任务提交时不用重新上次依赖文件,Flink也是如此。

Flink

public class WordCount {

    public static void main(String[] args) throws Exception {

        // 创建流处理执行环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        // 创建kafka配置对象
        Properties properties = new Properties();
        properties.setProperty(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop113:9092,hadoop114:9092,hadoop115:9092");
        properties.setProperty(ConsumerConfig.GROUP_ID_CONFIG, "flinkTestGroup");
        properties.setProperty(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
                "org.apache.kafka.common.serialization.StringDeserializer");
        properties.setProperty(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
                "org.apache.kafka.common.serialization.StringDeserializer");
        properties.setProperty(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest");

        // 从Kafka读取数据
        DataStreamSource<String> inputDataStream = env.addSource(new FlinkKafkaConsumer010<String>(
                "streamTest", new SimpleStringSchema(), properties));

        // 基于数据流记性转换计算
        DataStream<Tuple2<String, Integer>> resultStream = inputDataStream.flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() {
            @Override
            public void flatMap(String s, Collector<Tuple2<String, Integer>> collector) throws Exception {
            String[] fields = s.split(" ");
            for (String  field: fields) {
                collector.collect(new Tuple2<String, Integer>(field, 1));
            }

            }
        }).keyBy(0).sum(1);

        resultStream.addSink(new MyJdbcSink());


        // 执行任务
        env.execute();
    }

    public static class MyJdbcSink extends RichSinkFunction<Tuple2<String, Integer>> {

        Connection connection = null;
        PreparedStatement updateStmt = null;

        @Override
        public void open(Configuration parameters) throws Exception {

            connection = DriverManager.getConnection("jdbc:mysql://10.10.10.38:13306/stream_test",
                    "root",
                    "123456");

            updateStmt = connection.prepareStatement(
                    "insert into flink_test (word, cnt) " +
                    "values " +
                    "(?, ?) " +
                    "on duplicate key " +
                    "update cnt = cnt + ?");
        }

        // 每次更新数据时,调用连接执行sql
        @Override
        public void invoke(Tuple2<String, Integer> value, Context context) throws Exception {

            System.out.println(value.toString());

            updateStmt.setString(1, value.f0);
            updateStmt.setLong(2, value.f1);
            updateStmt.setLong(3, value.f1);

            updateStmt.execute();

        }

        @Override
        public void close() throws Exception {

            connection.close();
            updateStmt.close();
        }
    }
}

Flink Per Job

那么可以将以上代码打成jar包,然后提交运行的方式进行处理。

bin/flink run –m yarn-cluster -c com.starnet.server.bigdata.flink.WordCount /home/bd/FLINK/FlinkTest-1.0-SNAPSHOT-jar-with-dependencies.jar

通过jar提交成功之后,控制台输出如下:

2021-09-26 09:51:58,784 INFO  org.apache.flink.yarn.YarnClusterDescriptor                   - YARN application has been deployed successfully.
2021-09-26 09:51:58,785 INFO  org.apache.flink.yarn.YarnClusterDescriptor                   - The Flink YARN session cluster has been started in detached mode. In order to stop Flink gracefully, use the following command:
$ echo "stop" | ./bin/yarn-session.sh -id application_1631184398602_0052
If this should not be possible, then you can also kill Flink via YARN's web interface or via:
$ yarn application -kill application_1631184398602_0052
Note that killing Flink might not clean up all job artifacts and temporary files.
2021-09-26 09:51:58,785 INFO  org.apache.flink.yarn.YarnClusterDescriptor                   - Found Web Interface hadoop114:39315 of application 'application_1631184398602_0052'.
Job has been submitted with JobID 83ea15f02379ee196c730d6919f93243

其中可以通过以下两个命令进行任务的关闭

echo "stop" | /.../flink/.../bin/yarn-session.sh -id application_id

/.../hadoop/.../yarn application -kill application_id

并且其中有hadoop114:39315为提交flink任务之后,在yarn上开启的flink集群的web地址,可以在web上进行任务的关闭或者任务信息的获取等,也可以通过Flink的客户端api进行信息的获取和任务的停止https://ci.apache.org/projects/flink/flink-docs-release-1.13/docs/ops/rest_api/

以上信息均是在提交jar进程完成之后去进行信息的过滤和提取,提取出我们需要的信息,然后进行任务的退出。

目前Flink-on-yarn的per job模式只找到了通过jar包提交的方法。

Flink Application

对于per job模式,jar包的解析、生成JobGraph是在客户端上执行的,然后将生成的jobgraph提交到集群。如果任务特别多的话,那么这些生成JobGraph、提交到集群的操作都会在实时平台所在的机器上执行,那么将会给服务器造成很大的压力。

此外这种模式提交任务的时候会把本地flink的所有jar包先上传到hdfs上相应 的临时目录,这个也会带来大量的网络的开销,所以如果任务特别多的情况下,平台的吞吐量将会直线下降。

所以针对flink per job模式的一些问题,flink 引入了一个新的部署模式–Application模式。 目前 Application 模式支持 Yarn 和 K8s 的部署方式,Yarn Application 模式会在客户端将运行任务需要的依赖都上传到 Flink Master,然后在 Master 端进行任务的提交。

此外,还支持远程的用户jar包来提交任务,比如可以将jar放到hdfs上,进一步减少上传jar所需的时间,从而减少部署作业的时间。

提交方法如下:

jar包提交

那么可以将以上代码打成jar包,然后提交运行的方式进行处理。

bin/flink run -yd -m yarn-cluster -c com.starnet.server.bigdata.flink.WordCount /home/bd/FLINK/FlinkTest-1.0-SNAPSHOT-jar-with-dependencies.jar

/opt/module/flink-1.11.4/bin/flink run -yd -m yarn-cluster -c com.starnet.server.bigdata.flink.WordCount -yD yarn.provided.lib.dirs="hdfs://hadoop113:8020/jar/flink11/libs" /home/bd/FLINK/Flink1.11-1.0-SNAPSHOT-jar-with-dependencies.jar

通过jar提交成功之后,控制台输出如下:

2021-09-26 18:00:54,129 INFO  org.apache.flink.yarn.YarnClusterDescriptor                  [] - YARN application has been deployed successfully.
2021-09-26 18:00:54,529 INFO  org.apache.flink.yarn.YarnClusterDescriptor                  [] - Found Web Interface hadoop115:35553 of application 'application_1631184398602_0055'.

优雅退出和web访问均与以上相同。

java-api方式提交

提交相关代码如下:

public void crateStreamTaskByFlinkClient() {

    //flink的本地配置目录,为了得到flink的配置
    // 如果出现org.apache.flink.streaming.runtime.tasks.StreamTaskException: Cannot instantiate user function.错误
    // 则在flink-config.yaml加入
    // classloader.resolve-order: parent-first
    String configurationDirectory = "/opt/module/flink-1.11.4/conf";
    // String configurationDirectory = "/home/lxj/workspace/Olt-Test/bigdata/bigdataserver/src/main/resources/flink/conf";

    //存放flink集群相关的jar包目录
    String flinkLibs = "hdfs://hadoop113:8020/jar/flink11/libs";
    //用户jar
    String userJarPath = "hdfs://hadoop113:8020/jar/userTask/Flink1.11-1.0-SNAPSHOT-jar-with-dependencies.jar";
    String flinkDistJar = "hdfs://hadoop113:8020/jar/flink11/libs/flink-dist_2.12-1.11.4.jar";

    YarnClient yarnClient = YarnClient.createYarnClient();
    YarnConfiguration yarnConfiguration = new YarnConfiguration();
    yarnClient.init(yarnConfiguration);
    yarnClient.start();

    // 设置日志的,没有的话看不到日志
    YarnClusterInformationRetriever clusterInformationRetriever = YarnClientYarnClusterInformationRetriever
        .create(yarnClient);

    //获取flink的配置
    Configuration flinkConfiguration = GlobalConfiguration.loadConfiguration(
        configurationDirectory);

    flinkConfiguration.set(CheckpointingOptions.INCREMENTAL_CHECKPOINTS, true);

    flinkConfiguration.set(
        PipelineOptions.JARS,
        Collections.singletonList(userJarPath));

    Path remoteLib = new Path(flinkLibs);
    flinkConfiguration.set(
        YarnConfigOptions.PROVIDED_LIB_DIRS,
        Collections.singletonList(remoteLib.toString()));

    flinkConfiguration.set(
        YarnConfigOptions.FLINK_DIST_JAR,
        flinkDistJar);

    // 设置为application模式
    flinkConfiguration.set(
        DeploymentOptions.TARGET,
        YarnDeploymentTarget.APPLICATION.getName());

    // yarn application name
    flinkConfiguration.set(YarnConfigOptions.APPLICATION_NAME, "flink-application");

    YarnLogConfigUtil.setLogConfigFileInConfig(flinkConfiguration, configurationDirectory);

    ClusterSpecification clusterSpecification = new ClusterSpecification.ClusterSpecificationBuilder()
        .createClusterSpecification();

    // 设置用户jar的参数和主类
    ApplicationConfiguration appConfig = new ApplicationConfiguration(new String[] {"test"}, "com.starnet.server.bigdata.flink.WordCount");

    YarnClusterDescriptor yarnClusterDescriptor = new YarnClusterDescriptor(
        flinkConfiguration,
        yarnConfiguration,
        yarnClient,
        clusterInformationRetriever,
        true);

    try {
        ClusterClientProvider<ApplicationId> clusterClientProvider = yarnClusterDescriptor.deployApplicationCluster(
            clusterSpecification,
            appConfig);

        ClusterClient<ApplicationId> clusterClient = clusterClientProvider.getClusterClient();

        ApplicationId applicationId = clusterClient.getClusterId();
        String webInterfaceURL = clusterClient.getWebInterfaceURL();
        log.error("applicationId is {}", applicationId);
        log.error("webInterfaceURL is {}", webInterfaceURL);
        
        // 退出
        // yarnClusterDescriptor.killCluster(applicationId);
    } catch (Exception e){
        log.error(e.getMessage(), e);
    }
}

此方法可以远程提交Flink任务到yarn上运行,并且可以通过javaApi获取到提交之后的applicationId和web地址,以及任务的退出,整体可以不依赖小程序,非常的方便。

结果获取

以上结果最终都是保存如数据库了,因此本次实验的流式处理结果均以查询数据库的方式获取结果。