Написание быстрых и безопасных нативных модулей Node.js на Rust

Node.js Программа перевода самородков Rust

Написание быстрых и безопасных нативных модулей Node.js на Rust

Резюме. Разрабатывайте нативные модули Node.js на Rust вместо C++!

RisingStackВ прошлом году мы столкнулись с непростой задачей: мы заставили Node.js работать максимально эффективно, но нагрузка на сервер по-прежнему исчерпана. Чтобы улучшить производительность нашего приложения (и снизить затраты), мы решили полностью его переписать и перенести систему на другую инфраструктуру — несомненно, работы много и я не буду здесь вдаваться в подробности. Позже я узнал,Нам просто нужно написать нативный модуль!

Тогда мы не понимали, что есть лучший способ решить наши проблемы с производительностью. Всего несколько недель назад я узнал, что есть еще одно работающее решение.Используйте Rust вместо C++ для реализации нативных модулей.Я нашел, что это отличный вариант благодаря безопасности и простоте использования, которые он предлагает.

В этом руководстве по Rust я покажу вам, как написать продвинутый, быстрый и безопасный нативный модуль.

Проблемы с производительностью на серверах Node.js

Наша проблема обнаружилась в конце 2016 года, когда мы работали над Trace, продуктом мониторинга для Node.js, который был выпущен в октябре 2017 года сKeymetricsсливаться. Как и другие технологические стартапы того времени, мы развернули наши услуги дляHerokuвыше, чтобы сэкономить некоторые затраты на инфраструктуру и расходы на техническое обслуживание. Мы разрабатываем приложения с микросервисной архитектурой, что означает, что многие из наших сервисов обмениваются данными через HTTP(S).

Тут возникает каверзный вопрос:Мы хотели иметь безопасную связь между сервисами, но Heroku не поддерживает частные сети, поэтому нам пришлось реализовать собственное решение. Итак, мы рассмотрели некоторые схемы аутентификации безопасности и остановились на HTTP-подписях.

Кратко объясним: подписи HTTP основаны на асимметричной криптографии. Чтобы создать подпись HTTP, вам нужно получить все части запроса: URL-адрес, заголовки запроса, тело запроса и подписать его своим закрытым ключом. Затем вы можете отправить открытый ключ на устройства, которые получат запрос на подпись, чтобы они могли его проверить.

Со временем мы обнаружили, что в большинстве процессов HTTP-сервера загрузка ЦП достигает своего предела. Судя по всему, у наших подозрений есть основания — если вы хотите зашифровать, то так и происходит.

Однако вv8-profilerПосле тщательного анализа мы обнаружили, что проблема не в шифровании! даПарсинг URLЗанимает больше всего процессорного времени. Зачем? Потому что для проверки URL-адрес должен быть проанализирован для проверки подписи запроса.

Чтобы решить эту проблему, мы решили отказаться от Heroku (были и другие факторы), и вместо оптимизации нашего парсинга URL мы создали инфраструктуру Google Cloud с Kubernetes и внутренней сетью.

Что побудило меня написать этот рассказ (учебник)? Всего несколько недель назад я понял, что мы можем оптимизировать парсинг URL другим способом — написать нативную библиотеку на Rust.

Написание нативных модулей - требуется модуль Rust

Написание нативного кода не должно быть таким сложным, верно?

Мы в RisingStack считаем, что если вы хотите сделать что-то хорошее, вы должны сначала отточить свой инструмент. Мы часто ищем лучшие способы создания программного обеспечения и, при необходимости, пишем нативные модули на C++.

Бессовестно сказал: я также написал в блоге о своем пути обученияОбзор нативных модулей Node.js. Проверьте это!

До этого я думал, что в подавляющем большинстве бизнес-сценариев C++ был правильным выбором для написания быстрого и эффективного программного обеспечения. Однако теперь, когда у нас есть современные инструменты (в данном случае — Rust), мы можем использовать их для написания более эффективного, безопасного и быстрого кода с меньшими человеческими затратами, чем раньше.

Вернемся к первоначальному вопросу: сложно ли разобрать URL? Он включает в себя протокол, хост, параметры запроса...

URL-parsing-protocol

(отNode.js documentation)

Это выглядит так сложно. когда я прочиталthe URL standardПосле этого я обнаружил, что не хочу реализовывать это сам, поэтому начал искать альтернативы.

