[譯]使用 Rust 編寫快速安全的原生 Node.js 模塊

使用 Rust 編寫快速安全的原生 Node.js 模塊

內容梗概 - 使用 Rust 代替 C++ 開發原生 Node.js 模塊!html

RisingStack 去年面臨一件棘手的事:咱們已經儘量讓 Node.js 發揮出最高的性能,然而咱們的服務器開銷仍是達到的最高限度。爲了提升咱們應用的性能(而且下降成本),咱們決定完全重寫它,並將系統遷移到其餘的基礎設施上 - 毫無疑問,這個工做量很大,這裏不詳敘了。 後來我發現,咱們只要寫一個原生模塊就好了!前端

那時候,咱們還沒意識到有更好的方法來解決咱們的性能問題。就在幾周前,我發現有另一個方案可行 採用 Rust 代替 C ++ 來實現原生模塊。 我發現這是一個很好的選擇,這要歸功於它提供的安全性和易用性。node

在這篇 Rust 教程中,我將手把手教你寫一個先進、快速、安全的原生模塊。android

Node.js 服務器的性能問題

咱們的問題在 2016 年底的時候暴露出來,當時咱們一直在研究 Node.js 的監控產品 Trace,該產品於2017年10月與 Keymetrics 合併。 像當時的其餘科技創業公司同樣,咱們將服務部署到 Heroku 上以節省一些基礎設施成本和維護費用。咱們一直在構建微服務架構應用程序,這意味着咱們不少服務都是經過 HTTP(S) 進行通訊的。ios

棘手的問題來了: 咱們想讓各服務之間進行安全的通訊,可是 Heroku 不支持私有網絡,因此咱們不得不實現一個本身的方案。所以,咱們查閱了一些安全認證方案,最終選定了 HTTP 簽名。git

簡要地解釋一下:HTTP 簽名基於非對稱密碼體系。要建立一個 HTTP 簽名,你須要獲取一個請求的全部部分:URL、請求頭、請求體,使用你的私鑰對其簽名。而後,你能夠將公鑰發給將會收到簽名請求的設備,以便它們驗證。github

隨時間流逝,咱們發如今大多數 HTTP 服務器進程中,CPU 利用率已經達到了極限。顯然,一個緣由引發咱們懷疑 - 若是你想加密,那就會發生這樣的問題。chrome

然而,在對 v8-profiler 進行了嚴格分析以後,咱們發現問題不是由加密引發的!是 URL 解析佔用 CPU 最多的時間。爲何?由於要進行驗證,就必須解析 URL 來驗證請求籤名。npm

爲了解決這個問題,咱們決定放棄 Heroku(這其中也有其餘因素),咱們建立了一個包含 Kubernetes 和內部網絡的 Google 雲基礎設施,而不是優化咱們的 URL 解析。編程

是什麼緣由促使我寫這個故事(教程)呢?就在幾周前,我意識到咱們能夠用另外一種方法優化 URL 解析 —— 使用 Rust 寫一個原生庫。

編寫原生模塊 - 須要一個 Rust 模塊

編寫原生代碼應該不那麼難,對吧?

在 RisingStack,咱們奉行工欲善其事,必先利其器的宗旨。咱們常常對更好的軟件構件方式作調查,在必要的時候,也使用 C++ 來編寫原生模塊。

恬不知恥地說一句:我也在博客上寫了個人學習歷程 原生 Node.js 模塊之旅。去看一看!

在此以前,我認爲在絕大多數業務場景中,C++ 是編寫一個快速有效的軟件的正確選擇。然而如今咱們有了現代化的工具(本例中 - Rust),咱們能夠用它花費比之前都少的人力成原本編寫更有效、更安全、更快速的代碼。

讓咱們回到最初的問題:解析一個 URL 難道很困難麼?它包括協議、主機、查詢參數……

URL-parsing-protocol

(出自 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 來運行基準測試,若是所有成功了,你將會看到下圖:

Rust-Node-js-success-screen

測試程序解析了100個URL(隨機產生),咱們的應用只須要一次運行就能夠解析出結果。若是你想作基準測試,請增長 URL 數量(urlParser.js 中的 tryCount)或次數(urlGenerator.js 中的 urlLength)。

顯而易見,在基準測試中表現最好的是 Rust neon 版本,可是隨之數組長度的增長,V8 有愈來愈多的優化空間,他們之間的成績會接近。最終它將超過 Rust neon 實現。

Rust-node-js-benchmark

這只是一個簡單的例子,固然,在這個領域咱們還有不少東西要學習,

後續,咱們能夠進一步優化計算,儘量的利用併發計算提升性能,一些相似 rayon 的 crates 提供給咱們相似的功能。

在 Node.js 中實現 Rust 模塊

但願你今天跟我學到了在 Node.js 中實現 Rust 模塊的方法,今後你能夠從(工具鏈中的)新工具中受益。我想說的是,雖然這是能解決問題的(並且頗有趣),但它並非解決全部性能問題的銀彈。

請記住,在某些場景下,Rust 多是很便利的解決方案

若是你想看看我在 Rust 匈牙利研討會上關於本話題的發言,點這裏

若是你有任何問題或評論,請在下面留言,我將在這回復大家!


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索