Wir nutzen die Eleganz von kdb+ und die Leistungsfähigkeit von Rust, um Datenanwendungen zu erstellen, die Daten mit einer Geschwindigkeit von zehn GB/Sekunde auf Consumer-Hardware verarbeiten können.


Bei Red Sift nutzen wir unsere Plattform, um Data-Anwendungen der nächsten Generation für Cybersicherheit zu betreiben. Auf einer hohen Ebene ist unsere Plattform eine dieser trendigen Serverless-Compute-Umgebungen, außer dass unsere es Ihnen ermöglicht, echte Arbeit zu erledigen. Red Sift lässt uns Daten leiten, transformieren/erweitern und schließlich aggregieren, um Erkenntnisse zu finden, die Organisationen und Einzelpersonen schützen. Alle diese Funktionen kommen mit dem erforderlichen Gerüst, um reale Cloud-SaaS-Anwendungen zu erstellen. Wir sprechen von Keyed APIs, Web-Interfaces, rollenbasierten Berechtigungen – alles entscheidend für die Enterprise-Grade-Software, die wir für einige der größten Organisationen der Welt bereitstellen. Eine der coolsten Funktionen der Plattform ist die Fähigkeit, verschiedene Technologiekomponenten in eine einzige Anwendung zu integrieren, ohne Glue Code. Mit dieser Fähigkeit haben wir kürzlich kdb+ zu unserer Kernplattform hinzugefügt, da wir ein neues Cybersicherheitsprodukt entwickeln, das stark auf die Verarbeitung von Zeitreihendaten angewiesen ist.


Zunächst ein wenig zu kdb+. Man kann es sich als eine Plattform vorstellen, um mithilfe einer Box mächtiger Primitiven eine eigene Datenbank zu erstellen. Das mag komplex klingen, aber kdb+ und die treibende Sprache q machen es relativ schmerzlos. Der Hauptvorteil ist die vollständige Kontrolle über das Layout und die Zugriffsmuster, sowohl im Speicher als auch, sobald Sie die Daten auf Ihr Speichermedium der Wahl aufgeteilt haben. Diese Kontrolle ermöglicht es, eine erstaunliche Leistung zu erzielen, weshalb kdb+ in Bereichen wie den Finanzdienstleistungen intensiv genutzt wird, wo Latenz (das einzige harte Problem der Informatik) sich in viel Geld übersetzt.
Als nächstes, Rust. Wir nutzen Rust im Software-Agenten für unser neues IDS-Produkt, also begannen wir, damit auch für unsere Datenpipeline zu experimentieren. Rust ist eine enorm ausgereifte Sprache, die gerade einen Moment erlebt, unter anderem durch die Übernahme im neuen Firefox. Im Browser von Mozilla ersetzt es jetzt einige der haarigsten Teile des Browsers und erschließt marktführende Leistung. Rust basiert auf der LLVM-Toolchain, die ihm einige der qualitativ hochwertigsten native Code-Generierung auf dem Markt verleiht. Ebenfalls entscheidend für das, was wir damit vorhaben, bietet es Kompatibilität mit dem C-Speicherlayout und den Aufrufkonventionen. Das ist ein großer Vorteil, da wir bei unserer anderen Systemprogrammiersprache der Wahl oft auf diese Einschränkung stoßen.
Man nutzt kdb+, wenn man die schnellste Lösung braucht, also stellten wir die Frage: Wie schnell kann man werden? Wenn wir diese beiden Technologien kombinieren, können wir einen Zeitreihen-Datenstapel auf Steroiden erstellen. Während der vollständige technische Detail den Rahmen eines Blogbeitrags übersteigt, haben wir gerade eine kleine, aber nützliche Bindung als Open Source veröffentlicht, die uns Zugriff auf moderne Hashing-Funktionen in q gibt.
Hash++ : Hinzufügen einer einfachen Erweiterung
Eine Operation, die wir häufig durchführen, ist das Hashing. Wir müssen Identitätsfunktionen für die von uns aufgenommenen Daten generieren, damit wir effizient über mehrere Felder aggregieren und suchen können. Standardmäßig unterstützt q den guten alten MD5-Hash, aber wir können es besser machen.
Wir suchen nach einem nicht-kryptografischen Hash und ich bin besonders angetan von der smhasher-Bibliothek, da sie eine hardwarebeschleunigte Version von MetroHash enthält. Konkret enthält sie eine SSE4.2-Implementierung auf modernen x86-Prozessoren, die mit vielen GB (das sind Bytes, nicht Bits)/s vorankommen kann. Wir wollten dies über Rust in kdb+ einbinden und waren gespannt, ob wir einen Teil dieser Leistung in der realen Welt realisieren könnten.
Wir haben eine kleine Rust-Bibliothek erstellt, die zu und von kdb+-Typen abbildet. Sie enthält eine .q-Datei, die die Rust-Funktionen einbindet, die mit C-Aufrufkonventionen exportiert wurden, sowie einige Tests und Benchmarks.
Unsere Bindung erstellt 2 neue Hash-Funktionen basierend auf der 64-Bit- (rmetro64) und 128-Bit-Variante (rmetro128) des Metro-Hashs, die von q auf eine Weise aufgerufen werden können, die größtenteils mit der eingebauten MD5-Implementierung kompatibel ist.
q)\l krs-hash.q q)md5 “hello world” 0x5eb63bbbe01eeed093cb22bb8f5acdc3 q)rmetro128 “hello world” 32bd829a-47d0-8cb3-3b25-9bfbdfc07543 [/shell] Beachten Sie, dass die 128-Bit-Version von rmetro den Wert des Hashs als nativen q-Typ GUID zurückgibt, um den Vergleich und die Bereichsbildung zu erleichtern. Das eingebaute MD5 kann mit folgendem in einen kompatiblen Rückgabetyp umgewandelt werden:
Wie schnell?
Wir können schnell einige Testdaten mit einem q-Skript generieren und einige Szenarien testen. [shell]q krs-hash-dict-bench.q[/shell] testet einige Dictionary-Transformationen, bevor das Ganze gehasht wird.