Я уверен, что я не единственный, кто пытается анализировать URL-адреса. В браузерах, вероятно, уже есть исправление для этого, поэтому я искал решение в Chromium:гугл ссылка. Хотя эту реализацию можно легко вызвать из Node.js с помощью N-API, есть несколько причин, по которым я этого не делаю:

  • обновить:Когда я просто скопировал код из Интернета, я сразу занервничал. Люди делают это в течение долгого времени, и всегда есть много причин, по которым они не работают хорошо... Нет хорошего способа обновить большие куски кода в кодовой базе.
  • безопасность:Человек без большого опыта программирования на C++ не может проверить правильность кода, но опять же мы должны запустить его на нашем сервере. Кривая обучения C++ слишком крутая, и людям требуется много времени, чтобы освоить ее.
  • Конфиденциальность:Мы все слышали, что существует пригодный для использования код C++, однако я предпочитаю избегать повторного использования кода C++, потому что я не могу проверить его самостоятельно. Использование хорошо поддерживаемого модуля с открытым исходным кодом дает мне достаточно уверенности, что мне не нужно беспокоиться о его конфиденциальности.

Поэтому я предпочитаю язык, который проще в использовании, имеет простой механизм обновления и является современным: Rust!

Несколько слов о Rust

Rust позволяет нам писать быстрый и эффективный код.

Все проекты Rust создаютсяcargoУправление — вот что такое Rustnpm.cargoЗависимости проекта могут быть установлены, и есть реестр со всеми пакетами, которые вам нужно использовать.

Я нашел библиотеку, которую можно использовать в нашем примере —rust-url, большое спасибо команде Servo за их работу.

Мы тоже собираемся использовать Rust FFI! Я уже писал об этом два года назадusing Rust FFI with Node.js. С тех пор в экосистеме Rust многое изменилось.

У нас есть рабочая библиотека (rust-url), попробуем ее скомпилировать!

Как скомпилировать приложение на Rust?

в соответствии сrustup.rsруководство, которое мы можем использоватьrustcкомпилятор, но мы должны больше беспокоиться оcargo. Я не хочу описывать, как это работает, если вам интересно, приходите к намПредыдущие записи в блоге Rust.

Создайте новый проект Rust

Создать новый проект Rust очень просто:cargo new --lib <工程名>.

Вы можете проверить полный код в моем репоGitHub.com/Пит YY/ржавчина…

Чтобы сослаться на библиотеку Rust, мы просто указываем ее как зависимость вCargo.tomlв этом.

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

[dependencies]
url = "1.6"

У ржавчины нет аналоговnpm installТо же, что и команда установки зависимостей — вы должны добавить ее вручную. Однако естьcargo editЯщик может иметь аналогичную функциональность.

Примечание переводчика: crate — это концепция, аналогичная пакету в Rust, и приведенный выше rust-url также относится к crate.crates.ioПозволяет разработчикам Rust по всему миру искать или публиковать ящики.

Rust FFI

Чтобы вызвать Rust из Node.js, мы можем использовать FFI, предоставляемый Rust. FFI — это аббревиатура от «Интерфейс внешних функций». Внешний функциональный интерфейс (FFI) — это механизм, написанный на одном языке программирования, способный вызывать подпрограммы или использовать службы, написанные на другом языке.

Чтобы связать нашу библиотеку, нам также необходимоCargo.tomlдобавить две вещи

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

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

Здесь необходимо пояснить: наша библиотека является динамической библиотекой, а расширение файла.dylib, библиотека загружается во время выполнения, а не во время компиляции.

Мы также добавим в проектlibcполагаться,libc— это стандартная библиотека языка C, соответствующая стандарту ANSI C.

libcКрейт — это библиотека для Rust, которая имеет встроенные привязки для распространенных типов и функций, которые можно найти в различных системах, включая libc. Это позволяет нам использовать типы языка C в коде Rust, и мы должны использовать его для любых данных типа C, которые мы хотим получать или возвращать в функциях Rust.

Наш код довольно прост — я используюextern crateключевое слово для цитированияurlиlibcящик. Мы собираемся пометить функцию какpub externТак что эти функции могут быть выставлены наружу через FFI. Наша функция содержит представление в Node.jsStringТипc_charуказатель.

Нам нужно пометить преобразование типа какunsafe. отмеченunsafeБлоки кода с ключевыми словами могут обращаться к небезопасным функциям или разыменовывать необработанные указатели в безопасных функциях.

Ржавчина используетOption<T>тип для представления значения, допускающего значение NULL. Как и в JavaScript, значение может бытьnullилиundefinedТакой же. Вы можете (и должны) выполнять явную проверку каждый раз, когда пытаетесь получить доступ к возможному нулевому значению. В Rust есть несколько способов получить к нему доступ, но здесь я воспользуюсь самым простым: если значение пустое, будет выброшена ошибка (паника в терминах Rust)unwrap.

Когда мы закончим синтаксический анализ URL, нам нужно преобразовать результат вCStringчтобы вернуть 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()
}

Чтобы скомпилировать этот код Rust, вы можете использоватьcargo build --releaseЗаказ. Перед компиляцией убедитесь, что выCargo.tomlдобавить в зависимостиurlбиблиотека!

