"Spark Streaming + Kafka direct + checkpoints + 代碼改變" 引發的問題

一、基礎內容

Spark Streaming 從Kafka中接收數據,其有兩種方法:(1)、使用Receivers和Kafka高層次的API;(2)、使用 Direct API,這是使用低層次的Kafka API,並沒有使用到Receivers,是Spark1.3.0中開始引入。

由於本篇文章使用的是第二種 Direct API 方式,所以對其進行簡單的介紹一下:其會定期地從 Kafka 的 topic+partition 中查詢最新的偏移量,再根據定義的偏移量範圍在每個 batch 裏面處理數據。當作業需要處理的數據來臨時,spark 通過調用 Kafka 的簡單消費者 API 讀取一定範圍的數據。
和基於Receiver方式相比,這種方式主要有一些幾個優點:
  (1)、簡化並行。我們不需要創建多個 Kafka 輸入流,然後 union 他們。而使用 directStream,Spark Streaming 將會創建和 Kafka 分區一樣的 RDD 分區個數,而且會從 Kafka 並行地讀取數據,也就是說Spark 分區將會和 Kafka 分區有一一對應的關係,這對我們來說很容易理解和使用;
  (2)、高效。第一種實現零數據丟失是通過將數據預先保存在 WAL 中,這將會複製一遍數據,這種方式實際上很不高效,因爲這導致了數據被拷貝兩次:一次是被 Kafka 複製;另一次是寫到 WAL 中。但是 Direct API 方法因爲沒有 Receiver,從而消除了這個問題,所以不需要 WAL 日誌;
  (3)、恰好一次語義(Exactly-once semantics)。通過使用 Kafka 高層次的 API 把偏移量寫入 Zookeeper 中,這是讀取 Kafka 中數據的傳統方法。雖然這種方法可以保證零數據丟失,但是還是存在一些情況導致數據會丟失,因爲在失敗情況下通過 Spark Streaming 讀取偏移量和 Zookeeper 中存儲的偏移量可能不一致。而 Direct API 方法是通過 Kafka 低層次的 API,並沒有使用到 Zookeeper,偏移量僅僅被 Spark Streaming 保存在 Checkpoint 中。這就消除了 Spark Streaming 和 Zookeeper 中偏移量的不一致,而且可以保證每個記錄僅僅被 Spark Streaming 讀取一次,即使是出現故障。

但是本方法唯一的壞處就是沒有更新 Zookeeper 中的偏移量,所以基於 Zookeeper 的 Kafka 監控工具將會無法顯示消費的狀況。然而你可以通過 Spark 提供的 API 手動地將偏移量寫入到 Zookeeper 中。


二、問題重現

1、先編寫一個簡單的Spark Streaming WordCount 程序,但是此程序必須設置 checkpoint 並且可以 Recoverable,我的測試程序如下:

public class SparkStreamingOnKafkaDirect{

    public static JavaStreamingContext createContext(){

        SparkConf conf = new SparkConf().setMaster("local[4]").setAppName("SparkStreamingOnKafkaDirect");

        JavaStreamingContext jsc = new JavaStreamingContext(conf, Durations.seconds(30));
        jsc.checkpoint("/checkpoint");

        Map<String, String> kafkaParams = new HashMap<String, String>();
        kafkaParams.put("metadata.broker.list","192.168.1.151:1234,192.168.1.151:1235,192.168.1.151:1236");

        Set<String> topics = new HashSet<String>();
        topics.add("kafka_direct");

        JavaPairInputDStream<String, String> lines = KafkaUtils.createDirectStream(jsc, String.class,
                        String.class, StringDecoder.class,
                        StringDecoder.class, kafkaParams,
                        topics);

        JavaDStream<String> words = lines
                .flatMap(new FlatMapFunction<Tuple2<String, String>, String>() {
                    public Iterable<String> call(
                            Tuple2<String, String> event)
                            throws Exception {
                        String line = event._2;
                        return Arrays.asList(line);
                    }
                });

        JavaPairDStream<String, Integer> pairs = words
                .mapToPair(new PairFunction<String, String, Integer>() {

                    public Tuple2<String, Integer> call(
                            String word) throws Exception {
                        return new Tuple2<String, Integer>(
                                word, 1);
                    }
                });

        JavaPairDStream<String, Integer> wordsCount = pairs
                .reduceByKey(new Function2<Integer, Integer, Integer>() {
                    public Integer call(Integer v1, Integer v2)
                            throws Exception {
                        return v1 + v2;
                    }
                });

        wordsCount.print();

        return jsc;
    }