Die moderne Hashing-Implementierung in Rust ist zwischen 8x und 18x schneller als das eingebaute MD5 und ebenso einfach zu verwenden. Der Leistungsunterschied wächst, je größer die zu hashenden Daten werden, wie zu erwarten ist. Die heiße Schleife im Metro-Hash ist eng optimiert und amortisiert die kleinen, aber vorhandenen Kosten des Aufrufs der Bibliothek und der zugehörigen Bindungen. Wir bewegen uns nahe an 11GB/s bei größeren Parametergrößen. Ein einfacherer In-Memory-String-Hash, ebenfalls im Repository, erreicht 16GB/s beim 64-Bit-Hash auf derselben Hardware.


Diese Version testet auch eine Variante des 128-Bit-Hashs, die direkt auf q MixedLists operiert und eine Liste von GUIDs ausgibt, hier als METRO128_LIST dargestellt. Bei kleinen Größen ist dieser Ansatz nützlich, da er die Kosten des Aufrufs reduziert, obwohl die Speicher-Overheads höher sind.
Bei diesen Geschwindigkeiten sind wir in Wurfweite der maximalen Speicherbandbreite des DDR4-Speichers auf meinem Testrechner. 2.4Ghz Speicher * 2 Speicherkanäle * 8 Bytes (64 Bit) pro Takt = 38.4GB/s ist so schnell, wie ich möglicherweise werden könnte, wenn ich nur große Puffer kopieren würde.
Wir können dies komplexer gestalten, indem wir q dazu bringen, Hashes aus Dictionary-Inhalten zu erstellen, die von einem speicherabgebildeten Anymap eingelagert werden.


Wir können die Kosten des Lesens von Daten von einem Laufwerk sehen, wenn wir die Durchsatzwerte für jede Gesamteintragsgröße vergleichen, aber unsere Datenraten bleiben angenehm beeindruckend. Wir streamen Hashes mit einer Rate von 1,7GB/s von der Festplatte.
Offensichtlich ist ein einfacher Hash eine minimale Datentransformationsfunktion, aber dies dient als Basislinie, um zu demonstrieren, dass diese neuartige Architektur Daten nahe an den Grenzen der Hardware transformieren kann, während sie gleichzeitig High-Level-Abstraktionen ermöglicht.
Profiling auf macOS
Nebenbei bemerkt, wenn man auf einem Mac entwickelt (wie wir es tun), kann Instruments, Teil des Apple XCode-Pakets, einige Einblicke in die Leistung Ihrer nativen Bibliothek geben. Auf Linux gibt es einige bessere Optionen, aber sowohl das Einbetten von Rust in einen q-Prozess als auch macOS machen die Dinge etwas schwieriger. Im Folgenden führe ich manuell den Metro-Hash auf etwa 1GB Stringdaten aus und profiliere ihn mit dem Time Profile Instrument. Während Instruments nicht viel über die q-Binärdatei oder Symbole weiß, wie man erwarten würde, liefert unser Rust-Code einen interessanten Eintrag.


