[译]使用 Rust 编写快速安全的原生 Node.js 模块

2023-05-16

  • 原文地址:Writing fast and safe native Node.js modules with Rust
  • 原文作者:Peter Czibik
  • 译文出自:掘金翻译计划
  • 本文永久链接:github.com/xitu/gold-m…
  • 译者:LeopPro
  • 校对者:Serendipity96

使用 Rust 编写快速安全的原生 Node.js 模块

内容梗概 - 使用 Rust 代替 C++ 开发原生 Node.js 模块!

RisingStack 去年面临一件棘手的事:我们已经尽可能让 Node.js 发挥出最高的性能,然而我们的服务器开销还是达到的最高限度。为了提高我们应用的性能(并且降低成本),我们决定彻底重写它,并将系统迁移到其他的基础设施上 - 毫无疑问,这个工作量很大,这里不详叙了。 后来我发现,我们只要写一个原生模块就行了!

那时候,我们还没意识到有更好的方法来解决我们的性能问题。就在几周前,我发现有另外一个方案可行 采用 Rust 代替 C ++ 来实现原生模块。 我发现这是一个很好的选择,这要归功于它提供的安全性和易用性。

在这篇 Rust 教程中,我将手把手教你写一个先进、快速、安全的原生模块。

Node.js 服务器的性能问题

我们的问题在 2016 年末的时候暴露出来,当时我们一直在研究 Node.js 的监控产品 Trace,该产品于2017年10月与 Keymetrics 合并。 像当时的其他科技创业公司一样,我们将服务部署到 Heroku 上以节省一些基础设施成本和维护费用。我们一直在构建微服务架构应用程序,这意味着我们很多服务都是通过 HTTP(S) 进行通信的。

棘手的问题来了: 我们想让各服务之间进行安全的通信,但是 Heroku 不支持私有网络,所以我们不得不实现一个自己的方案。因此,我们查阅了一些安全认证方案,最终选定了 HTTP 签名。

简要地解释一下:HTTP 签名基于非对称密码体系。要创建一个 HTTP 签名,你需要获取一个请求的所有部分:URL、请求头、请求体,使用你的私钥对其签名。然后,你可以将公钥发给将会收到签名请求的设备,以便它们验证。

随时间流逝,我们发现在大多数 HTTP 服务器进程中,CPU 利用率已经达到了极限。显然,一个原因引起我们怀疑 - 如果你想加密,那就会发生这样的问题。

然而,在对 v8-profiler 进行了严格分析之后,我们发现问题不是由加密引起的!是 URL 解析占用 CPU 最多的时间。为什么?因为要进行验证,就必须解析 URL 来验证请求签名。

为了解决这个问题,我们决定放弃 Heroku(这其中也有其他因素),我们创建了一个包含 Kubernetes 和内部网络的 Google 云基础设施,而不是优化我们的 URL 解析。

是什么原因促使我写这个故事(教程)呢?就在几周前,我意识到我们可以用另一种方法优化 URL 解析 —— 使用 Rust 写一个原生库。

编写原生模块 - 需要一个 Rust 模块

编写原生代码应该不那么难,对吧?

在 RisingStack,我们奉行工欲善其事,必先利其器的宗旨。我们经常对更好的软件构件方式做调查,在必要的时候,也使用 C++ 来编写原生模块。

恬不知耻地说一句:我也在博客上写了我的学习历程 原生 Node.js 模块之旅。去看一看!

在此之前,我认为在绝大多数业务场景中,C++ 是编写一个快速有效的软件的正确选择。然而现在我们有了现代化的工具(本例中 - Rust),我们可以用它花费比以前都少的人力成本来编写更有效、更安全、更快速的代码。

让我们回到最初的问题:解析一个 URL 难道很困难么?它包括协议、主机、查询参数……


(出自 Node.js documentation)

这看起来真复杂。当我通读 the URL standard 之后,我发现我不想自己实现它,所以我开始寻找替代品。

