Cote – chatgpz.de – KI-Tools & Praxiswissen http://chatgpz.de Unabhängige Tests, Anleitungen und Hintergründe zu KI-Tools. Thu, 20 Jun 2024 12:58:23 +0000 de-DE hourly 1 https://wordpress.org/?v=6.4.2 234900186 Optimierung von Sigma-Regeln in Spark mit dem Aho-Corasick-Algorithmus | von Jean-Claude Cote | Juni 2024 http://chatgpz.de/machine-learning/optimierung-von-sigma-regeln-in-spark-mit-dem-aho-corasick-algorithmus-von-jean-claude-cote-juni-2024/ http://chatgpz.de/machine-learning/optimierung-von-sigma-regeln-in-spark-mit-dem-aho-corasick-algorithmus-von-jean-claude-cote-juni-2024/#respond Thu, 20 Jun 2024 12:58:23 +0000 http://chatgpz.de/machine-learning/optimierung-von-sigma-regeln-in-spark-mit-dem-aho-corasick-algorithmus-von-jean-claude-cote-juni-2024/ [ad_1]

Erweiterung von Spark für verbesserte Leistung bei der Verarbeitung mehrerer Suchbegriffe

Foto von Aditya Chinchure auf Unsplash

Während der Einführung unseres Intrusion Detection Programs in der Produktion bei CCCShaben wir festgestellt, dass viele der SigmaHQ-Regeln sehr umfangreiche Pay attention mit Suchmustern verwenden. Diese Pay attention werden verwendet, um zu testen, ob ein CommandLineeine bestimmte Zeichenfolge enthält oder CommandLinebeginnt mit oder endet mit einer bestimmten Teilzeichenfolge.

Wir waren besonders daran interessiert, die Regeln mit „enthält“-Bedingungen zu untersuchen, da wir vermuteten, dass die Auswertung dieser Bedingungen für Spark zeitaufwändig sein könnte. Hier ist ein Beispiel für eine typische Sigma-Regel:

detection:
selection_image:
- Picture|comprises:
- 'CVE-202'
- 'CVE202'
- Picture|endswith:
- 'poc.exe'
- 'artifact.exe'
- 'artifact64.exe'
- 'artifact_protected.exe'
- 'artifact32.exe'
- 'artifact32big.exe'
- 'obfuscated.exe'
- 'obfusc.exe'
- 'meterpreter'
selection_commandline:
CommandLine|comprises:
- 'inject.ps1'
- 'Invoke-CVE'
- 'pupy.ps1'
- 'payload.ps1'
- 'beacon.ps1'
- 'PowerView.ps1'
- 'bypass.ps1'
- 'obfuscated.ps1'

Die vollständige Regel zu verdächtigen Programmnamen finden Sie hier
https://github.com/SigmaHQ/sigma/blob/grasp/guidelines/home windows/process_creation/proc_creation_win_susp_progname.yml

Die Regel veranschaulicht die Verwendung von CommandLine|comprises und von Picture|endswith. Einige Sigma-Regeln haben Hunderte von Suchbegriffen unter einem <subject>|comprisesZustand.

Anwenden von Sigma-Regeln mit Spark SQL

Bei CCCSübersetzen wir Sigma-Regeln in ausführbare Spark-SQL-Anweisungen. Dazu haben wir den SQL Sigma-Compiler um ein benutzerdefiniertes Backend erweitert. Es übersetzt die obige Regel in eine Anweisung wie diese:

choose
map(
'Suspicious Program Names',
(
(
(
Imagepath LIKE '%cve-202%'
OR Imagepath LIKE '%cve202%'
)
OR (
Imagepath LIKE '%poc.exe'
OR Imagepath LIKE '%artifact.exe'
...
OR Imagepath LIKE '%obfusc.exe'
OR Imagepath LIKE '%meterpreter'
)
)
OR (
CommandLine LIKE '%inject.ps1%'
OR CommandLine LIKE '%invoke-cve%'
OR CommandLine LIKE '%pupy.ps1%'
...
OR CommandLine LIKE '%encode.ps1%'
OR CommandLine LIKE '%powercat.ps1%'
)
)
) as sigma_rules_map

Wir führen die obige Anweisung in einem Spark Structured Streaming-Job aus. In einem einzigen Durchlauf über die Ereignisse wertet Spark mehrere (Hunderte) Sigma-Regeln aus. Die sigma_rules_map Die Spalte enthält die Auswertungsergebnisse aller dieser Regeln. Mithilfe dieser Karte können wir feststellen, welche Regel ein Treffer ist und welche nicht.

Wie wir sehen, beinhalten die Regeln oft den Vergleich von Ereignisattributen, wie z. B. CommandLinezu mehreren Zeichenfolgenmustern.

Einige dieser Assessments sind exakte Übereinstimmungen, wie zum Beispiel CommandLine = ‘one thing’Andere verwenden startswithund werden wiedergegeben als Imagepath LIKE ‘%poc.exe’.

Equals, startswithUnd endswith werden sehr schnell ausgeführt, da diese Bedingungen alle an einer bestimmten Place im Attribut des Ereignisses verankert sind.

Allerdings können Assessments wie comprises werden wiedergegeben als CommandLine LIKE ‘%hound.ps1%’ Dazu muss Spark das gesamte Attribut scannen, um eine mögliche Startposition für den Buchstaben „h“ zu finden und dann zu prüfen, ob darauf der Buchstabe „o“, „u“ usw. folgt.

Intern verwendet Spark eine UTF8String Das Programm greift das erste Zeichen, durchsucht den Puffer und vergleicht, wenn es eine Übereinstimmung findet, die restlichen Bytes mit dem matchAt Funktion. Hier ist die Implementierung der UTF8String.comprises Funktion.

  public boolean comprises(last UTF8String substring) {
if (substring.numBytes == 0) {
return true;
}

byte first = substring.getByte(0);
for (int i = 0; i <= numBytes - substring.numBytes; i++) {
if (getByte(i) == first && matchAt(substring, i)) {
return true;
}
}
return false;
}

Der equals, startswithUnd endswith Bedingungen verwenden auch die matchAt Funktion, aber im Gegensatz zu comprises Diese Bedingungen wissen, wo der Vergleich beginnen muss, und werden daher sehr schnell ausgeführt.

