Escribir módulos nativos Node.js rápidos y seguros con Rust

Resultado de imagen para Node.js rápidos y seguros con Rust

RisingStack se enfrentó a un evento impactante el año pasado: alcanzamos la velocidad máxima que Node.js tenía para ofrecer en ese momento, mientras que los costos de nuestro servidor se volcaron al techo. Para aumentar el rendimiento de nuestra aplicación (y disminuir nuestros costos), decidimos reescribirla por completo y migrar nuestro sistema a una infraestructura diferente, lo que fue mucho trabajo, huelga decirlo.
Más tarde descubrí que podríamos haber implementado un módulo nativo.
En aquel entonces, no sabíamos que había un método mejor para resolver nuestro problema de rendimiento. Hace solo unas semanas descubrí que otra opción podría haber estado disponible. Fue entonces cuando recogí Rust en lugar de C ++ para implementar un módulo nativo. Descubrí que es una gran elección gracias a la seguridad y la facilidad de uso que proporciona.
En este tutorial de Rust, te guiaré por los pasos para escribir un módulo nativo moderno, rápido y seguro.

El problema con nuestra velocidad de servidor Node.js

Nuestro problema comenzó a fines de 2016 cuando trabajamos en Trace, nuestro producto de monitoreo Node.js, que recientemente se fusionó con Keymetrics en octubre de 2017.
Al igual que cualquier otra empresa de tecnología en ese momento, hemos estado ejecutando nuestros servicios en Heroku para ahorrar algunos gastos en mantenimiento y costos de infraestructura. Hemos estado construyendo una aplicación de arquitectura de microservicio, lo que significa que nuestros servicios se han estado comunicando mucho a través de HTTP (S).
1and1.com | Hosting, Domains, Website Services & Servers

Aquí es donde entra la parte difícil: queríamos comunicarnos de forma segura entre los servicios, pero Heroku no ofrecía redes privadas, así que tuvimos que implementar nuestra propia solución. Por lo tanto, buscamos algunas soluciones para la autenticación, y la que finalmente resolvimos fue firmas http.
Para explicarlo brevemente; Las firmas http se basan en la criptografía de clave pública. Para crear una firma http, toma todas las partes de una solicitud: la URL, el cuerpo y los encabezados, y los firmas con tu clave privada. Luego, puede dar su clave pública a quienes recibirán sus solicitudes firmadas para que puedan validarlas.
Pasó el tiempo y notamos que la utilización de CPU se volcó en la mayoría de nuestros procesos de servidores http. Sospechamos una razón obvia: si estás haciendo crypto, es así todo el tiempo.
Sin embargo, después de hacer algunos perfiles serios con el v8-profilerdescubrimos que en realidad no era la criptografía. Fue el análisis de URL el que requirió más tiempo de CPU. ¿Por qué? Porque para hacer la autenticación, tuvimos que analizar la URL para validar firmas de solicitud.
Para resolver este problema, decidimos abandonar Heroku (lo que queríamos hacer por otros motivos también) y crear una infraestructura de Google Cloud con Kubernetes y redes internas, en lugar de optimizar nuestro análisis de URL.
La razón para escribir esta historia / tutorial es que hace solo unas semanas me di cuenta de que podríamos haber optimizado el análisis de URL de otra manera: escribiendo una biblioteca nativa con Rust.

Desarrollador ingenuo que se vuelve nativo: la necesidad de un módulo Rust

