Skip to content

Rust se une a Q: Qué construimos y qué aprendimos

Los ingenieros de Red Sift crearon una integración de Rust con el lenguaje Q. Esta es la historia técnica, los desafíos que enfrentamos y por qué creemos que valió la pena.

Rahul Powar·Co-founder & CEO
Published: August 23, 2018·Updated: October 2, 2025·10 min read

Aprovechamos la elegancia de kdb+ y el poder de Rust para crear aplicaciones de datos que pueden procesar datos a una velocidad de decenas de GB/segundo en hardware de consumo.

Foto de SpaceX en UnsplashFoto de SpaceX en Unsplash
Foto de SpaceX en Unsplash

En Red Sift, usamos nuestra plataforma para impulsar aplicaciones de datos de próxima generación para la ciberseguridad. A un nivel general, nuestra plataforma es uno de esos entornos de computación serverless tan de moda, excepto que la nuestra te permite hacer trabajo real. Red Sift nos permite canalizar datos, transformarlos/aumentarlos y finalmente agregarlos para encontrar información que ayude a mantener seguras a organizaciones y personas. Todas estas funciones vienen con el andamiaje necesario para construir aplicaciones SaaS reales en la nube. Hablamos de APIs con claves, interfaces web, permisos basados en roles: todo crítico para el software de nivel empresarial que ofrecemos a algunas de las organizaciones más grandes del mundo. Una de las características más interesantes de la plataforma es la capacidad de integrar diferentes componentes tecnológicos en una sola aplicación sin código de conexión. Usando esta capacidad, recientemente añadimos kdb+ a nuestra plataforma principal ya que estamos construyendo un nuevo producto de ciberseguridad que depende en gran medida del procesamiento de datos de series temporales.

Análisis de series temporales para datos de streaming grandes y rápidos.Análisis de series temporales para datos de streaming grandes y rápidos.
Análisis de series temporales para datos de streaming grandes y rápidos.

Primero, un poco sobre kdb+. Puedes pensarlo como una plataforma para construir tu propia base de datos usando una caja de primitivas potentes. Aunque esto suena complejo, kdb+ y el lenguaje q que lo impulsa lo hacen relativamente sencillo. La principal ventaja es el control total sobre la disposición y los patrones de acceso, tanto en memoria como una vez que has volcado los datos al medio de almacenamiento que elijas. Este control permite desbloquear un rendimiento asombroso, razón por la cual kdb+ se usa extensamente en sectores como los servicios financieros, donde la latencia (el único problema difícil en informática) se traduce en mucho dinero.

Un lenguaje de programación de sistemas que funciona extremadamente rápido, evita segfaults y garantiza la seguridad de hilos.Un lenguaje de programación de sistemas que funciona extremadamente rápido, evita segfaults y garantiza la seguridad de hilos.
Un lenguaje de programación de sistemas que funciona extremadamente rápido, evita segfaults y garantiza la seguridad de hilos.

A continuación, Rust. Usamos Rust en el agente de software de nuestro nuevo producto IDS, así que también empezamos a experimentar con él en nuestro pipeline de datos. Rust es un lenguaje tremendamente sofisticado que está teniendo su momento, en parte debido a su adopción en el nuevo Firefox. En el navegador de Mozilla ahora está reemplazando algunas de las partes más complicadas del navegador y desbloqueando un rendimiento líder en el mercado. Rust está construido sobre la cadena de herramientas LLVM, lo que le da algunas de las generaciones de código nativo de más alta calidad del mercado. También, de forma crítica para lo que estamos a punto de hacer con él, proporciona compatibilidad con la disposición de memoria y las convenciones de llamada de C. Esto es muy importante, ya que a menudo nos encontramos con esta limitación con nuestro otro lenguaje de programación de sistemas preferido.

Usas kdb+ cuando necesitas la solución más rápida, así que nos planteamos la pregunta: ¿Qué tan rápido se puede ir? Cuando combinamos estos 2 componentes tecnológicos, podemos construir una pila de datos de series temporales potenciada. Aunque el detalle técnico completo está fuera del alcance de una publicación de blog, acabamos de liberar como código abierto un binding pequeño pero útil que nos da acceso a funciones de hashing modernas en q.

Hash++: Añadiendo una extensión simple

Una operación que terminamos haciendo mucho es el hashing. Necesitamos generar funciones de identidad para los datos que ingerimos para poder agregar y buscar de manera eficiente a través de múltiples campos. De fábrica, q soporta el buen y viejo hash MD5, pero podemos hacerlo mejor.