Um unsere Annahme zu bestätigen, dass comprises Bedingung ist teuer in der Ausführung, wir haben ein schnelles und einfaches Experiment durchgeführt. Wir haben alle entfernt comprises Bedingungen für die Sigma-Regeln, um zu sehen, wie sich dies auf die Gesamtausführungszeit auswirken würde. Der Unterschied battle signifikant und ermutigte uns, die Idee der Implementierung einer benutzerdefinierten Spark Catalyst-Funktion zur Handhabung weiter zu verfolgen comprises Operationen mit einer großen Anzahl von Suchbegriffen.

Der Aho-Corasick-Algorithmus

Ein wenig Recherche führte uns zu der Aho-Corasick-Algorithmus was für diesen Anwendungsfall intestine geeignet schien. Der Aho-Corasick-Algorithmus erstellt einen Präfixbaum (einen Trie) und kann viele comprises Ausdrücke in einem einzigen Durchgang über den zu testenden Textual content.

So verwenden Sie die Aho-Corasick Java-Implementierung von Robert Bor, die hier auf GitHub verfügbar ist https://github.com/robert-bor/aho-corasick

// create the trie
val triBuilder = Trie.builder()
triBuilder.addKeyword("test1")
triBuilder.addKeyword("test2")
trie = triBuilder.construct()

// apply the trie to some textual content
aTextColumn = "some textual content to scan for both test1 or test2"
discovered = trie.containsMatch(aTextColumn)

Entwerfen einer aho_corasick_in Spark-Funktion

Unsere Funktion benötigt zwei Dinge: die zu testende Spalte und die zu suchenden Suchmuster. Wir implementieren eine Funktion mit der folgenden Signatur:

boolean aho_corasick_in(string textual content, array<string> searches)

Wir haben unseren CCCS Sigma-Compiler modifiziert, um SQL-Anweisungen zu erzeugen, die den aho_corasick_inFunktion, anstatt mehrere ORed LIKE-Prädikate zu erzeugen. In der Ausgabe unten werden Sie die Verwendung der aho_corasick_in Funktion. Wir übergeben das zu testende Feld und ein Array von Zeichenfolgen, nach denen gesucht werden soll. Hier ist die Ausgabe unseres benutzerdefinierten Compilers, der mehrere comprises Bedingungen:

choose 
map(
'Suspicious Program Names',
(
(
(
Imagepath LIKE '%cve-202%'
OR Imagepath LIKE '%cve202%'
)
OR (
Imagepath LIKE '%poc.exe'
OR Imagepath LIKE '%artifact.exe'
...
OR Imagepath LIKE '%meterpreter'
)
)
OR (
aho_corasick_in(
CommandLine,
ARRAY(
'inject.ps1',
'invoke-cve',
...
'hound.ps1',
'encode.ps1',
'powercat.ps1'
)
)
)
)
) as sigma_rules_map

Beachten Sie, wie die aho_corasick_in Funktion erhält zwei Argumente: das erste ist eine Spalte und das zweite ist ein String-Array. Lassen Sie uns nun die aho_corasick_inFunktion.

Implementierung der Katalysatorfunktion

Wir fanden nicht viel Dokumentation zur Implementierung von Catalyst-Funktionen, additionally verwendeten wir stattdessen den Quellcode vorhandener Funktionen als Referenz. Wir nahmen die regulärer Ausdruck(str, regulärer Ausdruck) Funktion als Beispiel, da sie ihr Regexp-Muster vorkompiliert und es dann bei der Verarbeitung von Zeilen verwendet. Dies ist vergleichbar mit dem Vorkompilieren eines Aho-Corasick-Tries und dessen anschließender Anwendung auf jede Zeile.

Unser benutzerdefinierter Katalysatorausdruck nimmt zwei Argumente an. Es ist additionally ein BinaryExpression welches zwei Felder hat, die Spark benannt hat left Und proper. Unser AhoCorasickIn Konstruktor weist den textual content Spaltenargument für left Feld und das searches String-Array in proper Feld.

Das andere, was wir während der Initialisierung von AhoCorasickIn tun, ist die Auswertung der cacheTrie Feld. Die Auswertung prüft, ob das searches argument ist ein faltbarer Ausdruck, additionally ein konstanter Ausdruck. Wenn ja, wird es ausgewertet und es wird erwartet, dass es sich um ein String-Array handelt, das zum Aufrufen verwendet wird createTrie(searches).

Der createTrie Funktion iteriert über die Suchvorgänge und fügt sie dem trieBuilder und erstellt schließlich einen Aho-Corasick-Trie.

case class AhoCorasickIn(textual content: Expression, searches: Expression) 
extends BinaryExpression
with CodegenFallback
with ImplicitCastInputTypes
with NullIntolerant
with Predicate {

override def prettyName: String = "aho_corasick_in"
// Assign textual content to left subject
override def left: Expression = textual content
// Assign searches to proper subject
override def proper: Expression = searches

override def inputTypes: Seq(DataType) = Seq(StringType, ArrayType(StringType))

// Cache foldable searches expression when AhoCorasickIn is constructed
personal lazy val cacheTrie: Trie = proper match {
case p: Expression if p.foldable => {
val searches = p.eval().asInstanceOf(ArrayData)
createTrie(searches)
}
case _ => null
}

protected def createTrie(searches: ArrayData): Trie = {
val triBuilder = Trie.builder()
searches.foreach(StringType, (i, s) => triBuilder.addKeyword(s.toString()))
triBuilder.construct()
}

protected def getTrie(searches: ArrayData) = if (cacheTrie == null) createTrie(searches) else cacheTrie

override protected def nullSafeEval(textual content: Any, searches: Any): Any = {
val trie = getTrie(searches.asInstanceOf(ArrayData))
trie.containsMatch(textual content.asInstanceOf(UTF8String).toString())
}

override protected def withNewChildrenInternal(
newLeft: Expression, newRight: Expression): AhoCorasickIn =
copy(textual content = newLeft, searches = newRight)
}

Der nullSafeEval Methode ist das Herzstück von AhoCorasickIn. Spark ruft die eval-Funktion für jede Zeile im Datensatz auf. In nullSafeEvalwir holen die cacheTrie und testen Sie damit die textual content Zeichenfolgenargument.