No debería ser tan difícil escribir código nativo, ¿verdad?
Aquí en RisingStack, siempre hemos dicho que queremos usar la herramienta adecuada para el trabajo. Para hacerlo, siempre estamos investigando para crear un mejor software, incluidos algunos en módulos nativos de C ++ cuando sea necesario.
Enchufe descarado: He escrito una entrada de blog acerca de mi aprendizaje viaje en los módulos de Node.js nativos también. ¡Echar un vistazo!
En aquel entonces, pensé que en la mayoría de los casos, C ++ es la forma correcta de escribir software rápido y eficiente. Sin embargo, como ahora tenemos herramientas modernas a nuestro alcance (en este ejemplo, Rust), podemos usarlo para escribir de forma más eficiente y segura. y código rápido con mucho menos esfuerzo de lo que alguna vez requirió.
Volvamos a nuestro problema inicial: analizar una URL no debería ser tan difícil ¿no? Contiene un protocolo, host, parámetros de consulta ...
Eso se ve bastante complejo. Después de leer el estándar de URL, me di cuenta de que no quería implementarlo yo mismo, así que comencé a buscar alternativas.
Pensé que seguramente no soy la única persona que quiere analizar las URL. Los navegadores probablemente ya hayan resuelto este problema, así que revisé la solución de Chrome: google-url . Mientras que la implementación puede ser llamada fácilmente desde Node.js usando el N-API, tengo algunas razones para no hacerlo:
  • Actualizaciones: cuando solo copio y pego un código de internet, inmediatamente me da la sensación de peligro. La gente lo ha estado haciendo durante mucho tiempo, y hay muchas razones por las que no funcionó tan bien. Simplemente no hay una manera fácil de actualizar un gran bloque de código que está almacenado en mi repositorio.
  • Seguridad: una persona con poca experiencia en C ++ no puede validar que el código es correcto, pero eventualmente tendremos que ejecutarlo en nuestros servidores. C ++ tiene una curva de aprendizaje pronunciada, y lleva mucho tiempo dominarla.
  • Seguridad: todos escuchamos acerca del código explotable de C ++ que está disponible, que prefiero evitar porque no tengo forma de auditarlo yo mismo. El uso de módulos de fuente abierta bien mantenidos me da suficiente confianza para no preocuparme por la seguridad.
Así que preferiría un lenguaje más accesible, con un mecanismo de actualización fácil de usar y herramientas modernas: ¡Rust!

Algunas palabras sobre Rust

Rust nos permite escribir un código rápido y eficiente.
Todos los proyectos de Rust se gestionan con cargo, piense en ello como npmpara Rust. Se pueden instalar dependencias de proyecto cargo, y hay un registro lleno de paquetes esperando a que lo use.
Encontré una biblioteca que podemos usar en este ejemplo - rust-url , así que díganle al equipo de Servo por su trabajo.
Vamos a usar Rust FFI también! Ya habíamos cubierto el uso de Rust FFI con Node.js en un blog anterior hace dos años. Desde entonces, mucho ha cambiado en el ecosistema de Rust.
Tenemos una biblioteca supuestamente funcional (rust-url), así que intentemos construirla.
SEOBreeze Plugin

¿Cómo construyo una aplicación Rust?

Después de seguir las instrucciones en https://rustup.rs , podemos tener un rustccompilador en funcionamiento , pero todo lo que debemos preocuparnos ahora es cargoNo quiero entrar en detalles sobre cómo funciona, así que por favor revisa nuestro blog anterior de Rust si estás interesado.

Creando un nuevo proyecto de Rust

Crear un nuevo proyecto de Rust es tan simple como cargo new --lib <projectname>.
Puede consultar todo el código en mi repositorio de ejemplo https://github.com/peteyy/rust-url-parse
Para usar la biblioteca Rust que tenemos, podemos simplemente listarla como una dependencia en nuestro Cargo.toml

[package]
name = "ffi"
version = "1.0.0"
authors = ["Peter Czibik <p.czibik@gmail.com>"]

[dependencies]
url = "1.6"

No hay un formulario corto (incorporado) para agregar una dependencia como lo hace con npm installusted; tiene que agregarlo usted mismo manualmente. Sin embargo, hay una caja llamada cargo editque agrega una funcionalidad similar.

Rust FFI

Para poder usar los módulos Rust de Node.js, podemos usar el FFI proporcionado por Rust. FFI es un término corto para la Interfaz de Función Extranjera. La interfaz de función extranjera (FFI) es un mecanismo mediante el cual un programa escrito en un lenguaje de programación puede llamar rutinas o hacer uso de servicios escritos en otro.
Para poder enlazar a nuestra biblioteca, debemos agregar dos cosas a Cargo.toml

