史上最簡單的spark教程第八章-spark的自定義累加器與廣播變量Java案例實踐

這一章節以惡意請求流量記錄作爲我們的數據,編寫一個完整案例

史上最簡單的spark教程
所有代碼示例地址:https://github.com/Mydreamandreality/sparkResearch

(提前聲明:文章由作者:張耀峯 結合自己生產中的使用經驗整理,最終形成簡單易懂的文章,寫作不易,轉載請註明)
(文章參考:Elasticsearch權威指南,Spark快速大數據分析文檔,Elasticsearch官方文檔,實際項目中的應用場景)
(幫到到您請點點關注,文章持續更新中!)
Git主頁 https://github.com/Mydreamandreality

如果你在開始前沒有接觸過累加器的概念,
我強烈建議先簡單的瞭解下累加器的基礎概念,鏈接: https://baike.baidu.com/item/累加器/8590163?fr=aladdin

本章數據的格式如下,省略了一部分涉密數據:
         { "origin_id": 2,
          "asset_id": 152,
          "add_type": 0,
          "asset_name": "test",
          "level": 2,
          "status": 3,
          "event_desc": "漏掃任務觸發告警通知",
          "notice_date": "2019-01-22T20:42:31+0800",
          "create_date": "2019-01-22T20:42:31+0800",
          "dispose_result_id": 1,
          "disposal_user": 1,
          "disposal_date": "2019-01-23T16:12:54+0800",
          "note": "578468",
          "attachment": "/media/disposeresult_dbbc9812-e999-461a-83.docx"
          ..............省略一部分數據}
在這個案例中我們統計嚴重漏洞的情況.並且分發一張巨大的查詢表

  • 共享變量:累加器(accumulator)
  • 共享變量是一種可以在spark任務中使用的特殊類型的變量
    • whatis累加器?
      • 累加器簡單的來說就是對信息進行聚合
  • 廣播變量(broadcase variable)
    • whatis廣播變量
      • 高效的分發較大對象

=== 我的學習方法是,先有基礎的概念,然後再深入學習,
=== 下面具體的介紹下這些概念


我們爲什麼需要累加器和廣播變量?
  • 在正常的程序中,我們向spark傳遞函數,比如使用map()或者filter(),可以使用驅動器程序中定義的變量,把任務分發到各個集羣的計算節點
  • 這個時候集羣的計算節點會把驅動器的變量另保存一份副本,形成新的變量,
  • 這個時候我們更新節點中的數據,對驅動器中的變量是不會有任何影響的
  • 如果需要進行計算節點數據共享,讀寫共享變量效率是比較低下的
  • 而spark的共享變量.累加器和廣播變量這兩種常見的通信模式突破了這種限制
  • 說到這裏可能有點暈:那麼靈魂畫手上線啦
  • 在這裏插入圖片描述
  • 手動畫的實在是太好看了,不好展示[捂臉].
  • 這樣畫的不知道大家能否看的懂,實在不清楚可以留言交流

共享變量-累加器
  • 剛纔介紹的時候說了,累加器就是對信息進行聚合,其實準確的說,累加器提供將工作節點的值聚合到驅動器中的簡單語法
  • 那麼累加器在真實場景中有啥子用呢?
  • 舉個栗子
    • 我這裏有大量的惡意請求流量,裏面包含了各種各樣的信息,請求頭,請求體,協議,mac,IP,攻擊手法,攻擊者定位地址,攻擊時間等等,但是有些時候攻擊者的僞裝或者引擎的某些問題,可能會導致其中的某些數據是空,
    • 那我現在就要統計這些數據文件中有哪些key的value爲null或者0,並且輸出,其他結果不進行輸出
    • [當然你也可以延伸其他的需求]
    • 代碼案例:
    • 代碼略多,建議從我的GitHub上pull一份源碼,自己debug一遍
import lombok.Data;

/**
 * Created by 張燿峯
 * 定義攻擊流量中的字段
 *
 * @author 孤
 * @date 2019/3/25
 * @Varsion 1.0
 */

public class JavaBean {

    public static String origin_id = "origin_id";

    public static String asset_id = "asset_id";

    public static String add_type = "add_type";

    public static String asset_name = "asset_name";

    /*其他的我先省略了,太多了,先拿這麼多進行測試*/
}

import org.apache.spark.util.AccumulatorV2;

import java.util.HashMap;
import java.util.Map;

/**
 * Created by 張燿峯
 * 累加器
 * @author 孤
 * @date 2019/3/25
 * @Varsion 1.0
 */
public class AttackAccumulator extends AccumulatorV2<String, String> {

    /*定義我需要計算的變量*/
    private Integer emptyLine;

    /*定義變量的初始值*/
    private String initEmptyLine = JavaBean.origin_id + ":0;" + JavaBean.asset_name + ":0;";