我确信我不是唯一一个想要解析 URL 的人。浏览器可能已经解决了这个问题,所以我搜索了 Chromium 的解决方案:谷歌链接。尽管使用 N-API 可以很容易地从 Node.js 调用这个实现,但是有几个原因让我不这样做:

  • 更新: 当我只是从网上复制粘贴代码的时候,我立即感到了不安。长久以来,人们一直这样做,而且总有许多原因使它们不能很好地工作……没有什么好的方法去更新代码库中的大段代码。
  • 安全性: 一个没有丰富 C++ 编程经验的人是无法验证代码是否正确的,但是我们又不得不将它运行在我们服务器上。C++ 学习曲线过于陡峭,人们需要花费很长时间掌握它。
  • 私密性: 我们都听说过可用的 C++ 代码是存在的,然而我宁愿避免复用 C++ 代码,因为我没办法独自审计它。使用维护良好的开源模块给了我足够的信心,我不必担心它的私密性。

所以我更倾向于一门更易于使用的,具有简易更新机制和现代化的语言:Rust!

关于 Rust 简单说两句

Rust 允许我们编写快速有效的代码。

所有的 Rust 工程由 cargo 管理 —— 就是 Rust 界的 npmcargo 可以安装工程依赖,并且有一个注册表包含了所有你需要使用的包。

我发现了一个可以在我们例子中使用的库 - 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/peteyy/rust…

想要引用 Rust 库,我们只要将它作为一个依赖列在 Cargo.toml 中就可以了。

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

[dependencies]
url = "1.6"
复制代码

Rust 没有类似 npm install 一样安装依赖的命令 - 你必须自己手动添加它。然而有一个叫做 cargo edit 的 crate 可以实现类似功能。

译者注:crate 是 Rust 中一个类似包(package)的概念,上文中的 rust-url 也属于一个 crate。crates.io 允许全世界的 Rust 开发者搜索或者发布 crate。

Rust FFI

为了从 Node.js 中调用 Rust,我们可以使用 Rust 提供的 FFI。FFI 是外部函数接口(Foreign Function Interface)的缩写。外部函数接口(FFI)是由一种程序语言编写的,能够调用另一种语言编写的例程或使用服务的机制。

为了链接我们的库,我们还需要向 Cargo.toml 中添加两个东西

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

[dependencies]
libc = "0.2"
url = "1.6"
复制代码

在这里需要说明:我们的库是动态链接库,文件扩展名为 .dylib,这个库在运行期被加载而不是编译期。

我们还要为工程添加 libc依赖,libc 是遵从 ANSI C 标准的 C 语言标准库。

libc crate 是 Rust 的一个库,它具有与各种系统(包括libc)中常见类型和函数的本地绑定。这允许我们在 Rust 代码中使用 C 语言类型,我们想在 Rust 函数中接收或返回任何 C 类型数据,我们都必须使用它。

我们的代码相当简单 —— 我使用 extern crate 关键字来引用 urllibc crate。我们要把函数标记为 pub extern 使得这些函数可以通过 FFI 被暴漏给外部。我们的函数持有一个代表 Node.js 中 String 类型的 c_char 指针。

我们需要把类型转换标记为 unsafe。被标记了 unsafe 关键字的代码块可以访问非安全的函数或者取消引用在安全函数中的裸指针(raw pointer)。

Rust 使用 Option<T> 类型来表示一个可为空的值。就像 JavaScript 中一个值可以为 null 或者 undefined 一样。每次尝试访问可能为空的值时,都可以(也应该)明确地检查。在 Rust 中,有几种方式可以访问它,但是在这里,我将使用最简单的方式:如果值为空,则将会抛出一个错误(panic in Rust terms)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.js 的 ffi 包创建一个用于调用 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*,其中的 * 是你的库名。

美滋滋:我们已经有了一个可以从 Node.js 调用的 Rust 代码!虽说能拔脓的就是好膏药,但是你应该已经发现了,我们不得不做一大堆类型转换,这将增加我们函数调用的开销。一定有更好的办法将我们的代码与 JavaScript 做整合。

初遇 Neon

用于编写安全、快速的原生 Node.js 模块的 Rust 绑定。

Neon 让我们可以在 Rust 代码中使用 JavaScript 类型。要创建一个新的 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)
    });
复制代码

上述代码中,新类型 JsStringCallJsResult 是对 JavaScript 类型的封装,这样我们就可以接入 JavaScript VM ,执行上面的代码。Scope 将我们的新变量绑定到当前的 JavaScript 域中,这让我们的变量就可以被垃圾收集器回收。