Bewertung der Leistung

Zum Vergleich der Leistung der aho_corasick_in Funktion haben wir ein kleines Benchmarking-Skript geschrieben. Wir haben die Leistung mehrerer Assessments verglichen. LIKE Operationen im Vergleich zu einer einzelnen aho_corasick_in Anruf.

choose
*
from (
choose
textual content like '%' || uuid() || '%' OR
textual content like '%' || uuid() || '%' OR
textual content like '%' || uuid() || '%' OR
...
as end result
from (
choose
uuid()||uuid()||uuid()... as textual content
from
vary(0, 1000000, 1, 32)
)
)
the place
end result = TRUE

Dasselbe Experiment mit aho_corasick_in:

choose
*
from (
choose
aho_corasick_in(textual content, array(uuid(), uuid(),...) as end result
from (
choose
uuid()||uuid()||uuid()... as textual content
from
vary(0, 1000000, 1, 32)
)
)
the place
end result = TRUE

Wir führten diese beiden Experimente (wie vs aho_corasick_in) mit einem textual content Spalte mit 200 Zeichen und variierte die Anzahl der Suchbegriffe. Hier ist ein logarithmisches Diagramm, das beide Abfragen vergleicht.

Bild vom Autor

Dieses Diagramm zeigt, wie die Leistung nachlässt, wenn wir der Abfrage „LIKE“ weitere Suchbegriffe hinzufügen, während die Abfrage mit aho_corasick_in Funktion bleibt relativ konstant, wenn die Anzahl der Suchbegriffe zunimmt. Bei 100 Suchbegriffen ist die aho_corasick_in Die Funktion wird fünfmal schneller ausgeführt als mehrere LIKE-Anweisungen.

Wir haben festgestellt, dass die Verwendung von Aho-Corasick erst nach mehr als 20 Suchvorgängen von Vorteil ist. Dies lässt sich durch die anfänglichen Kosten für die Erstellung des Tries erklären. Mit zunehmender Anzahl der Suchbegriffe zahlen sich diese Vorabkosten jedoch aus. Dies steht im Gegensatz zu den LIKE-Ausdrücken, bei denen die Abfrage umso teurer wird, je mehr LIKE-Ausdrücke wir hinzufügen.

Als nächstes setzten wir die Anzahl der Suchbegriffe auf 20 und variierten die Länge der textual content Zeichenfolge. Wir haben festgestellt, dass sowohl LIKE als auch aho_corasick_in Funktion dauert bei verschiedenen Stringlängen etwa gleich lange. In beiden Experimenten ist die Ausführungszeit abhängig von der Länge des textual content Zeichenfolge.

Bild vom Autor

Es ist wichtig zu beachten, dass die Kosten für die Erstellung des Tries von der Anzahl der Spark-Aufgaben im Abfrageausführungsplan abhängen. Spark instanziiert Ausdrücke (d. h. instanziiert neue AhoCorasickIn-Objekte) für jede Aufgabe im Ausführungsplan. Mit anderen Worten: Wenn Ihre Abfrage 200 Aufgaben verwendet, wird der AhoCorasickIn-Konstruktor 200 Mal aufgerufen.

Zusammenfassend lässt sich sagen, dass die zu verwendende Strategie von der Anzahl der Begriffe abhängt. Wir haben diese Optimierung in unseren Sigma-Compiler eingebaut. Unter einem bestimmten Schwellenwert (sagen wir 20 Begriffe) werden LIKE-Anweisungen ausgegeben, und über diesem Schwellenwert wird eine Abfrage ausgegeben, die die aho_corasick_in Funktion.

Dieser Schwellenwert hängt natürlich von Ihren tatsächlichen Daten und der Anzahl der Aufgaben in Ihrem Spark-Ausführungsplan ab.

Unsere ersten Ergebnisse, die wir mit Produktionsdaten und echten SigmaHQ-Regeln durchgeführt haben, zeigen, dass die Anwendung der aho_corasick_in Funktion erhöht unsere Verarbeitungsrate (Ereignisse professional Sekunde) um den Faktor 1,4.

Bild vom Autor

Abschluss

In diesem Artikel haben wir gezeigt, wie man eine native Spark-Funktion implementiert. Dieser Catalyst-Ausdruck nutzt den Aho-Corasick-Algorithmus, der viele Suchbegriffe gleichzeitig testen kann. Wie bei jedem Ansatz gibt es jedoch auch hier Kompromisse. Die Verwendung von Aho-Corasick erfordert den Aufbau eines Tries (Präfixbaum), was die Leistung beeinträchtigen kann, wenn nur wenige Suchbegriffe verwendet werden. Unser Compiler verwendet einen Schwellenwert (Anzahl der Suchbegriffe), um die optimale Strategie auszuwählen und so die effizienteste Abfrageausführung sicherzustellen.

[ad_2]

]]>
http://chatgpz.de/machine-learning/optimierung-von-sigma-regeln-in-spark-mit-dem-aho-corasick-algorithmus-von-jean-claude-cote-juni-2024/feed/ 0 1586
Leistungseinblicke aus Sigma-Regelerkennungen in Spark Streaming | von Jean-Claude Cote | Juni 2024 http://chatgpz.de/machine-learning/leistungseinblicke-aus-sigma-regelerkennungen-in-spark-streaming-von-jean-claude-cote-juni-2024/ http://chatgpz.de/machine-learning/leistungseinblicke-aus-sigma-regelerkennungen-in-spark-streaming-von-jean-claude-cote-juni-2024/#respond Sat, 01 Jun 2024 22:28:33 +0000 http://chatgpz.de/machine-learning/leistungseinblicke-aus-sigma-regelerkennungen-in-spark-streaming-von-jean-claude-cote-juni-2024/ [ad_1]

Nutzung von Sigma-Regeln zur Anomalieerkennung in Cybersicherheitsprotokollen: Eine Studie zur Leistungsoptimierung

Foto von Ed Vazquez auf Unsplash

Eine der Rollen des Kanadisches Zentrum für Cybersicherheit (CCCS) besteht darin, Anomalien zu erkennen und schnellstmöglich Gegenmaßnahmen zu ergreifen.