    public static void main(String[] args) {
        JavaStreamingContextFactory factory = new JavaStreamingContextFactory() {
            public JavaStreamingContext create() {
              return createContext();
            }
          };

        JavaStreamingContext jsc = JavaStreamingContext.getOrCreate("/checkpoint", factory);

        jsc.start();

        jsc.awaitTermination();
        jsc.close();
    }

}

2、準備測試環境,並記錄目前topic中的信息,如下圖:
這裏寫圖片描述

從截圖中可以看出,目前 kafka_topic 這個 topic ,一共有3個分區,每個分區的 Latest Offset 都是 25 。

3、運行Spark Streaming 程序,並在運行幾個 batch 之後,退出程序。查看 checkpoint 目錄下生成的文件,如下圖:
這裏寫圖片描述

4、現在對上面的程序做一些改動,具體的改動如下:

        JavaDStream<Word> words = lines
                .flatMap(new FlatMapFunction<Tuple2<String, String>, Word>() {
                    public Iterable<Word> call(
                            Tuple2<String, String> event)
                            throws Exception {
                        String line = event._2;
                        return Arrays.asList(new Word(line));
                    }
                });

        JavaPairDStream<String, Integer> pairs = words
                .mapToPair(new PairFunction<Word, String, Integer>() {

                    public Tuple2<String, Integer> call(
                            Word word) throws Exception {
                        return new Tuple2<String, Integer>(
                                word.getWord(), 1);
                    }
                });

5、再次運行程序,看運行的效果,我的效果如下:

Using Spark's default log4j profile: org/apache/spark/log4j-defaults.properties
16/07/31 19:23:35 INFO CheckpointReader: Checkpoint files found: file:/checkpoint/checkpoint-1469964000000,file:/checkpoint/checkpoint-1469964000000.bk,file:/checkpoint/checkpoint-1469963970000,file:/checkpoint/checkpoint-1469963970000.bk
16/07/31 19:23:35 INFO CheckpointReader: Attempting to load checkpoint from file file:/checkpoint/checkpoint-1469964000000
16/07/31 19:23:35 WARN CheckpointReader: Error reading checkpoint from file file:/checkpoint/checkpoint-1469964000000
java.io.InvalidClassException: com.bixkjwfnh.spark.streaming.SparkStreamingOnKafkaDirect$2; local class incompatible: stream classdesc serialVersionUID = -6382155631557363180, local class serialVersionUID = 2116323532858154515
    at java.io.ObjectStreamClass.initNonProxy(Unknown Source)
......
    at com.bixkjwfnh.spark.streaming.SparkStreamingOnKafkaDirect.main(SparkStreamingOnKafkaDirect.java:85)
16/07/31 19:23:35 INFO CheckpointReader: Attempting to load checkpoint from file file:/checkpoint/checkpoint-1469964000000.bk
16/07/31 19:23:35 WARN CheckpointReader: Error reading checkpoint from file file:/checkpoint/checkpoint-1469964000000.bk
java.io.InvalidClassException: com.bixkjwfnh.spark.streaming.SparkStreamingOnKafkaDirect$2; local class incompatible: stream classdesc serialVersionUID = -6382155631557363180, local class serialVersionUID = 2116323532858154515
    at java.io.ObjectStreamClass.initNonProxy(Unknown Source)
......
    at com.bixkjwfnh.spark.streaming.SparkStreamingOnKafkaDirect.main(SparkStreamingOnKafkaDirect.java:85)
16/07/31 19:23:35 INFO CheckpointReader: Attempting to load checkpoint from file file:/checkpoint/checkpoint-1469963970000
16/07/31 19:23:35 WARN CheckpointReader: Error reading checkpoint from file file:/checkpoint/checkpoint-1469963970000
java.io.InvalidClassException: com.bixkjwfnh.spark.streaming.SparkStreamingOnKafkaDirect$2; local class incompatible: stream classdesc serialVersionUID = -6382155631557363180, local class serialVersionUID = 2116323532858154515
    at java.io.ObjectStreamClass.initNonProxy(Unknown Source)