这和我之前写的博文中 使用 C++ 编写原生 Node.js 模块 解释地非常类似。

值得注意的是,#[macro_use] 属性允许我们使用 register_module! 宏,这可以让我们像 Node.js 中的 module.exports 一样创建模块。

唯一棘手的地方是对参数的访问:

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

我们得接受所有类型的参数(如同任何 JavaScript 函数一样),所以我们没办法确定参数的数量,这就是我们必须要检查第一个元素是否存在的原因。

除此之外,我们可以摆脱大多数的序列化工作,直接使用 Js 类型就好了。

现在,我们尝试运行它!

如果你事先下载了我的示例代码,你需要进入 ffi 文件夹执行 cargo build --release ,然后进入 neon 文件夹执行 neon build(事先要装好 neon-cli)。

如果你都准备好了,你可以使用 Node.js 的 faker library 生成一个新的 URL 列表。

执行 node generateUrls.js 命令,这将会在你的文件夹中创建一个 urls.json 文件,我们的测试程序一会儿会尝试解析它。搞定了这些后,你可以执行 node urlParser.js 来运行基准测试,如果全部成功了,你将会看到下图:

测试程序解析了100个URL(随机产生),我们的应用只需要一次运行就可以解析出结果。如果你想做基准测试,请增加 URL 数量(urlParser.js 中的 tryCount)或次数(urlGenerator.js 中的 urlLength)。

显而易见,在基准测试中表现最好的是 Rust neon 版本,但是随之数组长度的增加,V8 有越来越多的优化空间,他们之间的成绩会接近。最终它将超过 Rust neon 实现。

这只是一个简单的例子,当然,在这个领域我们还有很多东西要学习,

后续,我们可以进一步优化计算,尽可能的利用并发计算提高性能,一些类似 rayon 的 crates 提供给我们类似的功能。

在 Node.js 中实现 Rust 模块

希望你今天跟我学到了在 Node.js 中实现 Rust 模块的方法,从此你可以从(工具链中的)新工具中受益。我想说的是,虽然这是能解决问题的(而且很有趣),但它并不是解决所有性能问题的银弹。

请记住,在某些场景下,Rust 可能是很便利的解决方案

如果你想看看我在 Rust 匈牙利研讨会上关于本话题的发言,点这里!

如果你有任何问题或评论,请在下面留言,我将在这回复你们!


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

[译]使用 Rust 编写快速安全的原生 Node.js 模块 的相关文章