Während wir unsere Sigma-Regelerkennungen in die Produktion einführten, machten wir eine interessante Beobachtung in unserer Spark-Streaming-Anwendung. Das Ausführen einer einzelnen großen SQL-Anweisung mit 1000 Sigma-Erkennungsregeln struggle langsamer als das Ausführen von fünf separaten Abfragen, von denen jede 200 Sigma-Regeln anwendete. Das struggle überraschend, da das Ausführen von fünf Abfragen Spark dazu zwingt, die Quelldaten fünfmal statt einmal zu lesen. Weitere Einzelheiten finden Sie in unserer Artikelserie:

Angesichts der enormen Menge an Telemetriedaten und Erkennungsregeln, die wir ausführen müssen, führt jede Leistungssteigerung zu erheblichen Kosteneinsparungen. Daher haben wir uns entschlossen, diese eigenartige Beobachtung zu untersuchen, um sie zu erklären und möglicherweise zusätzliche Möglichkeiten zur Leistungssteigerung zu entdecken. Dabei haben wir einiges gelernt und wollten es mit der breiteren Neighborhood teilen.

Einführung

Wir hatten das Gefühl, dass wir bei der Codegenerierung von Spark an eine Grenze stoßen würden. Daher ist ein wenig Hintergrundwissen zu diesem Thema erforderlich. Im Jahr 2014 führte Spark die Codegenerierung ein, um Ausdrücke der Type auszuwerten (id > 1 and id > 2) and (id < 1000 or (id + id) = 12)Dieser Artikel von Databricks erklärt es sehr intestine: Spannende Leistungsverbesserungen für Spark SQL in Sicht

Zwei Jahre später führte Spark die Entire-Stage-Codegenerierung ein. Diese Optimierung führt mehrere Operatoren zu einer einzigen Java-Funktion zusammen. Wie die Expression-Codegenerierung eliminiert die Entire-Stage-Codegenerierung virtuelle Funktionsaufrufe und nutzt CPU-Register für Zwischendaten. Sie wird jedoch nicht auf der Expression-Ebene, sondern auf der Operatorebene angewendet. Operatoren sind die Knoten in einem Ausführungsplan. Weitere Informationen finden Sie unter Apache Spark als Compiler: Eine Milliarde Zeilen professional Sekunde auf einem Laptop computer zusammenführen

Um diese Artikel zusammenzufassen, erstellen wir den Plan für diese einfache Abfrage:

clarify codegen
choose
id,
(id > 1 and id > 2) and (id < 1000 or (id + id) = 12) as take a look at
from
vary(0, 10000, 1, 32)

In dieser einfachen Abfrage verwenden wir zwei Operatoren: Vary zum Generieren von Zeilen und Choose zum Ausführen einer Projektion. Wir sehen diese Operatoren im physischen Plan der Abfrage. Beachten Sie das Sternchen (codegen id : 1)neben den Knoten und den zugehörigen

|== Bodily Plan ==
* Mission (2)
+- * Vary (1)

