幾個月前,我們發布了Encore.ts——一個 TypeScript 的開源後端框架。

由於已經有很多框架,我們想分享我們所做的一些異常設計決策以及它們如何帶來顯著的效能資料。

性能基準

我們對 Encore.ts、Bun、Fastify 和 Express 進行了基準測試,無論是否有模式驗證。

對於模式驗證,我們盡可能使用 Zod。就 Fastify 而言,我們使用 Ajv 作為官方支援的模式驗證庫。

對於每個基準測試,我們選取五次執行中的最佳結果。每次執行都是透過 150 個並發工作執行緒發出盡可能多的請求來執行,時間超過 10 秒。負載產生是使用oha執行的,oha 是一個基於 Rust 和 Tokio 的 HTTP 負載測試工具。

廢話不多說,讓我們來看看數字吧!

Encore.ts 每秒處理的請求比 Express.js 多 9 倍

每秒請求數

Encore.ts 的反應延遲比 Express.js 減少 80%

回應延遲

查看GitHub上的基準程式碼。

除了效能之外,Encore.ts 還實現了這一點,同時保持了與 Node.js 100% 的兼容性

這怎麼可能?透過我們的測試,我們確定了效能的三個主要來源,所有這些都與 Encore.ts 的底層工作方式有關。

Boost #1:將事件循環放入事件循環中

Node.js 使用單執行緒事件循環執行 JavaScript 程式碼。儘管它是單線程的,但在實踐中它具有相當大的可擴展性,因為它使用非阻塞 I/O 操作,並且底層的 V8 JavaScript 引擎(也為 Chrome 提供支援)經過了極其優化。

但您知道什麼比單線程事件循環更快嗎?多線程的。

Encore.ts 由兩個部分組成:

  1. 使用 Encore.ts 編寫後端時使用的 TypeScript SDK。

  2. 高效能執行時,具有用 Rust 編寫的多執行緒非同步事件循環(使用TokioHyper )。

Encore Runtime 處理所有 I/O,例如接受和處理傳入的 HTTP 請求。它作為一個完全獨立的事件循環執行,利用底層硬體支援的盡可能多的執行緒。

一旦請求被完全處理和解碼,它就會被移交給 Node.js 事件循環,然後從 API 處理程序取得回應並將其寫回客戶端。

(在你說之前:是的,我們在你的事件循環中放置了一個事件循環,這樣你就可以在事件循環時進行事件循環。)

圖表

Boost #2:預計算請求模式

Encore.ts,顧名思義,是專為 TypeScript 設計的。但您實際上無法執行 TypeScript:它首先必須透過剝離所有類型資訊來編譯為 JavaScript。這意味著執行時類型安全性更難實現,這使得驗證傳入請求之類的事情變得困難,導致像Zod這樣的解決方案在執行時定義 API 模式變得流行。

Encore.ts 的工作方式有所不同。透過 Encore,您可以使用本機 TypeScript 類型定義類型安全的 API:

import { api } from "encore.dev/api";

interface BlogPost {
    id:    number;
    title: string;
    body:  string;
    likes: number;
}

export const getBlogPost = api(
    { method: "GET", path: "/blog/:id", expose: true },
    async ({ id }: { id: number }) => Promise<BlogPost> {
        // ...
    },
);

然後,Encore.ts 解析原始程式碼以了解每個 API 端點所需的請求和回應架構,包括 HTTP 標頭、查詢參數等。然後,對架構進行處理、最佳化並儲存為 Protobuf 檔案。

當 Encore Runtime 啟動時,它會讀取此 Protobuf 檔案並預先計算請求解碼器和回應編碼器,並使用每個 API 端點所需的確切類型定義,針對每個 API 端點進行最佳化。事實上,Encore.ts 甚至直接在 Rust 中處理請求驗證,確保無效請求永遠不必接觸 JS 層,從而減輕許多拒絕服務攻擊。

從效能角度來看,Encore 對請求模式的理解也被證明是有益的。像 Deno 和 Bun 這樣的 JavaScript 執行時使用與 Encore 基於 Rust 的執行時類似的架構(事實上,Deno 也使用 Rust+Tokio+Hyper),但缺乏 Encore 對請求模式的理解。因此,他們需要將未處理的 HTTP 請求交給單執行緒 JavaScript 引擎執行。

