Spark Streaming 动态读取配置

Spark Streaming 动态读取配置
为什么要动态读取配置?

在Spark 流式计算业务中, 比如通常复杂事件处理 (CEP) 的规则或者黑白名单一些配置数据. 当application 在运行期间读取相应的配置文件, 这个时候当这些名单发生改变时, 怎样能让application 动态的获取配置.

目前一般采用采用两种实现方式:

  • 轮询拉取方式,即作业算子定时或其他方式检测在外部系统的配置是否有变更,若有则同步配置。

  • 控制流方式,即作业除了用于计算的一个或多个普通数据流以外,还有提供一个用于改变作业算子状态的元数据流,也就是控制流。

目前一般Sparkstreaming 使用类似轮询的方式来实现, Flink使用控制流的方式

Sparkstreaming 实现的几种具体方式

Spark Streaming 为用户提供了 Broadcast Varialbe,可以用于节点算子状态的初始化和后续更新。

  • 通过广播变量, 并通过设置 TTL 来定时更新.
  • 通过在每一个batch计算前 get 配置变量来实现.
  • 通过一种方式使当配置数据改变时再去通过广播变量更新. 比如: 启动一个线程监听 或 其他方式
在程序什么地方获取配置数据 — 通过执行过程
1. 执行流程 — spark on yarn client

在这里插入图片描述

  1. Client 向Resource Manager 申请启动 Application Master, 同时并初始化SparkContext
  2. RM 根据 NM 节点资源情况分配一个Container 并在该Container中启动 Application Master
  3. SparkContext 初始化完成之后会与AM 通信, 告诉AM 所需资源情况, AM从RM 去申请资源.
  4. AM 得到RM 回复后, 通知在相应的NM Container 中启动executor
  5. SparkContext将taskSet 分配给executor 执行worker
2. 程序的执行过程

在Spark Streaming 中, 只有当遇到 Output Operator 操作 才是DStream上所有transformation的真正触发计算点, 类似于RDD中的action操作.

一个用户在编写的应用提交给Spark 集群执行分为两部分:

  • 驱动: Driver与Master、Worker协作完成application进程的启动、DAG划分、计算任务封装、计算任务分发到各个计算节点(Worker)、计算资源的分配等。
  • 计算逻辑本身: 当计算任务在Worker执行时,执行计算逻辑完成application的计算任务

在执行的过程可分为三部分来分析: mian方法, Dtream处理方法, RDD处理方法

执行的JVM虚拟机可以分为两部分: driver端, worker端

main方法 与 Dtream处理方法

在测试代码中我们可以看到每部分在 Driver or Worker执行 :

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

从日志中, 我们可以看出在main 方法中, 除了算子的代码, 其他的都是在Driver端执行, 且只执行一次.

​ 在DSUtil的DStream中, 其中一部分代码是driver, 一部分是worker端的.

日志中发现, transformToPair算子 与foreachRDD算子(spark Streaming 的outpot operator)每一个batch会被Driver触发一次, 因此, 我们可以考虑在这两个地方加上读取外部配置, 这样每一个batch就会被触发一次.

因此 : 采用pull

  • 从外部系统同步变量 — 在每一个Worker的Executor 执行进程里, 每次计算调用一次
    • 在transformation 算子 或者 状态算子
  • 从外部系统同步变量 — 在每一个会调用Driver的Executor 进程里, 每个batch调用一次
    • 在Transform Operation 或 TransformToPair Operation
    • 在output 算子

每个batch 会调用一次.

1. 每一个batch计算前 get 配置变量
在transformToPair or transform 实现动态读取配置
  1. 如下图代码所示:

这样在每一个batch 数据操作之前都会与数据数据库交互一次,读取一次名单, 会随着数据库名单的改变, 过滤条件发生改变.

在这里插入图片描述

  1. 改进之后— 读取的数据进行广播变量, 然后在 这里每个batch 触发一次, 进行重新广播, 代码如下:

在这里插入图片描述

