這一章節以惡意請求流量記錄作爲我們的數據,編寫一個完整案例
史上最簡單的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累加器?
- 累加器簡單的來說就是對信息進行聚合
- whatis累加器?
- 廣播變量(broadcase variable)
- whatis廣播變量
- 高效的分發較大對象
- 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屬性選擇另一個序列化庫來優化序列化過程