另一方面,Encore.ts 在 Rust 內部處理更多的請求處理,並且只移交解碼後的請求物件。透過在多執行緒 Rust 中處理更多的請求生命週期,JavaScript 事件循環可以專注於執行應用程式業務邏輯,而不是解析 HTTP 請求,從而獲得更大的效能提升。

推動#3:基礎設施集成

細心的讀者可能已經注意到一個趨勢:效能的關鍵是盡可能從單執行緒 JavaScript 事件循環中卸載工作。

我們已經了解了 Encore.ts 如何將大部分請求/回應生命週期卸載給 Rust。那麼還有什麼事情要做呢?

嗯,後端應用程式就像三明治。您有硬殼頂層,您可以在其中處理傳入的請求。中間有美味的食材(當然,也就是你的業務邏輯)。在底部有硬殼資料存取層,您可以在其中查詢資料庫、呼叫其他 API 端點等。

我們對業務邏輯無能為力——畢竟我們想用 TypeScript 寫! — 但是讓所有資料存取操作佔用我們的 JS 事件循環並沒有太大意義。如果我們將它們移至 Rust,我們將進一步釋放事件循環,以便能夠專注於執行我們的應用程式程式碼。

這就是我們所做的。

使用 Encore.ts,您可以直接在原始程式碼中聲明基礎架構資源。

例如,定義一個 Pub/Sub 主題:

import { Topic } from "encore.dev/pubsub";

interface UserSignupEvent {
    userID: string;
    email:  string;
}

export const UserSignups = new Topic<UserSignupEvent>("user-signups", {
    deliveryGuarantee: "at-least-once",
});

// To publish:
await UserSignups.publish({ userID: "123", email: "[email protected]" });

“那麼它使用哪種 Pub/Sub 技術?”

- 他們全部!

Encore Rust 執行時包括最常見的 Pub/Sub 技術的實現,包括 AWS SQS+SNS、GCP Pub/Sub 和 NSQ,以及更多計劃中的技術(Kafka、NATS、Azure Service Bus 等)。您可以在應用程式啟動時在執行時間配置中按資源指定實現,或讓 Encore 的 Cloud DevOps 自動化為您處理。

除了 Pub/Sub 之外,Encore.ts 還包括 PostgreSQL 資料庫、Secrets、Cron Jobs 等的基礎架構整合。

所有這些基礎設施整合都在 Encore.ts Rust 執行時中實現。

這意味著,一旦您呼叫.publish() ,有效負載就會移交給 Rust,Rust 負責發布訊息,並在必要時重試,等等。資料庫查詢、訂閱 Pub/Sub 訊息等也是如此。

最終結果是,使用 Encore.ts,幾乎所有非業務邏輯都從 JS 事件循環中卸載。

圖表

本質上,透過 Encore.ts,您可以「免費」獲得真正的多執行緒後端,同時仍能夠在 TypeScript 中編寫所有業務邏輯。

結論

此效能是否重要取決於您的用例。如果您正在建立一個小型愛好專案,那麼它主要是學術性的。但如果您將生產後端發送到雲,它可能會產生相當大的影響。

較低的延遲對使用者體驗有直接影響。顯而易見的是:更快的後端意味著更快的前端,這意味著更快樂的使用者。

更高的吞吐量意味著您可以使用更少的伺服器為相同數量的用戶提供服務,這直接對應於更低的雲端費用。或者,相反,您可以使用相同數量的伺服器為更多用戶提供服務,確保您可以進一步擴展而不會遇到效能瓶頸。

雖然我們有偏見,但我們認為 Encore 為在 TypeScript 中建立高效能後端提供了一個非常出色、最好的解決方案。它速度快、類型安全,並且與整個 Node.js 生態系統相容。

而且它都是開源的,因此您可以查看程式碼並在GitHub上做出貢獻。

或者嘗試一下,讓我們知道您的想法!


原文出處:https://dev.to/encore/encorets-9x-faster-than-expressjs-3x-faster-than-bun-zod-4boe

按讚的人:

共有 0 則留言