    /*初始化原始狀態*/
    private String resetInitEmptyLine = initEmptyLine;

    /*判斷是否等於初始狀態*/
    @Override
    public boolean isZero() {
        return initEmptyLine.equals(resetInitEmptyLine);
    }

    //複製新的累加器
    @Override
    public AccumulatorV2<String, String> copy() {
        return new AttackAccumulator();
    }

    /*初始化原始狀態*/
    @Override
    public void reset() {
        initEmptyLine = resetInitEmptyLine;
    }

    /*針對傳入的新值,與當前累加器已有的值進行累加*/
    @Override
    public void add(String s) {
        initEmptyLine = mergeData(s,initEmptyLine,";");
    }

    /*將兩個累加器的計算結果合併*/
    @Override
    public void merge(AccumulatorV2<String, String> accumulatorV2) {
        initEmptyLine = mergeData(accumulatorV2.value(), initEmptyLine, ";");
    }

    /*返回累加器的值*/
    @Override
    public String value() {
        return initEmptyLine;
    }

    /*篩選字段爲null的*/
    private Integer mergeEmptyLine(String key, String value, String delimit) {
        return 0;
    }


    private static String mergeData(String data_1, String data_2, String delimit) {
        StringBuffer stringBuffer = new StringBuffer();
        //通過分割的方式獲取value
        String[] info_1 = data_1.split(delimit);
        String[] info_2 = data_2.split(delimit);

        //處理info_1數據
        Map<String, Integer> mapNode = resultKV(":", info_1);

        //處理info_2數據
        Map<String, Integer> mapNodeTo = resultKV(":", info_2);

        consoleResult(delimit, stringBuffer, mapNodeTo, mapNode);

        consoleResult(delimit, stringBuffer, mapNode, mapNodeTo);

        return stringBuffer.toString().substring(0, stringBuffer.toString().length() - 1);
    }

    private static void consoleResult(String delimit, StringBuffer stringBuffer, Map<String, Integer> mapNode, Map<String, Integer> mapNodeTo) {
        for (Map.Entry<String, Integer> entry : mapNodeTo.entrySet()) {
            String key = entry.getKey();
            Integer value = entry.getValue();
            if (value == null || 0 == (value)) {
                value += 1;
                if (mapNode.containsKey(key) && (mapNode.get(key) == null || 0 == (mapNode.get(key)))) {
                    value += 1;
                    mapNode.remove(key);
                    stringBuffer.append(key + ":" + value + delimit);
                    continue;
                }
                stringBuffer.append(key + ":" + value + delimit);
            }
        }
    }


    private static Map<String, Integer> resultKV(String delimit, String[] infos) {
        Map<String, Integer> mapNode = new HashMap<>();
        for (String info : infos) {
            String[] kv = info.split(delimit);
            if (kv.length == 2) {
                String k = kv[0];
                Integer v = Integer.valueOf(kv[1]);
                mapNode.put(k, v);
                continue;
            }
        }
        return mapNode;
    }
}

import com.fasterxml.jackson.databind.ObjectMapper;
import com.sun.org.apache.xpath.internal.SourceTree;
import org.apache.avro.ipc.specific.Person;
import org.apache.spark.InternalAccumulator;
import org.apache.spark.api.java.JavaPairRDD;
import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.api.java.JavaSparkContext;
import org.apache.spark.api.java.function.FlatMapFunction;
import org.apache.spark.api.java.function.PairFunction;
import org.apache.spark.api.java.function.VoidFunction;
import org.apache.spark.sql.SparkSession;
import org.apache.spark.util.AccumulatorV2;
import org.codehaus.janino.Java;
import scala.Tuple2;

import java.io.IOException;
import java.util.*;

/**
 * Created by 張燿峯
 * 第八章案例
 * 累加器運行
 * @author 孤
 * @date 2019/3/25
 * @Varsion 1.0
 */
public class Accumulator {

    public static void main(String[] args) {
        SparkSession sparkSession = SparkSession.builder()
                .master("local[4]").appName("AttackFind").getOrCreate();
        //初始化sparkContext
        JavaSparkContext javaSparkContext = JavaSparkContext.fromSparkContext(sparkSession.sparkContext());
        //日誌輸出級別
        javaSparkContext.setLogLevel("ERROR");
        //創建RDD
        JavaRDD<String> rdd = javaSparkContext.parallelize(Arrays.asList(JavaBean.origin_id, JavaBean.asset_name));

        AttackAccumulator attackAccumulator = new AttackAccumulator();
        //註冊累加器
        javaSparkContext.sc().register(attackAccumulator, "attack_count");
        //生成一個隨機數作爲value
        JavaPairRDD<String, String> javaPairRDD = rdd.mapToPair(new PairFunction<String, String, String>() {
            @Override
            public Tuple2<String, String> call(String s) {
                Integer random = new Random().nextInt(10);
                return new Tuple2<>(s, s + ":" + random);
            }
        });

        javaPairRDD.foreach((VoidFunction<Tuple2<String, String>>) tuple2 -> {
            attackAccumulator.add(tuple2._2);
        });
    }
}