Estamos buscando un hash no criptográfico y soy particularmente aficionado a la biblioteca smhasher, ya que incluye una versión acelerada por hardware de MetroHash. Específicamente, incluye una implementación SSE4.2 en procesadores x86 modernos que puede avanzar a muchos GB (eso es bytes, no bits)/s. Queríamos conectar esto a kdb+ a través de Rust y estábamos interesados en ver si podíamos alcanzar parte de este rendimiento en el mundo real.

Hemos creado una pequeña biblioteca de Rust que mapea hacia y desde tipos de kdb+. Incluye un archivo .q que enlaza las funciones de Rust que han sido exportadas con convenciones de llamada de C, además de algunas pruebas y benchmarks.

Nuestro binding crea 2 nuevas funciones de hash basadas en la variante de 64 bits (rmetro64) y 128 bits (rmetro128) del hash Metro, que pueden ser llamadas por q de una manera mayormente compatible con la implementación integrada de MD5.

q)\l krs-hash.q q)md5 “hello world” 0x5eb63bbbe01eeed093cb22bb8f5acdc3 q)rmetro128 “hello world” 32bd829a-47d0-8cb3-3b25-9bfbdfc07543 [/shell] Ten en cuenta que la versión de 128 bits de rmetro devuelve el valor del hash como un tipo nativo de q GUID para facilitar la comparación y el rango. El MD5 integrado se puede convertir a un tipo de retorno compatible usando lo siguiente:

¿Qué tan rápido?

Podemos generar rápidamente algunos datos de prueba con un script q y probar algunos escenarios. [shell]q krs-hash-dict-bench.q[/shell] prueba algunas transformaciones de diccionario antes de hacer hash de todo el conjunto.

Rendimiento de Rust metro64/128 frente a Q MD5Rendimiento de Rust metro64/128 frente a Q MD5
MB/s frente a los tamaños de datos que se hashean. CPU i7-8750H @ 2.20GHz en macOS

La implementación moderna de hashing en Rust es entre 8x y 18x más rápida que el MD5 integrado y es igual de fácil de usar. La diferencia de rendimiento crece a medida que aumenta el tamaño de los datos que se hashean, como era de esperar. El bucle activo en el hash Metro está muy optimizado y amortiza el costo pequeño pero presente de llamar a la biblioteca y los bindings asociados. Estamos alcanzando cerca de 11GB/s en los tamaños de parámetros más grandes. Un hash de cadena en memoria más simple, también en el repositorio, alcanzará un máximo de 16GB/s para el hash de 64 bits en el mismo hardware.

MB/s frente a los tamaños de datos que se hashean. CPU i7-8750H @ 2.20GHz en macOSMB/s frente a los tamaños de datos que se hashean. CPU i7-8750H @ 2.20GHz en macOS
MB/s frente a los tamaños de datos que se hashean. CPU i7-8750H @ 2.20GHz en macOS

Esta versión también prueba una variante del hash de 128 bits que opera directamente sobre MixedLists de q y emite una lista de GUIDs graficados aquí como METRO128_LIST. En tamaños pequeños, este enfoque es útil ya que reduce el costo de invocación, aunque los overheads de memoria son más altos.

A esas velocidades, estamos dentro del rango del ancho de banda máximo de memoria de la memoria DDR4 en mi máquina de prueba. 2.4Ghz de memoria * 2 canales de memoria * 8 bytes (64 bits) por ciclo = 38.4GB/s es lo más rápido que podría llegar a ir si simplemente estuviera copiando grandes buffers.

Podemos hacer esto más complejo haciendo que q genere hashes a partir del contenido de un diccionario que se pagina desde un Anymap mapeado en memoria.

MB/s frente a los tamaños de datos que se hashean para cadenas desde SSD. CPU i7-8750H @ 2.20GHz en macOSMB/s frente a los tamaños de datos que se hashean para cadenas desde SSD. CPU i7-8750H @ 2.20GHz en macOS
MB/s frente a los tamaños de datos que se hashean para cadenas desde SSD. CPU i7-8750H @ 2.20GHz en macOS

Podemos ver el costo de leer datos desde una unidad al comparar los valores del rendimiento para cada tamaño total de entrada, pero nuestras tasas de datos siguen siendo cómodamente impresionantes. Estamos transmitiendo hashes a un ritmo de 1.7GB/s desde el disco.

