Nous tirons parti de l’élégance de kdb+ et de la puissance de Rust pour créer des applications de données capables de traiter des données à un débit de dizaines de Go/seconde sur du matériel grand public.


Chez Red Sift, nous utilisons notre plateforme pour alimenter les applications de données de nouvelle génération dans la cybersécurité. À un niveau élevé, notre plateforme est l’un de ces environnements informatiques serverless à la mode, sauf que le nôtre vous permet de faire du vrai travail. Red Sift nous permet de faire circuler les données, de les transformer/augmenter et enfin de les agréger pour trouver des informations qui contribuent à protéger les organisations et les individus. Toutes ces fonctions bénéficient de la structure nécessaire pour construire de véritables applications cloud SaaS. Il s’agit d’API à clé, d’interfaces web, de permissions basées sur les rôles — tout ce qui est crucial pour les logiciels de niveau entreprise que nous fournissons à certaines des plus grandes organisations du monde. L’une des fonctionnalités les plus impressionnantes de la plateforme est la capacité à intégrer différents composants technologiques dans une application unique, sans code d’interfaçage. Grâce à cette capacité, nous avons récemment ajouté kdb+ à notre plateforme core alors que nous développons un nouveau produit de cybersécurité qui repose largement sur le traitement des séries temporelles.


Tout d’abord, un mot sur kdb+. On peut l’envisager comme une plateforme pour construire sa propre base de données à partir d’un ensemble de puissants éléments de base. Cela peut sembler complexe, mais kdb+ et le langage q qui l’anime rendent l’opération relativement simple. L’avantage principal est le contrôle total sur la disposition et les modes d’accès en mémoire, et une fois que les données sont réparties sur le support de stockage de votre choix. Ce contrôle permet d’atteindre des performances impressionnantes, raison pour laquelle kdb+ est largement utilisé dans des secteurs comme la finance, où la latence (le seul problème vraiment difficile de l’informatique) se traduit par d’importantes sommes d’argent.
Ensuite, Rust. Nous utilisons Rust dans l’agent logiciel de notre nouveau produit IDS, ce qui nous a amené à l’expérimenter également dans notre pipeline de données. Rust est un langage d’une sophistication remarquable qui connaît un certain succès, notamment grâce à son adoption dans le nouveau Firefox. Dans le navigateur de Mozilla, il remplace désormais certaines des parties les plus complexes, offrant ainsi des performances parmi les meilleures du marché. Rust est construit sur la chaîne d’outils LLVM qui lui confère une génération de code natif de très haute qualité. De plus — et cela est crucial pour ce que nous allons en faire — il est compatible avec la disposition mémoire C et les conventions d’appel. C’est un gros avantage, car nous sommes fréquemment confrontés à cette contrainte avec notre autre langage de programmation système favori.
On utilise kdb+ quand on a besoin de la solution la plus rapide, donc nous nous sommes posés la question : jusqu'où peut-on aller ? En combinant ces deux technologies, nous pouvons bâtir une pile de données temporelles survitaminée. Bien que tous les détails techniques dépassent le cadre de ce billet, nous avons open-sourcé récemment une liaison légère mais utile qui nous donne accès à des fonctions de hachage modernes dans q.
Hash++ : Ajouter une extension simple
Une opération que nous effectuons très souvent est le hachage. Nous devons générer des fonctions d'identité pour les données que nous ingérons afin de pouvoir les agréger et les rechercher efficacement à travers plusieurs champs. Par défaut, q prend en charge le traditionnel hachage MD5, mais nous pouvons aller plus loin.
Nous recherchons un hachage non cryptographique et j'apprécie particulièrement la bibliothèque smhasher qui inclut une version accélérée matériellement de MetroHash. Plus précisément, elle propose une implémentation SSE4.2 sur les processeurs x86 modernes pouvant atteindre de nombreux Go (il s’agit bien de octets, pas de bits)/s. Nous voulions intégrer cela à kdb+ via Rust et étions impatients de voir si ces performances pouvaient être atteintes en pratique.
Nous avons créé une petite bibliothèque Rust qui fait le lien entre les types kdb+ et Rust. Elle inclut un fichier .q qui relie les fonctions Rust exportées selon les conventions d’appel C, ainsi que quelques tests et benchmarks.
Notre liaison crée deux nouvelles fonctions de hachage basées sur les variantes 64 bits (rmetro64) et 128 bits (rmetro128) du hachage Metro, appelables dans q d’une façon quasi compatible avec l’implémentation MD5 intégrée.
q)\l krs-hash.q q)md5 “hello world” 0x5eb63bbbe01eeed093cb22bb8f5acdc3 q)rmetro128 “hello world” 32bd829a-47d0-8cb3-3b25-9bfbdfc07543 [/shell] À noter : la version 128 bits de rmetro retourne la valeur du hash sous forme de GUID natif de type q pour faciliter les comparaisons et les plages de valeurs. Le MD5 intégré peut être transformé en un type retour compatible grâce à :
Quelle rapidité ?
Nous pouvons rapidement générer des données fictives via un script q et tester divers scénarios. [shell]q krs-hash-dict-bench.q[/shell] teste certaines transformations de dictionnaire avant de hacher l’ensemble.