......
    at com.bixkjwfnh.spark.streaming.SparkStreamingOnKafkaDirect.main(SparkStreamingOnKafkaDirect.java:85)
16/07/31 19:23:35 INFO CheckpointReader: Attempting to load checkpoint from file file:/checkpoint/checkpoint-1469963970000.bk
16/07/31 19:23:35 WARN CheckpointReader: Error reading checkpoint from file file:/checkpoint/checkpoint-1469963970000.bk
java.io.InvalidClassException: com.bixkjwfnh.spark.streaming.SparkStreamingOnKafkaDirect$2; local class incompatible: stream classdesc serialVersionUID = -6382155631557363180, local class serialVersionUID = 2116323532858154515
    at java.io.ObjectStreamClass.initNonProxy(Unknown Source)
......
    at com.bixkjwfnh.spark.streaming.SparkStreamingOnKafkaDirect.main(SparkStreamingOnKafkaDirect.java:85)
Exception in thread "main" org.apache.spark.SparkException: Failed to read checkpoint from directory /checkpoint
    at org.apache.spark.streaming.CheckpointReader$.read(Checkpoint.scala:367)
......
    at com.bixkjwfnh.spark.streaming.SparkStreamingOnKafkaDirect.main(SparkStreamingOnKafkaDirect.java:85)
Caused by: java.io.InvalidClassException: com.bixkjwfnh.spark.streaming.SparkStreamingOnKafkaDirect$2; local class incompatible: stream classdesc serialVersionUID = -6382155631557363180, local class serialVersionUID = 2116323532858154515
    at java.io.ObjectStreamClass.initNonProxy(Unknown Source)
......
    at org.apache.spark.streaming.CheckpointReader$.read(Checkpoint.scala:350)
    ... 4 more
16/07/31 19:23:35 INFO ShutdownHookManager: Shutdown hook called

從報錯的信息中,可以看出,其是因爲checkpoint目錄下的內容沒有辦法正常的反序列化導致的,最後程序退出。

如果在沒有任何措施的情況下,出現了上面的錯誤,那問題就出來了,是什麼問題呢?
問題是:我現在要從 topic 中 partition 的哪些位置開始任何呢!!! 如果我能知道上一次每個partition中的數據都取到什麼位置了,那該多好啊!!!

三、問題解決

正如上面提到的,”如果我能知道上一次每個partition中的數據都取到什麼位置了,那該多好啊!!!“,所以我們就先把 Streaming 獲取的每個partition的位置信息保存到數據庫中。
其實次處使用的解決方法,在官方網站中也提到過了,

You can also start consuming from any arbitrary offset using other variations of KafkaUtils.createDirectStream. Furthermore, if you want to access the Kafka offsets consumed in each batch, you can do the following.

final AtomicReference<OffsetRange[]> offsetRanges = new AtomicReference<>();

 directKafkaStream.transformToPair(
   new Function<JavaPairRDD<String, String>, JavaPairRDD<String, String>>() {
     @Override
     public JavaPairRDD<String, String> call(JavaPairRDD<String, String> rdd) throws Exception {
       OffsetRange[] offsets = ((HasOffsetRanges) rdd.rdd()).offsetRanges();
       offsetRanges.set(offsets);
       return rdd;
     }
   }
 ).map(
   ...
 ).foreachRDD(
   new Function<JavaPairRDD<String, String>, Void>() {
     @Override
     public Void call(JavaPairRDD<String, String> rdd) throws IOException {
       for (OffsetRange o : offsetRanges.get()) {
         System.out.println(
           o.topic() + " " + o.partition() + " " + o.fromOffset() + " " + o.untilOffset()
         );
       }
       ...
       return null;
     }
   }
 );

那我們就採用此方法將 topic 中的 partition 的 offset 保存到mysql 數據庫中。

1、根據官網上的內容修改現有代碼,並可以將offset保存到mysql數據庫中。

public class SparkStreamingOnKafkaDirect{