Теперь мы можем использовать Node.jsffiПакет создает модуль для вызова кода 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
};

cargo build --releaseскомпилировано по команде.dylibСоглашение об именахlib*,один из них*имя вашей библиотеки.

Мизз: У нас уже есть код Rust, который можно вызывать из Node.js! Хотя это хороший пластырь, чтобы вытащить гной, но вы должны были заметить, что нам приходится делать много преобразований типов, что увеличит накладные расходы на наши вызовы функций. Должен быть лучший способ интегрировать наш код с JavaScript.

Первая встреча с Неоном

Привязки Rust для написания безопасных и быстрых нативных модулей Node.js.

Neon позволяет нам использовать типы JavaScript в коде Rust. Чтобы создать новый проект Neon, мы можем использовать его собственный инструмент командной строки. воплощать в жизньnpm install neon-cli --globalустановить его.

воплощать в жизньneon new <projectname>Новый проект Neon будет создан без какой-либо настройки.

После создания проекта Neon мы перепишем приведенный выше код следующим образом:

#[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)
    });

В приведенном выше коде новый типJsString,CallиJsResultЭто инкапсуляция типов JavaScript, чтобы мы могли получить доступ к виртуальной машине JavaScript и выполнить приведенный выше код.ScopeСвяжите нашу новую переменную с текущей областью видимости JavaScript, что позволит собирать нашу переменную сборщику мусора.

Это то же самое, что и в моем предыдущем посте в блогеНаписание собственных модулей Node.js на C++Объяснение очень похожее.

Примечательно,#[macro_use]свойства позволяют использоватьregister_module!макросы, которые позволяют нам делать такие вещи, как в Node.jsmodule.exportsТаким же образом создайте модуль.

Единственная сложная часть — это доступ к параметрам:

let url = call.arguments.require(scope, 0)?.check::<JsString>()?.value();

Мы должны принимать аргументы всех типов (как и любая функция JavaScript), поэтому у нас нет возможности определить количество аргументов, поэтому мы должны проверить, существует ли первый элемент.

Кроме этого, мы можем избавиться от большей части работы по сериализации и просто использоватьJsтипа нормально.

Теперь попробуем запустить!

Если вы заранее загрузили мой пример кода, вам нужно войти в папку ffi для выполненияcargo build --release, а затем войдите в папку neon для выполненияneon build(neon-cli должен быть установлен заранее).

Если у вас все готово, вы можете использовать Node.jsfaker libraryСоздайте новый список URL-адресов.

воплощать в жизньnode generateUrls.jsкоманда, это создастurls.jsonфайл, наша тестовая программа попытается разобрать его через мгновение. После этого вы можете выполнитьnode urlParser.jsЗапустим бенчмарки, если все пройдет успешно, вы увидите следующее изображение:

Rust-Node-js-success-screen

Тестовая программа проанализировала 100 URL-адресов (сгенерированных случайным образом), и нашему приложению потребовался только один запуск для анализа результатов. Если вы хотите провести бенчмаркинг, увеличьте количество URL-адресов (в urlParser.jstryCount) или количество раз (в urlGenerator.jsurlLength).

Очевидно, что лучшим показателем в тесте является версия Rust neon, но по мере увеличения длины массива V8 имеет все больше возможностей для оптимизации, и их оценки будут близки. В конце концов, он превзойдет реализацию Rust neon.

Rust-node-js-benchmark

Это всего лишь простой пример, конечно, нам еще многое предстоит узнать в этой области,

В будущем мы можем дополнительно оптимизировать вычисления и использовать параллельные вычисления, чтобы максимально повысить производительность.rayonЯщики предоставляют нам аналогичную функциональность.

Реализация модулей Rust в Node.js

Надеюсь, сегодня со мной вы узнали, как реализовать модули Rust в Node.js, и с этого момента вы сможете извлечь выгоду из новых инструментов (в цепочке инструментов). Я бы сказал, что хотя это и решает проблему (и доставляет удовольствие), это не панацея от всех проблем с производительностью.

Имейте в виду, что Rust может быть удобным решением в некоторых сценариях.

Если вы хотите увидеть мой доклад на эту тему на симпозиуме Rust в Венгрии,кликните сюда!

Если у вас есть какие-либо вопросы или комментарии, оставьте комментарий ниже, и я свяжусь с вами здесь!


Программа перевода самородковэто сообщество, которое переводит высококачественные технические статьи из ИнтернетаНаггетсДелитесь статьями на английском языке на . Охват контентаAndroid,iOS,внешний интерфейс,задняя часть,блокчейн,продукт,дизайн,искусственный интеллекти другие поля, если вы хотите видеть больше качественных переводов, пожалуйста, продолжайте обращать вниманиеПрограмма перевода самородков,официальный Вейбо,Знай колонку.