(1) Vary (codegen id : 1)
Output (1): (id#36167L)
Arguments: Vary (0, 10000, step=1, splits=Some(32))

(2) Mission (codegen id : 1)
Output (2): (id#36167L, (((id#36167L > 1) AND (id#36167L > 2)) AND ((id#36167L < 1000) OR ((id#36167L + id#36167L) = 12))) AS take a look at#36161)
Enter (1): (id#36167L)

. Dies zeigt an, dass diese beiden Operatoren mithilfe der Entire-Stage-Codegenerierung zu einer einzigen Java-Funktion zusammengeführt wurden.

Generated code:
/* 001 */ public Object generate(Object() references) {
/* 002 */ return new GeneratedIteratorForCodegenStage1(references);
/* 003 */ }
/* 004 */
/* 005 */ // codegenStageId=1
/* 006 */ ultimate class GeneratedIteratorForCodegenStage1 extends org.apache.spark.sql.execution.BufferedRowIterator {
/* 007 */ non-public Object() references;
/* 008 */ non-public scala.assortment.Iterator() inputs;
/* 009 */ non-public boolean range_initRange_0;
/* 010 */ non-public lengthy range_nextIndex_0;
/* 011 */ non-public TaskContext range_taskContext_0;
/* 012 */ non-public InputMetrics range_inputMetrics_0;
/* 013 */ non-public lengthy range_batchEnd_0;
/* 014 */ non-public lengthy range_numElementsTodo_0;
/* 015 */ non-public org.apache.spark.sql.catalyst.expressions.codegen.UnsafeRowWriter() range_mutableStateArray_0 = new org.apache.spark.sql.catalyst.expressions.codegen.UnsafeRowWriter(3);
/* 016 */
/* 017 */ public GeneratedIteratorForCodegenStage1(Object() references) {
/* 018 */ this.references = references;
/* 019 */ }
/* 020 */
/* 021 */ public void init(int index, scala.assortment.Iterator() inputs) {
/* 022 */ partitionIndex = index;
/* 023 */ this.inputs = inputs;
/* 024 */
/* 025 */ range_taskContext_0 = TaskContext.get();
/* 026 */ range_inputMetrics_0 = range_taskContext_0.taskMetrics().inputMetrics();
/* 027 */ range_mutableStateArray_0(0) = new org.apache.spark.sql.catalyst.expressions.codegen.UnsafeRowWriter(1, 0);
/* 028 */ range_mutableStateArray_0(1) = new org.apache.spark.sql.catalyst.expressions.codegen.UnsafeRowWriter(1, 0);
/* 029 */ range_mutableStateArray_0(2) = new org.apache.spark.sql.catalyst.expressions.codegen.UnsafeRowWriter(2, 0);
/* 030 */
/* 031 */ }
/* 032 */
/* 033 */ non-public void project_doConsume_0(lengthy project_expr_0_0) throws java.io.IOException {
/* 034 */ // frequent sub-expressions
/* 035 */
/* 036 */ boolean project_value_4 = false;
/* 037 */ project_value_4 = project_expr_0_0 > 1L;
/* 038 */ boolean project_value_3 = false;
/* 039 */
/* 040 */ if (project_value_4) {
/* 041 */ boolean project_value_7 = false;
/* 042 */ project_value_7 = project_expr_0_0 > 2L;
/* 043 */ project_value_3 = project_value_7;
/* 044 */ }
/* 045 */ boolean project_value_2 = false;
/* 046 */
/* 047 */ if (project_value_3) {
/* 048 */ boolean project_value_11 = false;
/* 049 */ project_value_11 = project_expr_0_0 < 1000L;
/* 050 */ boolean project_value_10 = true;
/* 051 */
/* 052 */ if (!project_value_11) {
/* 053 */ lengthy project_value_15 = -1L;
/* 054 */
/* 055 */ project_value_15 = project_expr_0_0 + project_expr_0_0;
/* 056 */
/* 057 */ boolean project_value_14 = false;
/* 058 */ project_value_14 = project_value_15 == 12L;
/* 059 */ project_value_10 = project_value_14;
/* 060 */ }
/* 061 */ project_value_2 = project_value_10;
/* 062 */ }
/* 063 */ range_mutableStateArray_0(2).reset();
/* 064 */
/* 065 */ range_mutableStateArray_0(2).write(0, project_expr_0_0);
/* 066 */
/* 067 */ range_mutableStateArray_0(2).write(1, project_value_2);
/* 068 */ append((range_mutableStateArray_0(2).getRow()));
/* 069 */
/* 070 */ }
/* 071 */
/* 072 */ non-public void initRange(int idx) {
/* 073 */ java.math.BigInteger index = java.math.BigInteger.valueOf(idx);
/* 074 */ java.math.BigInteger numSlice = java.math.BigInteger.valueOf(32L);
/* 075 */ java.math.BigInteger numElement = java.math.BigInteger.valueOf(10000L);
/* 076 */ java.math.BigInteger step = java.math.BigInteger.valueOf(1L);
/* 077 */ java.math.BigInteger begin = java.math.BigInteger.valueOf(0L);
/* 078 */ lengthy partitionEnd;
/* 079 */
/* 080 */ java.math.BigInteger st = index.multiply(numElement).divide(numSlice).multiply(step).add(begin);
/* 081 */ if (st.compareTo(java.math.BigInteger.valueOf(Lengthy.MAX_VALUE)) > 0) {
/* 082 */ range_nextIndex_0 = Lengthy.MAX_VALUE;
/* 083 */ } else if (st.compareTo(java.math.BigInteger.valueOf(Lengthy.MIN_VALUE)) < 0) {
/* 084 */ range_nextIndex_0 = Lengthy.MIN_VALUE;
/* 085 */ } else {
/* 086 */ range_nextIndex_0 = st.longValue();
/* 087 */ }
/* 088 */ range_batchEnd_0 = range_nextIndex_0;
/* 089 */
/* 090 */ java.math.BigInteger finish = index.add(java.math.BigInteger.ONE).multiply(numElement).divide(numSlice)
/* 091 */ .multiply(step).add(begin);
/* 092 */ if (finish.compareTo(java.math.BigInteger.valueOf(Lengthy.MAX_VALUE)) > 0) {
/* 093 */ partitionEnd = Lengthy.MAX_VALUE;
/* 094 */ } else if (finish.compareTo(java.math.BigInteger.valueOf(Lengthy.MIN_VALUE)) < 0) {
/* 095 */ partitionEnd = Lengthy.MIN_VALUE;
/* 096 */ } else {
/* 097 */ partitionEnd = finish.longValue();
/* 098 */ }
/* 099 */
/* 100 */ java.math.BigInteger startToEnd = java.math.BigInteger.valueOf(partitionEnd).subtract(
/* 101 */ java.math.BigInteger.valueOf(range_nextIndex_0));
/* 102 */ range_numElementsTodo_0 = startToEnd.divide(step).longValue();
/* 103 */ if (range_numElementsTodo_0 < 0) {
/* 104 */ range_numElementsTodo_0 = 0;
/* 105 */ } else if (startToEnd.the rest(step).compareTo(java.math.BigInteger.valueOf(0L)) != 0) {
/* 106 */ range_numElementsTodo_0++;
/* 107 */ }
/* 108 */ }
/* 109 */
/* 110 */ protected void processNext() throws java.io.IOException {
/* 111 */ // initialize Vary
/* 112 */ if (!range_initRange_0) {
/* 113 */ range_initRange_0 = true;
/* 114 */ initRange(partitionIndex);
/* 115 */ }
/* 116 */
/* 117 */ whereas (true) {
/* 118 */ if (range_nextIndex_0 == range_batchEnd_0) {
/* 119 */ lengthy range_nextBatchTodo_0;
/* 120 */ if (range_numElementsTodo_0 > 1000L) {
/* 121 */ range_nextBatchTodo_0 = 1000L;
/* 122 */ range_numElementsTodo_0 -= 1000L;
/* 123 */ } else {
/* 124 */ range_nextBatchTodo_0 = range_numElementsTodo_0;
/* 125 */ range_numElementsTodo_0 = 0;
/* 126 */ if (range_nextBatchTodo_0 == 0) break;
/* 127 */ }
/* 128 */ range_batchEnd_0 += range_nextBatchTodo_0 * 1L;
/* 129 */ }
/* 130 */
/* 131 */ int range_localEnd_0 = (int)((range_batchEnd_0 - range_nextIndex_0) / 1L);
/* 132 */ for (int range_localIdx_0 = 0; range_localIdx_0 < range_localEnd_0; range_localIdx_0++) {
/* 133 */ lengthy range_value_0 = ((lengthy)range_localIdx_0 * 1L) + range_nextIndex_0;
/* 134 */
/* 135 */ project_doConsume_0(range_value_0);
/* 136 */
/* 137 */ if (shouldStop()) {
/* 138 */ range_nextIndex_0 = range_value_0 + 1L;
/* 139 */ ((org.apache.spark.sql.execution.metric.SQLMetric) references(0) /* numOutputRows */).add(range_localIdx_0 + 1);
/* 140 */ range_inputMetrics_0.incRecordsRead(range_localIdx_0 + 1);
/* 141 */ return;
/* 142 */ }
/* 143 */
/* 144 */ }
/* 145 */ range_nextIndex_0 = range_batchEnd_0;
/* 146 */ ((org.apache.spark.sql.execution.metric.SQLMetric) references(0) /* numOutputRows */).add(range_localEnd_0);
/* 147 */ range_inputMetrics_0.incRecordsRead(range_localEnd_0);
/* 148 */ range_taskContext_0.killTaskIfInterrupted();
/* 149 */ }
/* 150 */ }
/* 151 */
/* 152 */ }

Der generierte Code zeigt deutlich, wie die beiden Operatoren zusammengeführt werden. project_doConsume_0 Der (id > 1 and id > 2) and (id < 1000 or (id + id) = 12)Funktion enthält den auszuwertenden Code

. Beachten Sie, wie dieser Code generiert wird, um diesen spezifischen Ausdruck auszuwerten. Dies ist eine Illustration der Generierung von Ausdruckscode. processNext Die ganze Klasse ist ein Operator mit einem project_doConsume_0Methode. Dieser generierte Operator führt sowohl die Projektions- als auch die Bereichsoperation aus. Innerhalb der while-Schleife in Zeile 117 sehen wir den Code zum Erzeugen von Zeilen und einen spezifischen Aufruf (keine virtuelle Funktion) zum

Dies veranschaulicht, was die Codegenerierung für ganze Phasen bewirkt.

Aufschlüsselung der Leistung Imagepath Nachdem wir nun ein besseres Verständnis für die Codegenerierung von Spark haben, versuchen wir zu erklären, warum die Aufteilung einer Abfrage mit 1000 Sigma-Regeln in kleinere eine bessere Leistung bringt. Betrachten wir eine SQL-Anweisung, die zwei Sigma-Regeln auswertet. Diese Regeln sind unkompliziert: Regel1 gleicht Ereignisse mit einem Imagepath endet mit ’schtask.exe‘, und Rule2 entspricht einem


choose /* #3 */
Imagepath,
CommandLine,
PID,
map_keys(map_filter(results_map, (okay,v) -> v = TRUE)) as matching_rules
from (
choose /* #2 */
*,
map('rule1', rule1, 'rule2', rule2) as results_map
from (
choose /* #1 */
*,
(lower_Imagepath like '%schtasks.exe') as rule1,
(lower_Imagepath like 'd:%') as rule2
from (
choose
decrease(PID) as lower_PID,
decrease(CommandLine) as lower_CommandLine,
decrease(Imagepath) as lower_Imagepath,
*
from (
choose
uuid() as PID,
uuid() as CommandLine,
uuid() as Imagepath,
id
from
vary(0, 10000, 1, 32)
)
)
)
)

beginnend mit „d:“. results_mapDie Auswahl mit der Bezeichnung #1 führt die Erkennung durch und speichert die Ergebnisse in neuen Spalten mit den Namen rule1 und rule2. Auswahl #2 gruppiert diese Spalten unter einer einzigen map_filter und schließlich wandelt choose #3 die Karte in ein Array von Matching-Regeln um. Es verwendet map_keys um nur die Einträge der Regeln zu behalten, die tatsächlich übereinstimmen, und dann

wird verwendet, um die Map-Einträge in eine Liste passender Regelnamen umzuwandeln.


== Bodily Plan ==
Mission (4)
+- * Mission (3)
+- * Mission (2)
+- * Vary (1)

...

(4) Mission
Output (4): (Imagepath#2, CommandLine#1, PID#0, map_keys(map_filter(map(rule1, EndsWith(lower_Imagepath#5, schtasks.exe), rule2, StartsWith(lower_Imagepath#5, d:)), lambdafunction(lambda v#12, lambda okay#11, lambda v#12, false))) AS matching_rules#9)
Enter (4): (lower_Imagepath#5, PID#0, CommandLine#1, Imagepath#2)

Drucken wir den Spark-Ausführungsplan für diese Abfrage aus:

Beachten Sie, dass Knoten Projekt (4) kein Code generiert. Knoten 4 hat eine Lambda-Funktion. Verhindert diese die Codegenerierung für die gesamte Section? Mehr dazu später.

+--------------------+--------------------+--------------------+--------------+
| Imagepath| CommandLine| PID| matched_rule|
+--------------------+--------------------+--------------------+--------------+
|09401675-dc09-4d0...|6b8759ee-b55a-486...|44dbd1ec-b4e0-488...| rule1|
|e2b4a0fd-7b88-417...|46dd084d-f5b0-4d7...|60111cf8-069e-4b8...| rule1|
|1843ee7a-a908-400...|d1105cec-05ef-4ee...|6046509a-191d-432...| rule2|
+--------------------+--------------------+--------------------+--------------+

Diese Abfrage ist nicht ganz das, was wir wollen. Wir möchten eine Ereignistabelle mit einer Spalte erstellen, die die Regel angibt, die erfüllt wurde. Etwa so: matching_rules Das ist ganz einfach. Wir müssen nur die


choose
Imagepath,
CommandLine,
PID,
matched_rule
from (
choose
*,
explode(matching_rules) as matched_rule
from (
/* unique assertion */
)
)

Spalte.

== Bodily Plan ==
* Mission (7)
+- * Generate (6)
+- Mission (5)
+- * Mission (4)
+- Filter (3)
+- * Mission (2)
+- * Vary (1)

...

(3) Filter
Enter (3): (PID#34, CommandLine#35, Imagepath#36)
Situation : (measurement(map_keys(map_filter(map(rule1, EndsWith(decrease(Imagepath#36),
schtasks.exe), rule2, StartsWith(decrease(Imagepath#36), d:)),
lambdafunction(lambda v#47, lambda okay#46, lambda v#47, false))), true) > 0)
...

(6) Generate (codegen id : 3)
Enter (4): (PID#34, CommandLine#35, Imagepath#36, matching_rules#43)
Arguments: explode(matching_rules#43), (PID#34, CommandLine#35, Imagepath#36), false, (matched_rule#48)

(7) Mission (codegen id : 3)
Output (4): (Imagepath#36, CommandLine#35, PID#34, matched_rule#48)
Enter (4): (PID#34, CommandLine#35, Imagepath#36, matched_rule#48)

Dadurch entstehen zwei zusätzliche Operatoren: Generieren (6) und Projizieren (7). Es gibt jedoch auch einen neuen Filter (3). explode Der explode Funktion generiert Zeilen für jedes Factor im Array. Wenn das Array leer ist,

erzeugt keine Zeilen und filtert effektiv Zeilen heraus, bei denen das Array leer ist. org.apache.spark.sql.catalyst.optimizer.InferFiltersFromGenerateSpark verfügt über eine Optimierungsregel, die die Explode-Funktion erkennt und diese zusätzliche Bedingung erzeugt. Der Filter ist ein Versuch von Spark, die Verarbeitung so weit wie möglich abzukürzen. Der Quellcode für diese Regel, genannt

erklärt es so:

Leitet Filter aus Generate ab, sodass Zeilen, die durch dieses Generate entfernt worden wären, früher entfernt werden können – vor Verknüpfungen und in Datenquellen. Weitere Einzelheiten zur Optimierung von Ausführungsplänen durch Spark finden Sie im Artikel von David Vrba.

Abfragepläne in Spark 3.0 meistern

Eine weitere Frage stellt sich: Profitieren wir von diesem zusätzlichen Filter? Beachten Sie, dass dieser zusätzliche Filter vermutlich aufgrund der Lambda-Funktion auch kein Code ist, der für die gesamte Section generiert wird. Versuchen wir, dieselbe Abfrage auszudrücken, jedoch ohne Verwendung einer Lambda-Funktion. map_filterStattdessen können wir die Regelergebnisse in eine Karte einfügen, diese Karte auflösen und die Zeilen herausfiltern. Dadurch wird die Notwendigkeit umgangen,


choose
Imagepath,
CommandLine,
PID,
matched_rule
from (
choose
*
from (
choose
*,
explode(results_map) as (matched_rule, matched_result)
from (
/* unique assertion */
)
)
the place
matched_result = TRUE
)

. matched_rule Die Choose #3-Operation zerlegt die Karte in zwei neue Spalten. matched_result enthält den Schlüssel, der den Regelnamen darstellt, während die matched_resultSpalte enthält das Ergebnis des Erkennungstests. Um die Zeilen zu filtern, behalten wir einfach nur diejenigen mit einem positiven

.


== Bodily Plan ==
* Mission (8)
+- * Filter (7)
+- * Generate (6)
+- * Mission (5)
+- * Mission (4)
+- * Filter (3)
+- * Mission (2)
+- * Vary (1)

Der physische Plan zeigt, dass es sich bei allen Knoten um Vollphasencode handelt, der in einer einzigen Java-Funktion generiert wird, was vielversprechend ist. map_filter Führen wir einige Assessments durch, um die Leistung der Abfrage zu vergleichen mit

und diejenige, die „explodieren“ und dann „filtern“ verwendet.

Wir haben diese Assessments auf einer Maschine mit 4 CPUs ausgeführt. Wir haben 1 Million Zeilen mit jeweils 100 Regeln generiert und jede Regel hat 5 Ausdrücke ausgewertet. Diese Assessments wurden 5 Mal ausgeführt.

  • Im Durchschnitt
  • map_filter dauerte 42,6 Sekunden

explode_then_filter dauerte 51,2 Sekunden

Daher ist map_filter etwas schneller, obwohl es keine Codegenerierung für die gesamte Section verwendet.

Brought on by: org.codehaus.commons.compiler.InternalCompilerException: Code grows past 64 KB

In unserer Produktionsabfrage führen wir jedoch viel mehr Sigma-Regeln aus – insgesamt 1000 Regeln. Dazu gehören 29 Regex-Ausdrücke, 529 „Gleich“-Ausdrücke, 115 „beginnt mit“-Ausdrücke, 2352 „endet mit“-Ausdrücke und 5838 „enthält“-Ausdrücke. Lassen Sie uns unsere Abfrage noch einmal testen, diesmal jedoch mit einer leichten Erhöhung der Anzahl der Ausdrücke, indem wir 7 statt 5 professional Regel verwenden. Dabei ist in unseren Protokollen ein Fehler aufgetreten: spark.sql.codegen.maxFields Wir haben versucht, spark.sql.codegen.hugeMethodLimitUnd

aber grundsätzlich haben Java-Klassen eine Funktionsgrößenbeschränkung von 64 KB. Darüber hinaus beschränkt sich der JVM-JIT-Compiler auf das Kompilieren von Funktionen, die kleiner als 8 KB sind.

Die Abfrage läuft jedoch weiterhin einwandfrei, da Spark für bestimmte Teile des Plans auf das Volcano-Ausführungsmodell zurückgreift. WholeStageCodeGen ist schließlich nur eine Optimierung.

  • Wenn Sie den gleichen Take a look at wie zuvor ausführen, jedoch mit 7 Ausdrücken professional Regel statt 5, ist explode_then_filter viel schneller als map_filter.
  • map_filter dauerte 68,3 Sekunden

explode_then_filter dauerte 15,8 Sekunden org.apache.spark.sql.catalyst.optimizer.InferFiltersFromGenerate Eine Erhöhung der Anzahl der Ausdrücke führt dazu, dass Teile des explode_then_filter nicht mehr als Code für die gesamte Section generiert werden. Insbesondere der Filteroperator, der durch die Regel eingeführt wurde

spark.sql("SET spark.sql.optimizer.excludedRules=org.apache.spark.sql.catalyst.optimizer.InferFiltersFromGenerate")

ist zu groß, um in die Codegenerierung der gesamten Section integriert zu werden. Sehen wir uns an, was passiert, wenn wir die Regel InferFiltersFromGenerate ausschließen:


== Bodily Plan ==
* Mission (6)
+- * Generate (5)
+- Mission (4)
+- * Mission (3)
+- * Mission (2)
+- * Vary (1)

== Bodily Plan ==
* Mission (7)
+- * Filter (6)
+- * Generate (5)
+- * Mission (4)
+- * Mission (3)
+- * Mission (2)
+- * Vary (1)

Wie erwartet verfügt der physische Plan beider Abfragen nicht mehr über einen zusätzlichen Filteroperator.

  • Das Entfernen der Regel hatte tatsächlich erhebliche Auswirkungen auf die Leistung:
  • map_filter dauerte 22,49 Sekunden

explode_then_filter dauerte 4,08 Sekunden

Beide Abfragen profitierten stark vom Entfernen der Regel. Angesichts der verbesserten Leistung beschlossen wir, die Anzahl der Sigma-Regeln auf 500 und die Komplexität auf 21 Ausdrücke zu erhöhen:

  • Ergebnisse:
  • map_filter dauerte 195,0 Sekunden

explode_then_filter dauerte 25,09 Sekunden

Trotz der erhöhten Komplexität liefern beide Abfragen noch immer eine ziemlich gute Leistung, wobei explode_then_filter map_filter deutlich übertrifft.

Es ist interessant, die verschiedenen Aspekte der Codegenerierung zu untersuchen, die Spark verwendet. Auch wenn wir derzeit möglicherweise nicht von der Codegenerierung für die gesamte Section profitieren, können wir dennoch von der Ausdrucksgenerierung profitieren. spark.sql.codegen.methodSplitThreshold Die Generierung von Ausdrücken unterliegt nicht den gleichen Einschränkungen wie die Generierung von Code in der gesamten Section. Sehr große Ausdrucksbäume können in kleinere aufgeteilt werden, und Sparks

steuert, wie diese aufgeteilt werden. Obwohl wir mit dieser Eigenschaft experimentiert haben, konnten wir keine signifikanten Verbesserungen feststellen. Die Standardeinstellung scheint zufriedenstellend. spark.sql.codegen.factoryModeSpark bietet eine Debug-Eigenschaft namens spark.sql.codegen.factoryMode=NO_CODEGENdas auf FALLBACK, CODEGEN_ONLY oder NO_CODEGEN gesetzt werden kann. Wir können die Generierung von Ausdruckscodes deaktivieren, indem wir

was zu einer drastischen Leistungsverschlechterung führt:

  • Mit 500 Regeln und 21 Ausdrücken:
  • map_filter dauerte 1581 Sekunden

explode_then_filter dauerte 122,31 Sekunden.

Auch wenn nicht alle Operatoren an der Codegenerierung für die gesamte Section teilnehmen, stellen wir dennoch erhebliche Vorteile bei der Codegenerierung für Ausdrücke fest.

Die Ergebnisse

Bild vom Autor

Mit unserem Bestfall von 25,1 Sekunden zum Auswerten von 10.500 Ausdrücken in 1 Million Zeilen erreichen wir eine sehr respektable Charge von 104 Millionen Ausdrücken professional Sekunde professional CPU. map_filter Das Fazit dieser Studie ist, dass wir bei der Auswertung einer großen Anzahl von Ausdrücken davon profitieren, unsere Abfragen, die org.apache.spark.sql.catalyst.optimizer.InferFiltersFromGenerate zu denen, die einen Explodieren-und-Filtern-Ansatz verwenden. Darüber hinaus

Regel scheint in unserem Anwendungsfall nicht vorteilhaft zu sein, daher sollten wir diese Regel aus unseren Abfragen ausschließen.

Erklärt es unsere ersten Beobachtungen?

Die Umsetzung dieser Erkenntnisse in unseren Produktionsjobs brachte erhebliche Vorteile. Aber auch nach diesen Optimierungen brachte die Aufteilung der großen Abfrage in mehrere kleinere weiterhin Vorteile. Bei weiterer Untersuchung stellten wir fest, dass dies nicht nur auf die Codegenerierung zurückzuführen struggle, sondern eine einfachere Erklärung hatte.

Beim Spark-Streaming wird ein Mikro-Batch vollständig ausgeführt und anschließend der Fortschritt überprüft, bevor ein neuer Mikro-Batch gestartet wird.

Während jedes Mikrobatches muss Spark alle seine Aufgaben erledigen, normalerweise 200. Allerdings sind nicht alle Aufgaben gleich. Spark verwendet eine Spherical-Robin-Strategie, um Zeilen auf diese Aufgaben zu verteilen. Daher können einige Aufgaben gelegentlich Ereignisse mit großen Attributen enthalten, z. B. eine sehr große Befehlszeile, wodurch bestimmte Aufgaben schnell abgeschlossen werden, während andere viel länger dauern. Hier beispielsweise die Verteilung der Ausführungszeit einer Mikrobatch-Aufgabe. Die mittlere Aufgabenzeit beträgt 14 Sekunden. Der schlimmste Nachzügler benötigt jedoch 1,6 Minuten!

Bild vom Autor

Dies wirft tatsächlich Licht auf ein anderes Phänomen. Die Tatsache, dass Spark während jedes Mikro-Batches auf ein paar Nachzügler-Aufgaben wartet, lässt viele CPUs ungenutzt, was erklärt, warum die Aufteilung der großen Abfrage in mehrere kleinere Abfragen zu einer schnelleren Gesamtleistung führte.

Dieses Bild zeigt 5 kleinere Abfragen, die parallel in derselben Spark-Anwendung ausgeführt werden. Batch3 wartet auf eine Nachzügleraufgabe, während die anderen Abfragen weiter ausgeführt werden.

Bild vom Autor

Während dieser Wartezeiten kann Spark die inaktiven CPUs für die Bearbeitung anderer Abfragen nutzen und so die Ressourcennutzung und den Gesamtdurchsatz maximieren.

Abschluss

In diesem Artikel haben wir einen Überblick über den Codegenerierungsprozess von Spark gegeben und erläutert, dass integrierte Optimierungen nicht immer zu den gewünschten Ergebnissen führen. Darüber hinaus haben wir gezeigt, dass die Umgestaltung einer Abfrage von Lambda-Funktionen zu einer Abfrage mit einer einfachen Explode-Operation zu Leistungsverbesserungen führte. Schließlich kamen wir zu dem Schluss, dass das Aufteilen einer großen Anweisung zwar zu Leistungssteigerungen führte, der Hauptfaktor für diese Verbesserungen jedoch die Ausführungstopologie und nicht die Abfragen selbst struggle.

[ad_2]

]]>
http://chatgpz.de/machine-learning/leistungseinblicke-aus-sigma-regelerkennungen-in-spark-streaming-von-jean-claude-cote-juni-2024/feed/ 0 1075