    public static JavaStreamingContext createContext(){
        final Map<String, String> params = new HashMap<String, String>();
        params.put("driverClassName", "com.mysql.jdbc.Driver");
        params.put("url", "jdbc:mysql://192.168.1.151:3306/hive");
        params.put("username", "hive");
        params.put("password", "hive");

        SparkConf conf = new SparkConf().setMaster("local[4]").setAppName("SparkStreamingOnKafkaDirect");

        JavaStreamingContext jsc = new JavaStreamingContext(conf, Durations.seconds(30));
        jsc.checkpoint("/checkpoint");

        Map<String, String> kafkaParams = new HashMap<String, String>();
        kafkaParams.put("metadata.broker.list","192.168.1.151:1234,192.168.1.151:1235,192.168.1.151:1236");

        Set<String> topics = new HashSet<String>();
        topics.add("kafka_direct");

        JavaPairInputDStream<String, String> lines = KafkaUtils.createDirectStream(jsc, String.class,
                        String.class, StringDecoder.class,
                        StringDecoder.class, kafkaParams,
                        topics);

        final AtomicReference<OffsetRange[]> offsetRanges = new AtomicReference<>();

        JavaDStream<String> words = lines.transformToPair(
                new Function<JavaPairRDD<String, String>, JavaPairRDD<String, String>>() {
                    @Override
                    public JavaPairRDD<String, String> call(JavaPairRDD<String, String> rdd) throws Exception {
                      OffsetRange[] offsets = ((HasOffsetRanges) rdd.rdd()).offsetRanges();
                      offsetRanges.set(offsets);
                      return rdd;
                    }
                  }
                ).flatMap(new FlatMapFunction<Tuple2<String, String>, String>() {
                    public Iterable<String> call(
                            Tuple2<String, String> event)
                            throws Exception {
                        String line = event._2;
                        return Arrays.asList(line);
                    }
                });

        JavaPairDStream<String, Integer> pairs = words
                .mapToPair(new PairFunction<String, String, Integer>() {

                    public Tuple2<String, Integer> call(
                            String word) throws Exception {
                        return new Tuple2<String, Integer>(
                                word, 1);
                    }
                });

        JavaPairDStream<String, Integer> wordsCount = pairs
                .reduceByKey(new Function2<Integer, Integer, Integer>() {
                    public Integer call(Integer v1, Integer v2)
                            throws Exception {
                        return v1 + v2;
                    }
                });

        lines.foreachRDD(new VoidFunction<JavaPairRDD<String,String>>(){
            @Override
            public void call(JavaPairRDD<String, String> t) throws Exception {
                DataSource ds = DruidDataSourceFactory.createDataSource(params);
                Connection conn = ds.getConnection();
                Statement stmt = conn.createStatement();
                for (OffsetRange offsetRange : offsetRanges.get()) {
                    stmt.executeUpdate("update kafka_offsets set offset ='"
                            + offsetRange.untilOffset() + "'  where topic='"
                            + offsetRange.topic() + "' and partition='"
                            + offsetRange.partition() + "'");
                }
                conn.close();
            }

        });

        wordsCount.print();

        return jsc;
    }

    public static void main(String[] args) {
        JavaStreamingContextFactory factory = new JavaStreamingContextFactory() {
            public JavaStreamingContext create() {
              return createContext();
            }
          };

        JavaStreamingContext jsc = JavaStreamingContext.getOrCreate("/checkpoint", factory);

        jsc.start();

        jsc.awaitTermination();
        jsc.close();
    }

}

2、準備環境
2.1、數據庫
這裏寫圖片描述

至於裏面的字段,一看就應該明白。

2.2、最新的offset
這裏寫圖片描述

3、運行Spark Streaming 程序(注意:要先清空 checkpoint 目錄下的內容),在程序運行的過程中查看 mysql 數據庫表中的字段值如下圖:
第一次 Job:
這裏寫圖片描述
表字段值:
這裏寫圖片描述

第三次Job:
這裏寫圖片描述
表字段值:
這裏寫圖片描述
kafka Manager 中的圖:
這裏寫圖片描述

從截圖中可以看出,完全沒有問題。

其實我們完全可以將此 offset 回寫 Zookeeper 中,這樣就同步到以在Kafka Manager 中進行監控了。

發佈了29 篇原創文章 · 獲贊 18 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章