運行結果
  • 如果鍵值對的數據中value是null或者0,則進行數據的記錄
    在這裏插入圖片描述

  • 如果鍵值對的數據中value不是null或者不是0,則不進行數據的記錄
    在這裏插入圖片描述

  • JavaBean中定義了一些需要輸出的字段

  • AttackAccumulator中重寫了累加器的一些函數

  • Accumulator是啓動腳本

  • 啓動腳本中我們使用了SaprkSession,這是2.0的新概念,下一章中解釋一下吧

  • 在我的git中還有一個文件是TestMerge,各位可以下載debug一遍,.就知道整個運行機制了

在這對累加器做一個小總結
首先創建驅動器,註冊我們的累加器,此處的累加器是繼承了AccumulatorV2的類
其次執行我們自定義累加器中的方法 +=(add)增加累加器的值
最終驅動器程序可以調用累加器的value屬性, java中Value()訪問累加器的值
在計算節點上的任務不能訪問累加器的值,對於計算節點這只是一個寫的變量,在這種模式下,累加器可以更加高效

spark容錯性
  • 在開發中節點異常是非常有可能發生的

  • 舉個栗子

  • 如果我們對某個分區執行map()操作的節點失敗了,spark會自動重新在另一個節點運行該任務,就算這個節點沒有崩潰,只是處理速度比其他節點慢很多,spark也可以搶佔式的在另外一個節點上啓動一個投機型的任務副本

  • 那麼這就衍生出一個問題,累計器是如何應對容錯的

  • 在RDD的轉化操作中使用累加器可能會發生不止一次的更新,故我們需要對RDD先做緩存

  • rdd.cache();

  • 以保證我們的數據重新讀取時無需從頭開始

  • 如果此時你想要一個無論失敗還是重複計算都絕對可靠的累加器,那就需要把累加器放進foreach()這樣的行動操作中

  • 因爲在行動操作中,spark只會把每個任務對各累加器的修改應用一次


廣播變量

  • 廣播變量可以高效的向所有的工作節點發送一個較大的只讀值,提供給一個或者多個spark操作使用

  • 廣播變量的使用場景就是你需要向所有節點發送只讀的查詢表,又或者機器學習中很大的特徵向量

  • 你可以這麼理解:

  • 當在Executor端用到了Driver變量,不使用廣播變量,在每個Executor中有多少個task就有多少個Driver端變量副本,如果使用廣播變量在每個Executor端中只有一份Driver端的變量副本
    注意:
    1.不能將RDD廣播出去,可以將RDD的結果廣播出去
    2.廣播變量在Driver定義,在Exector端不可改變,在Executor端不能定義

  • 代碼示例:

   /**
     * 廣播變量測試
     * @param args
     */
    public static void main(String[] args) {
        SparkSession sparkSession = SparkSession.builder()
                .master("local[4]").appName("AttackFind").getOrCreate();
        //初始化sparkContext
        JavaSparkContext javaSparkContext = JavaSparkContext.fromSparkContext(sparkSession.sparkContext());
        //在這裏假定一份廣播變量
        //因爲我們之前說過,廣播變量只可讀
        final List<String> broadcastList = Arrays.asList("190099HJLL","98392QUEYY","561788LLKK");
        //設置廣播變量,把broadcast廣播出去
        final Broadcast<List<String>> broadcast = javaSparkContext.broadcast(broadcastList);
        //定義數據
        JavaPairRDD<String,String> pairRDD = javaSparkContext.parallelizePairs(Arrays.asList(new Tuple2<>("000", "000")));
        JavaPairRDD<String,String> resultPairRDD = pairRDD.filter((Function<Tuple2<String, String>, Boolean>) v1 -> broadcast.value().contains(v1._2));
        resultPairRDD.foreach((VoidFunction<Tuple2<String, String>>) System.out::println);
    }

廣播優化

  • 當廣播一個比較大的值時,選擇既快又好的序列化格式是很重要的
  • 因爲如果序列化對象的時間很長或者傳送花費的時間太久,這段時間很容易就成爲性能瓶頸
  • 尤其是Spark 的Scala和Java API中默認使用的序列化庫爲Java序列化庫,
  • 因此它對於除基本類型的數組以外的任何對象都比較低效
  • 你可以使用spark.serializer屬性選擇另一個序列化庫來優化序列化過程

下一章節更新sparkSQL

哪裏有問題希望大家留言交流哦

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章