L’implémentation moderne du hachage en Rust est entre 8x et 18x plus rapide que le MD5 natif, tout en restant aussi simple d’utilisation. L’écart de performance s’accroît avec la taille des données à hacher, comme attendu. La boucle principale de MetroHash est extrêmement optimisée et amortit le léger coût d’appel de la bibliothèque et des liaisons associées. Nous approchons les 11Go/s pour les plus gros paramètres. Un simple hachage en mémoire de chaînes, également dans le dépôt, culmine à 16Go/s en 64 bits sur le même matériel.


Cette version teste également une variante du hachage 128 bits agissant directement sur les MixedLists de q et émettant une liste de GUIDs tracée ici en tant que METRO128_LIST. Pour les petites tailles, cette méthode est intéressante car elle réduit le coût d’invocation, même si les usages mémoire sont plus importants.
À de tels débits, nous frôlons la bande passante maximale de la mémoire DDR4 de ma machine de test. Mémoire à 2,4GHz * 2 canaux * 8 octets (64 bits) par cycle = 38,4Go/s : c’est la vitesse théorique maximale pour une simple copie de gros tampons en mémoire.
On peut complexifier en demandant à q de créer des hashes depuis le contenu de dictionnaires paginés depuis un Anymap mappé en mémoire.


On constate le coût lié à la lecture depuis le disque lorsqu’on compare les débits pour chaque taille totale, mais nos vitesses restent très impressionnantes. Nous générons des hachages à un débit de 1,7Go/s depuis le disque.
Certes, un simple hachage est une fonction de transformation minimale, mais cela pose une base pour démontrer que cette architecture peut transformer les données à la limite du matériel tout en maintenant des abstractions de haut niveau.
Profilage sous macOS
À part, pour le développement sur Mac (comme chez nous), Instruments — partie d’Apple XCode — offre un aperçu performant de la bibliothèque native. Sous Linux, il existe de meilleures solutions mais coupler Rust et un processus q sur macOS n’est pas trivial. Ci-dessous, je fais tourner manuellement le hachage metro sur environ 1Go de chaînes et le profile avec le Time Profile d’Instruments. Instruments ne connaît pas bien le binaire ou les symboles q, mais notre code Rust est bien identifié.


On voit notamment qu’une part significative du temps est passée à vérifier l’encodage UTF8 des chaînes avant de créer une [rust]&str[/rust] Rust. Cela provient du binding Rust vers kdb+ utilisé par notre hasher, qui fait une conversion vérifiée car les chaînes Rust doivent être en UTF8 pour fonctionner correctement. Cependant, dans notre cas, la fonction de hashage s’intéresse uniquement à la séquence d’octets, donc nous pouvons activer la fonctionnalité [rust]unchecked_utf8[/rust] du crate et supprimer ce garde-fou.


Un second passage avec Instruments et la fonctionnalité activée montre que la majeure partie du temps CPU est désormais consacrée au calcul du hash metro 128 bits. Comme attendu, la performance du hash mesurée dans q gagne entre 10% et 15%.
Une chose simple à faire est d’ajouter la prise en charge du hachage de dictionnaires dans notre bibliothèque. Cela rend le composant simple à utiliser : il suffit de passer une liste clé/valeur triée pour générer une identité du contenu.
Si vous regardez le dépôt, vous verrez que la bibliothèque contient quelques nouveautés que nous n’avons pas évoquées ici.
1. Allocation système
Par défaut, Rust utilise l’allocateur de mémoire jemalloc. Cependant, sur macOS, sur ce cas d’usage, l’allocateur mémoire système est environ 5% plus rapide dans la boucle critique et Instruments visualise mieux les allocations. Cela demande le canal nightly de Rust et la fonctionnalité [rust]alloc_system[/rust].
2. Erreurs Rust
Il faut également protéger l’exécution de q de toute erreur Rust, par exemple le panic d’un [rust].unwrap()[/rust] qui traverserait la frontière de la bibliothèque. Ainsi, chaque appel est encapsulé.
3. Symboles dans les exécutables Rust
Cargo ne supporte pas pour l’instant une release build with debug symbols (compilation de release avec symboles de debug). Cette configuration sert habituellement aux profileurs comme Instruments. La configuration Cargo le prend ici en charge via un patch ajouté à la build release.
4. Traitement parallèle
Tout ce dont nous avons parlé jusqu’ici se passe sur un seul cœur. Il y a beaucoup à couvrir lorsqu’on examine [js]rmetro128 each data[/js] versus [js]rmetro128 peach data[/js] versus [js].Q.fc[{rmetro128 x}][/js] data, puisque les options parallèles introduisent de nouveaux compromis. En plus, la bibliothèque supporte aussi les listes non atomiques — par exemple, des listes de chaînes transformées en listes de hashes — et la création d’une identité de hash pour le contenu d’un dictionnaire. Tout ceci sera approfondi dans un prochain billet.
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 grand merci à mes collègues rsdy & dc qui m’ont aidé à rassembler les données pour cette expérience.
Vous avez lu jusqu’au bout ? Vous adoreriez probablement travailler sur de tels sujets. Envoyez-nous un mot avec votre profil GitHub/Bitbucket/GitLab et/ou un bon vieux CV à jobs@redsift.com
Édité le 25 août 2018 : correction précisant que l’absence de symboles dans les binaires release est un problème Cargo, pas Rust.
[shell] KDB+ 3.6 2018.06.14 Copyright (C) 1993-2018 Kx Systems
m64/ 12()core 16384MB
[shell] q)/ convertir la séquence d’octets en GUID avec 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]