随机推荐

  • 字符串 - 字符串排序

    1 字符串排序 对于许多排序应用来说 xff0c 决定顺序的键都是字符串 给定一列字符串 xff0c 需要按一定顺序排列整齐方便后序处理 2 键索引计数法 这个方法名字有点拗口 xff0c 过程有点绕 xff0c 但是每一步其实很简单 举个
  • iOS-高德地图API的定位与搜索功能

    环境 xff1a Xcode10 1 Swift4 2 真机6s xff0c ios11 Demo xff1a https github com cxymq AmapSwift 高德地图API使用 需要 xff08 https lbs am
  • iOS 录音,播放并上传

    1 界面布局 xff0c 以及相关功能 点击中间开始录音 xff0c 点击左上角播放或暂停播放 xff0c 点击右上角移除文件 2 定义相关属性 import 34 SendVoiceController h 34 import impor
  • 【Python基础】request.post()方法

    00 序言 爬取懂che帝的车型信息时 xff0c 没太整明白request post 里面的参数是干什么用的 xff0c 所以写篇学习笔记提醒一下自己 url 61 39 https www dongchedi com motor bra
  • 老虎证券web端PWA实践总结

    历时两个月 xff0c PWA功能终于在web端稳定落地使用 xff0c 网站 web itiger com 从最新研究到落地上线 xff0c 遇到不少坑 xff1b 开发过程中也参考了不少资料 xff0c 但总有那么几个是没有答案 xff
  • 硬盘inode节点简单介绍

    一 inode是什么 xff1f 理解inode xff0c 要从文件储存说起 文件储存在硬盘上 xff0c 硬盘的最小存储单位叫做 34 扇区 34 xff08 Sector xff09 每个扇区储存512字节 xff08 相当于0 5K
  • NoMachine 远程桌面控制

    它是一个基于企业级对比套装的开源的终端服务器 它允许用户在连接速度缓慢或者窄带宽的情况下 xff0c 对X11会话进行远程访问 NX项目提供一整套的运行库文件以及优化的来自X11 xff0c SMB xff0c IPP xff0c HTTP
  • Angular之路--带你来搭建Webpack 2 + Angular 4项目

    上个月Angular发布了4 0 0版本 xff0c 少年们 xff0c 赶快学起来吧 xff0c 这篇文章带领大家搭建一个简单的Angular应用 xff0c 会尽量详细的把每个点都解释到 首先我选择了用webpack2来作为打包工具 x
  • C- unsigned :1之位域分析

    1 首先回忆结构体 我们都知道定义一个结构体可以这样的方式定义 struct Point float x float y point 等价于 struct Point point 除此之外 如果不想声明结构体 只想定义结构体的话 还可以这样
  • ShareSDK自定义UI的方法

    说明 xff1a 我们的分享菜单可以修改背景 xff0c 里面的图标以及文字 xff0c 颜色等 xff0c 另外可以自己自定义UI xff0c 用自己的方法写界面 xff0c 写好了之后可以调用我们以下无UI的分享方法 xff0c 另外我
  • HttpServletRequest & HttpServletResponse 中 Body 的获取

    为什么80 的码农都做不了架构师 xff1f gt gt gt 获取 HttpServletRequest 中的请求体 HttpServletRequest getInputStream 获取到请求的输入流 xff0c 从该输入流中可以读取
  • 图像学习之如何理解方向梯度直方图(Histogram Of Gradient)

    特征描述子 Feature Descriptor 特征描述子就是图像的表示 xff0c 抽取了有用的信息 xff0c 丢掉了不相关的信息 通常特征描述子会把一个w h 3 宽高3 xff0c 3个channel 的图像转换成一个长度为n的向
  • SQL查询语句练习题27道

    练习环境为 xff1a XP 43 SQL2000 数据库 练习使用的数据库为 xff1a 学生管理数据库 数据库下载地址为 http download csdn net download friendan 4648150 说明 这是我在学
  • 使用IDEA社区版如何创建SpringBoot项目?

    Spring Boot 就是 Spring 框架的脚 架 xff0c 它就是为了快速开发 Spring 框架 诞 的 首先谈谈SpringBoot的优点 xff1a 1 快速集成框架 xff0c Spring Boot 提供了启动添加依赖的
  • 从零转行数据分析的亲身经历

    作者 xff1a xiaoyu 微信公众号 xff1a Python数据科学 知乎 xff1a python数据分析师 快两周没更新了 xff0c 先跟大家说一下抱歉 最近生活上确实有点忙 xff0c 不过后续将恢复正常 今天和大家聊一个非
  • NodeBB搭建,维护,discuz!数据迁移

    为什么选择了NodeBB 无法回答 NodeBB官方Github NodeBB中文论坛 NodeBB官方文档 NodeBB中文文档 安装 此处的方式是Docker安装部署 https hub docker com r nodebb dock
  • _vimrc

    为什么80 的码农都做不了架构师 xff1f gt gt gt code 34 34 34 34 34 34 34 34 34 34 34 34 34 34 34 34 34 34 34 34 34 34 34 34 34 34 34 34
  • ubuntu编译qemu报错:‘ERROR: pixman >= 0.21.8 not present.’

    在ubuntu14 04中用源码方式编译安装qemu时 xff0c 执行 configure步骤提示错误 xff1a configure ERROR pixman gt 61 0 21 8 not present Your options
  • mysql my.conf 配置_Mysql my.conf配置说明

    MySQL配置文件my cnf 例子最详细翻译 可以保存做笔记用 BEGIN CONFIG INFO DESCR 4GB RAM 只使用InnoDB ACID 少量的连接 队列负载大 TYPE SYSTEM END CONFIG INFO
  • [译]使用 Rust 编写快速安全的原生 Node.js 模块

    原文地址 xff1a Writing fast and safe native Node js modules with Rust原文作者 xff1a Peter Czibik译文出自 xff1a 掘金翻译计划本文永久链接 xff1a gi