[lib]
crate-type = ["dylib"]

[dependencies]
libc = "0.2"
url = "1.6"

Tenemos que declarar que nuestra biblioteca es una biblioteca dinámica. Un archivo que termina con la extensión .dylibes una biblioteca dinámica: es una biblioteca que se carga en tiempo de ejecución en lugar de en tiempo de compilación.
También tendremos que vincular nuestro programa contra libclibces la biblioteca estándar para el lenguaje de programación C, como se especifica en el estándar ANSI C.
La libccaja es una biblioteca de Rust con enlaces nativos a los tipos y funciones que se encuentran comúnmente en varios sistemas, incluido libc. Esto nos permite usar tipos C de nuestro código Rust, lo que tendremos que hacer si queremos aceptar o devolver algo de nuestras funciones Rust. :)
Nuestro código es bastante simple: estoy usando el urly la libccaja con la extern cratepalabra clave. Para exponer esto al mundo exterior a través de FFI, es importante marcar nuestra función como pub externNuestra función toma un c_charpuntero que representa los Stringtipos procedentes de Node.js.
Necesitamos marcar nuestra conversión como unsafeUn bloque de código que está prefijado con la palabra clave insegura se usa para permitir llamar a funciones inseguras o eliminar referencias de punteros crudos dentro de una función segura.
Rust usa el Option<T>tipo para representar un valor que puede estar vacío. Piense en ello como un valor que puede ser nullundefineden su JavaScript. Puede (y debe) verificar explícitamente cada vez que intente acceder a un valor que puede ser nulo. Hay algunas maneras de abordar esto en Rust, pero esta vez voy con el método más simple: unwrapque simplemente arrojará un error (pánico en términos de óxido) si el valor no está presente.
Cuando finalice el análisis de URL, tendremos que convertirlo a a CString, que se puede pasar a JavaScript.

extern crate libc;
extern crate url;

use std::ffi::{CStr,CString};
use url::{Url};

#[no_mangle]
pub extern "C" fn get_query (arg1: *const libc::c_char) -> *const libc::c_char {

    let s1 = unsafe { CStr::from_ptr(arg1) };

    let str1 = s1.to_str().unwrap();

    let parsed_url = Url::parse(
        str1
    ).unwrap();

    CString::new(parsed_url.query().unwrap().as_bytes()).unwrap().into_raw()
}

Para construir este código Rust, puedes usar el cargo build --releasecomando. Antes de la compilación, asegúrese de agregar la urlbiblioteca a su lista de dependencias Cargo.tomlpara este proyecto también.
Podemos usar el ffipaquete Node.js para crear un módulo que expone el código Rust.

const path = require('path');
const ffi = require('ffi');

const library_name = path.resolve(__dirname, './target/release/libffi');
const api = ffi.Library(library_name, {
  get_query: ['string', ['string']]
});

module.exports = {
  getQuery: api.get_query
};

La convención de nomenclatura es lib*, donde *está el nombre de su biblioteca, para el .dylibarchivo que se cargo build --releasecrea.
Esto es genial; ¡tenemos un código de Rust que hemos llamado desde Node.js! Funciona, pero ya se puede ver que tuvimos que hacer un montón de conversiones entre los tipos, lo que puede agregar un poco de sobrecarga a nuestras llamadas a funciones. Debería haber una forma mucho mejor de integrar nuestro código con JavaScript.

Conoce a Neon

Enlaces de óxido para escribir módulos nativos Node.js seguros y rápidos.
Neon nos permite usar tipos de JavaScript en nuestro código Rust. Para crear un nuevo proyecto de Neon, podemos usar su propio cli. Úselo npm install neon-cli --globalpara instalarlo.
neon new <projectname> creará un nuevo proyecto de neón con configuración cero.
Con nuestro proyecto de neón hecho, podemos reescribir el código de arriba como el siguiente:

#[macro_use]
extern crate neon;

extern crate url;

use url::{Url};
use neon::vm::{Call, JsResult};
use neon::js::{JsString, JsObject};

fn get_query(call: Call) -> JsResult<JsString> {
    let scope = call.scope;
    let url = call.arguments.require(scope, 0)?.check::<JsString>()?.value();

    let parsed_url = Url::parse(
        &url
    ).unwrap();

    Ok(JsString::new(scope, parsed_url.query().unwrap()).unwrap())
}

register_module!(m, {
    m.export("getQuery", get_query)
});

Esos nuevos tipos que estamos usando en la parte superior JsStringCallJsResultson contenedores para los tipos de JavaScript que nos permiten engancharnos a la VM de JavaScript y ejecutar el código sobre ella. Esto Scopenos permite vincular nuestras nuevas variables a los ámbitos de JavaScript existentes, por lo que nuestras variables pueden ser recolectadas.
Esto es muy similar a escribir módulos nativos de Node.js en C ++ que he explicado en un blogpost anterior.
Observe el #[macro_use]atributo que nos permite utilizar la register_module!macro, lo que nos permite crear módulos al igual que en Node.js module.exports.
La única parte difícil aquí es acceder a argumentos:
let url = call.arguments.require(scope, 0)?.check::<JsString>()?.value();
Tenemos que aceptar todo tipo de argumentos (como lo hace cualquier otra función de JavaScript), por lo que no podemos estar seguros de si la función se invocó con argumentos únicos o múltiples. Es por eso que debemos verificar la existencia del primer elemento.
Aparte de ese cambio, podemos deshacernos de la mayoría de la serialización y simplemente usar Jstipos directamente.
¡Ahora intentemos ejecutarlos!
Si descargó mi ejemplo primero, debe ingresar a la carpeta ffi y hacer una cargo build --releasey luego en la carpeta de neón y (con neon-cli previamente instalado a nivel mundial) ejecutar neon build.
Si está listo, puede usar Node.js para generar una nueva lista de URL con la biblioteca de faker .
Ejecute el node generateUrls.jscomando que colocará un urls.jsonarchivo en su carpeta, lo que nuestras pruebas leerán y tratarán de analizar. Cuando esté listo, puede ejecutar los "puntos de referencia" con node urlParser.jsSi todo fue exitoso, deberías ver algo como esto:
Rust-Node-js-success-screen
Esta prueba se realizó con 100 URL (generadas aleatoriamente) y nuestra aplicación las analizó solo una vez para obtener un resultado. Si desea realizar un análisis de referencia, aumente el número ( tryCounten urlParser.js) de las URL o el número de veces ( urlLengthen urlGenerator.js).
Puede ver que el ganador en mi punto de referencia es la versión Rust de neón, pero a medida que aumenta la longitud de la matriz, habrá más optimización que V8 puede hacer, y se acercarán más. Finalmente, superará la implementación de Rust Neon.
Rust-node-js-benchmark
Este fue solo un ejemplo simple, por lo tanto, por supuesto, hay mucho que aprender de este campo,
Podemos optimizar aún más este cálculo en el futuro, utilizando potencialmente bibliotecas de concurrencia proporcionadas por algunas cajas como rayon.

Implementación de módulos Rust en Node.js

Afortunadamente, también aprendió algo sobre la implementación de los módulos de Rust en Node.js conmigo, y a partir de ahora puede beneficiarse de una nueva herramienta en su cadena de herramientas. Quería demostrar que si bien esto es posible (y divertido), no es una solución mágica que resuelva todos los problemas de rendimiento.
Solo tenga en cuenta que saber Rust puede ser útil en ciertas situaciones.
En caso de que quieras verme hablando de este tema durante la reunión de Rust Hungary, ¡mira este video !
Si tiene alguna pregunta o comentario, hágamelo saber en la siguiente sección: ¡estaré aquí para responderlas!
MyAppBuilder - Crete iPhone Apps in Minutes

Fuente | risingstack

Comentarios