这种方式的优点在于一致性更有保证。因为 Broadcast Variable 是统一由 Driver 更新并推到 Executor 的,这就保证不同节点的更新时间是一致的。然而相对地,缺点是会给 Driver 带来比较大的负担,因为需要不断分发全量的 Broadcast Variable (试想下一个巨大的 Map,每次只会更新少数 Entry,却要整个 Map 重新分发)。在 Spark 2.0 版本以后,Broadcast Variable 的分发已经从 Driver 单点改为基于 BitTorrent 的 P2P 分发,这一定程度上缓解了随着集群规模提升 Driver 分发变量的压力,但这种方式能支持到多大规模的部署还是持怀疑态度。另外一点是重新分发 Broadcast Variable 需要阻塞作业进行,这也会使作业的吞吐量和延迟受到比较大的影响。

每个batch取一次数据, 会造成很大压力.

2. 广播变量TTL定时

在这里插入图片描述

这样每5min重新进行一次广播变量, 在一定程度程度上减小了一直广播带来的性能压力, 但缺点也很明显, 就是会导致更新延迟, 不会及时更新.

3. 通过利用redis的订阅发布, 当配置改变 立刻进行广播一次
/**
 * @Author: chenshitong
 * @Date: 2019/8/4 下午4:52
 * @Desription:  test for 当每一次收到redis的订阅之后, 进行一次变量广播, 否则不进行广播
 */
public class test5{
    private static Broadcast<List<String>> BlackList = null;
    private static Map<Boolean,List<String>> mapData = new HashMap<>();

    //这里内部类利用的是redis的订阅发布, 每一次发布我们就可以得到最新的数据
    static class RedisMsgSubListener extends JedisPubSub {
        @Override
        public void onMessage(String channel, String message) {
            mapData.put(true, Arrays.asList(message.split(",")));
        }
    }

    private static void run() throws IllegalAccessException, SQLException, InstantiationException, InterruptedException {

        SparkConf conf = new SparkConf().setMaster("local[5]").setAppName("test var");
        JavaStreamingContext jsc = new JavaStreamingContext(conf, new Duration(5000));
        JavaSparkContext sparkContext = jsc.sparkContext();
        JavaReceiverInputDStream<String> lines =  jsc.socketTextStream("127.0.0.1", 9999);

        lines.flatMap(line -> Arrays.asList(line.split(" ")).iterator())
            .transformToPair(new Function2<JavaRDD<String>, Time, JavaPairRDD<String, Integer>>() {
            @Override
            public JavaPairRDD<String, Integer> call(JavaRDD<String> li, Time time) throws Exception {

                if(mapData.containsKey(true)){
                    System.out.println("更新一次广播变量值");
                    if(BlackList != null){
                        BlackList.unpersist(true);
                    }
                    BlackList = JavaSparkContext.fromSparkContext(li.context()).broadcast(mapData.get(true));
                    mapData.remove(true);
                }

                JavaPairRDD<String, Integer> outData= li.mapToPair(word -> new Tuple2<String,Integer>(word,1))
                    .filter(tuple -> {
                    if(BlackList != null && BlackList.getValue().contains(tuple._1)){
                        return false;
                    }else{
                        return true;
                    }
                }).reduceByKey((num1,num2) -> num1+num2);
                return outData;
            }
        }).print();

        try {
            jsc.start();
            jsc.awaitTermination();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException, SQLException, InstantiationException, IllegalAccessException {
        RedisMsgSubListener subscriber = new RedisMsgSubListener();
        new Thread(new Runnable() {
            public void run() {
                try {
                    System.out.println("Subscribing to mychannel,this thread will be block");
                    redisUtil.getJedis().subscribe(subscriber, "blackword");
                    System.out.println("subscription ended");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
        
        run();
    }
}

通过这种方式, 当每一次更新redis数据的同时把数据广播, 当我们订阅该频道之后就可以及时拿到数据, 当每次拿到数据就进行一次广播, 这样可以极大提高效率.

这种方式同时也避免了上面两种方式的缺点, 利用了两种方式的优点.

​ END

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值