Konkret können wir sehen, dass ein erheblicher Teil der Zeit für die Überprüfung des UTF8-Strings aufgewendet wird, bevor ein Rust [rust]&str[/rust] erstellt wird. Das liegt daran, dass die Rust-zu-kdb+-Bindung, die von unserem Hasher verwendet wird, eine sinnvolle geprüfte Konvertierung durchführt, da Rust-Strings UTF8 sein müssen, um definiertes Verhalten zu gewährleisten. Für diesen Anwendungsfall interessiert sich der Hash jedoch wirklich nur für die Byte-Sequenz, sodass wir die [rust]unchecked_utf8[/rust]-Funktion des Crates aktivieren und dieses Sicherheitsventil entfernen können.


Ein zweiter Durchlauf mit Instruments mit aktivierter Funktion zeigt mir nun, dass der Großteil der CPU-Zeit für die Berechnung des 128-Bit-Metro-Hashs aufgewendet wird. Wie erwartet ist die in q gemessene Hash-Leistung zwischen 10% und 15% schneller.
Eine einfache Sache, die wir tun können, ist, Unterstützung für das Hashing von Dictionaries in unserer Bibliothek hinzuzufügen. Das macht die Komponente einfach zu verwenden, sodass wir einfach ein sortiertes Schlüssel/Wert-Set übergeben und eine Identität des Inhalts generieren können.
Wenn Sie sich das Repository ansehen, werden Sie feststellen, dass die Bibliothek einige Punkte enthält, die wir hier nicht besprochen haben.
1. Systemzuweisung
Standardmäßig verwendet Rust den jemalloc-Allocator. Einige Tests für diesen Anwendungsfall auf macOS zeigen uns jedoch, dass der Systemspeicher-Allocator in dieser benchmarkten heißen Schleife etwa 5% schneller ist und Instruments Sichtbarkeit auf die Zuweisungen gibt. Dies erfordert den Nightly-Rust-Channel und die [rust]alloc_system[/rust]-Funktion.
2. Rust-Fehler
Wir müssen auch die q-Laufzeitumgebung vor jeglichen Rust-Fehlern schützen, z.B. einem [rust].unwrap()[/rust]-Panic, der über die Bibliotheksgrenze hinaus aufsteigt. Daher werden Sie sehen, dass jeder Aufruf umschlossen ist.
3. Symbole in Rust-Binärdateien
Cargo unterstützt derzeit kein Release-Build mit Debug-Symbolen-Profil. Diese Build-Konfiguration wird üblicherweise für Profiler wie Instruments verwendet. Sie werden feststellen, dass die Cargo-Konfiguration dies in den Release-Build eingepatcht hat.
4. Parallele Verarbeitung
Alles, worüber wir bisher gesprochen haben, ist ein einzelner Kern. Es gibt viel zu behandeln, wenn man [js]rmetro128 each data[/js] vs. [js]rmetro128 peach data[/js] vs. [js].Q.fc[{rmetro128 x}][/js] data untersucht, da die parallelen Optionen eine neue Reihe von Kompromissen mit sich bringen. Darüber hinaus unterstützt die Bibliothek auch nicht-atomare Typen, die Listen abbilden, z.B. Listen von Strings in Listen von Hashes, und die Erstellung einer Hash-Identität für den Inhalt eines Dictionaries. Dies und mehr wird in einem zukünftigen Beitrag untersucht.
In summary, a combination of the excellent and flexible data storage models available in kdb+ and the power and interoperability of Rust backed by the LLVM toolchain allows us to ingest and process data at the rate of > 10 GB/s on a single host. We will be announcing our next generation cyber products built on this novel architecture very soon.
Vielen Dank an meine Kollegen rsdy & dc, die mir geholfen haben, die Daten für dieses Experiment zusammenzustellen.
Bis zum Ende gelesen? Sie würden wahrscheinlich gerne daran arbeiten. Schicken Sie uns eine Nachricht mit Ihrem GitHub/Bitbucket/GitLab-Link und/oder einem klassischen Lebenslauf an jobs@redsift.com
Bearbeitet 25.-Aug.-18: Korrektur, um festzuhalten, dass Symbole in Release-Binärdateien ein Cargo-Problem sind, kein Rust-Problem.
[shell] KDB+ 3.6 2018.06.14 Copyright (C) 1993-2018 Kx Systems
m64/ 12()core 16384MB
[shell] q)/ turn the byte sequence into a GUID using sv
q)0x0 sv md5 “hello world”
5eb63bbb-e01e-eed0-93cb-22bb8f5acdc3
[/shell]
[rust] #![feature(alloc_system)] extern crate alloc_system;
[/rust]
[rust] let result = catch_unwind(|| {
…
panic!(“Oops!”);
});
[/rust]
[ini] [profile.release] opt-level = 3
debug = true
rpath = false
lto = false
debug-assertions = false
codegen-units = 1
[/ini]
Rahul Powar is the co-founder and CEO of Red Sift. A technologist and thought leader in both AI and cybersecurity, Rahul's career previously includes Thomas Reuters and the Shazam.




