Sfruttiamo l'eleganza di kdb+ e la potenza di Rust per creare applicazioni dati che possono elaborare dati alla velocità di decine di GB/secondo su hardware di livello consumer.


In Red Sift, utilizziamo la nostra piattaforma per alimentare applicazioni dati di nuova generazione per la cybersecurity. A un livello generale, la nostra piattaforma è uno di quegli ambienti di elaborazione serverless alla moda, tranne per il fatto che la nostra ti permette di portare a termine un lavoro reale. Red Sift ci permette di far circolare i dati, trasformarli/potenziarli e infine aggregarli per trovare informazioni utili che aiutano a mantenere al sicuro organizzazioni e individui. Tutte queste funzionalità sono accompagnate dall'impalcatura necessaria per costruire applicazioni SaaS cloud reali. Parliamo di API con chiavi, interfacce web, permessi basati sui ruoli – tutti elementi critici per il software di livello enterprise che forniamo ad alcune delle più grandi organizzazioni al mondo. Una delle funzionalità più interessanti della piattaforma è la capacità di integrare diversi componenti tecnologici in un'unica applicazione senza codice di collegamento. Utilizzando questa capacità, recentemente abbiamo aggiunto kdb+ alla nostra piattaforma principale mentre stiamo costruendo un nuovo prodotto di cybersecurity che si basa pesantemente sull'elaborazione di dati di serie temporali.


Prima di tutto, qualche informazione su kdb+. Puoi pensarla come una piattaforma per costruire il proprio database usando un insieme di potenti primitive. Sebbene questo sembri complesso, kdb+ e il linguaggio q che la guida rendono il processo relativamente indolore. Il vantaggio principale è il controllo totale sul layout e sui pattern di accesso, sia in memoria che una volta che hai distribuito i dati sul tuo supporto di archiviazione preferito. Questo controllo permette di sbloccare prestazioni straordinarie, motivo per cui kdb+ è utilizzato ampiamente in settori come i servizi finanziari, dove la latenza (l'unico vero problema difficile dell'informatica) si traduce in grandi guadagni.
Ora, Rust. Utilizziamo Rust nell'agente software per il nostro nuovo prodotto IDS, quindi abbiamo iniziato a sperimentarlo anche per l'uso nella nostra pipeline di dati. Rust è un linguaggio straordinariamente sofisticato che sta vivendo un momento di popolarità, in parte grazie alla sua adozione nel nuovo Firefox. Nel browser di Mozilla sta ora sostituendo alcune delle parti più complesse del browser, sbloccando prestazioni leader nel settore. Rust è costruito sulla toolchain LLVM, che gli conferisce una delle generazioni di codice nativo di più alta qualità sul mercato. Inoltre, in modo critico per quello che stiamo per fare con esso, fornisce compatibilità con il layout di memoria e le convenzioni di chiamata di C. Questo è un grande vantaggio poiché spesso ci troviamo di fronte a questa limitazione con il nostro altro linguaggio di programmazione a livello di sistema preferito.
Si usa kdb+ quando serve la soluzione più veloce, quindi ci siamo posti la domanda: quanto si può andare veloci? Quando combiniamo questi due elementi tecnologici, possiamo costruire uno stack di dati di serie temporali potenziato. Sebbene i dettagli tecnici completi vadano oltre lo scopo di un post del blog, abbiamo appena rilasciato in open source un binding piccolo ma utile che ci dà accesso a funzioni di hashing moderne in q.
Hash++: Aggiungere una semplice estensione
Un'operazione che finiamo per fare molto spesso è l'hashing. Dobbiamo generare funzioni di identità per i dati che ingeriamo, così da poter aggregare e cercare in modo efficiente su più campi. Di base, q supporta il buon vecchio hash MD5, ma possiamo fare di meglio.
Stiamo cercando un hash non crittografico e sono particolarmente affezionato alla libreria smhasher poiché include una versione accelerata via hardware di MetroHash. In particolare include un'implementazione SSE4.2 sui processori x86 moderni che può procedere a molti GB (ovvero byte, non bit) al secondo. Volevamo integrare questo in kdb+ tramite Rust ed eravamo curiosi di vedere se potevamo realizzare parte di queste prestazioni nel mondo reale.
Abbiamo creato una piccola libreria Rust che mappa da e verso i tipi kdb+. Include un file .q che collega le funzioni Rust esportate con convenzioni di chiamata C e alcuni test e benchmark.
Il nostro binding crea 2 nuove funzioni hash basate sulla variante a 64 bit (rmetro64) e 128 bit (rmetro128) dell'hash Metro, che possono essere chiamate da q in un modo perlopiù compatibile con l'implementazione MD5 integrata.
q)\l krs-hash.q q)md5 “hello world” 0x5eb63bbbe01eeed093cb22bb8f5acdc3 q)rmetro128 “hello world” 32bd829a-47d0-8cb3-3b25-9bfbdfc07543 [/shell] Nota che la versione a 128 bit di rmetro restituisce il valore dell'hash come tipo nativo q GUID per un confronto e un ranging più semplici. L'MD5 integrato può essere convertito in un tipo di ritorno compatibile utilizzando quanto segue:
Quanto è veloce?
Possiamo generare rapidamente alcuni dati fittizi con uno script q e testare alcuni scenari. [shell]q krs-hash-dict-bench.q[/shell] testa alcune trasformazioni di dizionari prima di eseguire l'hashing dell'intero contenuto.