Obviamente, un hash simple es una función de transformación de datos mínima, pero esto sirve como referencia para demostrar que esta arquitectura novedosa puede transformar datos cerca de los límites del hardware, mientras sigue permitiendo abstracciones de alto nivel.

Perfilado en macOS

Como nota al margen, al desarrollar en una Mac (como hacemos nosotros), Instruments, parte del paquete Apple XCode, puede darte información sobre el rendimiento de tu biblioteca nativa. En Linux hay una serie de opciones mejores, pero tanto incrustar Rust en un proceso q como macOS hacen las cosas un poco más difíciles. A continuación, ejecuto manualmente el hash metro en aproximadamente 1GB de datos de cadena y lo perfilo usando el instrumento Time Profile. Aunque Instruments no sabe mucho sobre el binario de q o los símbolos, como sería de esperar, nuestro código Rust proporciona una entrada interesante.

~25% del tiempo se dedica a la validación UTF8.~25% del tiempo se dedica a la validación UTF8.
~25% del tiempo se dedica a la validación UTF8.

Específicamente, podemos ver que una parte significativa del tiempo se dedica a verificar la cadena UTF8 antes de crear un [rust]&str[/rust] de Rust. Esto es porque el binding de Rust a kdb+ usado por nuestro hasher hace una conversión verificada sensata, ya que las cadenas de Rust deben ser UTF8 para un comportamiento definido. Sin embargo, para este caso de uso el hash realmente solo se preocupa por la secuencia de bytes, así que podemos activar la función [rust]unchecked_utf8[/rust] del crate y eliminar esta válvula de seguridad.

Sin verificación UTF8, la biblioteca ahora hace exclusivamente trabajo útil.Sin verificación UTF8, la biblioteca ahora hace exclusivamente trabajo útil.
Sin verificación UTF8, la biblioteca ahora hace exclusivamente trabajo útil.

Un segundo pase con Instruments con la función activada ahora me muestra que la mayor parte del tiempo de CPU se dedica a calcular el hash metro de 128 bits. Como se esperaba, el rendimiento del hash medido en q es entre 10% y 15% más rápido.

Una cosa simple que podemos hacer es añadir soporte para hacer hash de diccionarios en nuestra biblioteca. Esto hace que el componente sea fácil de usar, de modo que podamos simplemente pasar un conjunto ordenado de clave/valor y generar una identidad del contenido.

Si miras el repositorio, notarás que la biblioteca incluye algunos elementos que no hemos discutido aquí.

1. Asignación del sistema

Por defecto, Rust usará el asignador jemalloc. Sin embargo, algunas pruebas para este caso de uso en macOS nos muestran que el asignador de memoria del sistema es alrededor de un 5% más rápido en este bucle activo evaluado y le da a Instruments visibilidad sobre las asignaciones. Esto requiere el canal nightly de Rust y la función [rust]alloc_system[/rust].

2. Errores de Rust

También necesitamos proteger el runtime de q de cualquier error de Rust, por ejemplo, un pánico de [rust].unwrap()[/rust] que se propague a través del límite de la biblioteca. Como resultado, verás cada llamada envuelta.

3. Símbolos en binarios de Rust

Cargo actualmente no admite un perfil de build de release con símbolos de depuración. Esta configuración de compilación se usa normalmente para perfiladores como Instruments. Notarás que la configuración de Cargo tiene esto parcheado en el build de release.

4. Procesamiento paralelo

Todo lo que hemos discutido hasta ahora es de un solo núcleo. Hay mucho que cubrir al explorar [js]rmetro128 each data[/js] frente a [js]rmetro128 peach data[/js] frente a [js].Q.fc[{rmetro128 x}][/js] data, ya que las opciones paralelas introducen un nuevo conjunto de compensaciones. Además, la biblioteca también soporta tipos no atómicos que mapean listas, por ejemplo, listas de cadenas en listas de hashes y la creación de una identidad de hash para el contenido de un diccionario. Esto y más será explorado en una futura publicación.

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. 

Muchas gracias a mis colegas rsdy y dc que me ayudaron a reunir los datos para este experimento.

¿Has leído hasta el final? Probablemente te encantaría trabajar en esto. Envíanos una nota con tu enlace de GitHub/Bitbucket/GitLab y/o un CV a la vieja usanza a jobs@redsift.com

Editado el 25-ago-18: Corrección para señalar que los símbolos en los binarios de release es un problema de Cargo, no de 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
Rahul Powar
Co-founder & CEO

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.