L'implementazione moderna dell'hashing in Rust è tra 8 e 18 volte più veloce dell'MD5 integrato ed è altrettanto facile da usare. La differenza di prestazioni aumenta con l'aumentare della dimensione dei dati sottoposti a hashing, come ci si potrebbe aspettare. Il ciclo caldo nell'hash Metro è finemente ottimizzato e ammortizza il costo piccolo ma presente della chiamata alla libreria e ai binding associati. Ci stiamo spostando verso quasi 11GB/s alle dimensioni dei parametri più grandi. Un semplice hash di stringhe in memoria, anch'esso nel repository, raggiungerà il picco di 16GB/s per l'hash a 64 bit sullo stesso hardware.


Questa versione testa anche una variante dell'hash a 128 bit che opera direttamente su MixedList di q ed emette una lista di GUID, rappresentata qui come METRO128_LIST. Per dimensioni piccole, questo approccio è utile poiché riduce il costo della chiamata, sebbene gli overhead di memoria siano più elevati.
A quelle velocità, siamo a distanza di tiro dalla larghezza di banda massima della memoria DDR4 sulla mia macchina di test. 2.4Ghz di memoria * 2 canali di memoria * 8 byte (64 bit) per clock = 38.4GB/s è la velocità massima che potrei raggiungere se stessi semplicemente copiando grandi buffer in giro.
Possiamo rendere questo più complesso facendo generare a q hash dal contenuto di dizionari che vengono caricati in memoria da un Anymap mappato in memoria.


Possiamo vedere il costo della lettura dei dati da un disco quando confrontiamo i valori del throughput per ogni dimensione totale delle voci, ma i nostri tassi di dati rimangono comodamente impressionanti. Stiamo eseguendo lo streaming di hash alla velocità di 1.7GB/s da disco.
Ovviamente, un semplice hash è una funzione di trasformazione dati minimale, ma serve come base per dimostrare che questa architettura innovativa può trasformare i dati vicino ai limiti dell'hardware, pur consentendo abstrazioni di alto livello.
Profilazione su macOS
Come nota a margine, quando si sviluppa su un Mac (come facciamo noi), Instruments, parte del pacchetto Apple XCode, può fornire alcune informazioni sulle prestazioni della propria libreria nativa. Su Linux ci sono diverse opzioni migliori, ma sia l'incorporazione di Rust in un processo q sia macOS rendono le cose un po' più difficili. Di seguito, eseguo manualmente l'hash metro su circa 1GB di dati di stringhe e ne faccio la profilazione utilizzando lo strumento Time Profile. Sebbene Instruments non sappia molto sul binario q o sui simboli, come ci si aspetterebbe, il nostro codice Rust fornisce una voce interessante.


In particolare, possiamo vedere che una parte significativa del tempo viene spesa nel controllo della stringa UTF8 prima di creare uno [rust]&str[/rust] Rust. Questo accade perché il binding Rust verso kdb+ utilizzato dal nostro hasher effettua una conversione controllata sensata, poiché le stringhe Rust devono essere UTF8 per un comportamento definito. Tuttavia, per questo caso d'uso l'hash si preoccupa davvero solo della sequenza di byte, quindi possiamo attivare la funzionalità [rust]unchecked_utf8[/rust] del crate e rimuovere questa valvola di sicurezza.


Un secondo passaggio con Instruments con la funzionalità attivata mostra ora che la maggior parte del tempo CPU viene spesa nel calcolo dell'hash metro a 128 bit. Come previsto, le prestazioni dell'hash misurate in q sono tra il 10% e il 15% più veloci.
Una cosa semplice che possiamo fare è aggiungere il supporto per l'hashing dei dizionari nella nostra libreria. Questo rende il componente facile da usare, così possiamo semplicemente passare un insieme ordinato di chiavi/valori e generare un'identità del contenuto.
Se guardi il repository, noterai che la libreria include alcuni elementi che non abbiamo discusso qui.
1. Allocazione di sistema
Per impostazione predefinita, Rust utilizzerà l'allocatore jemalloc. Tuttavia, alcuni test per questo caso d'uso su macOS ci mostrano che l'allocatore di memoria di sistema è circa il 5% più veloce in questo ciclo caldo sottoposto a benchmark e offre visibilità a Instruments sulle allocazioni. Questo richiede il canale nightly di Rust e la funzionalità [rust]alloc_system[/rust].
2. Errori Rust
Dobbiamo anche proteggere il runtime q da eventuali errori Rust, ad esempio un panic da [rust].unwrap()[/rust] che risale attraverso il confine della libreria. Di conseguenza, vedrai ogni chiamata avvolta.
3. Simboli nei binari Rust
Cargo attualmente non supporta un profilo di build release con simboli di debug. Questa configurazione di build viene solitamente utilizzata per profiler come Instruments. Noterai che la configurazione Cargo ha questo elemento applicato come patch nella build release.
4. Elaborazione parallela
Tutto ciò di cui abbiamo parlato finora riguarda un singolo core. C'è molto da approfondire nell'esplorare [js]rmetro128 each data[/js] rispetto a [js]rmetro128 peach data[/js] rispetto a [js].Q.fc[{rmetro128 x}][/js] data, poiché le opzioni parallele introducono una nuova serie di compromessi. Inoltre, la libreria supporta anche il mapping di tipi non atomici come liste, ad esempio liste di stringhe in liste di hash, e la creazione di un'identità hash per il contenuto di un dizionario. Questo e altro sarà esplorato in un post futuro.
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.
Un grande grazie ai miei colleghi rsdy e dc che mi hanno aiutato a raccogliere i dati per questo esperimento.
Sei arrivato a leggere fino alla fine? Probabilmente ti piacerebbe lavorare su questo. Scrivici con il tuo link GitHub/Bitbucket/GitLab e/o un CV vecchio stile all'indirizzo jobs@redsift.com
Modificato il 25 agosto 2018: Correzione per notare che i simboli nei binari release sono un problema di Cargo, non di Rust.
[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.




