数か月前、私たちは TypeScript 用のオープンソース バックエンド フレームワークである Encore.ts をリリースしました。
すでに多くのフレームワークが存在するため、私たちが行った珍しい設計上の決定のいくつかと、それがどのようにして驚くべきパフォーマンス数値につながるのかを共有したいと思いました。
以前、Encore.ts が Express の 9 倍、Fastify の 2 倍高速であることを示すベンチマークを公開しました。
今回は、2 つの最新の高性能 TypeScript フレームワークである ElysiaJS および Hono に対して Encore.ts をベンチマークしました。
スキーマ検証ありとなしの両方で各フレームワークのベンチマークを行い、ElsyiaJS と Hono での検証に TypeBox を使用しました。これは、これらのフレームワークでネイティブにサポートされている検証ライブラリであるためです。 (Encore.ts には、エンドツーエンドで機能する独自の型検証が組み込まれています。)
各ベンチマークについて、5 回の実行のうち最良の結果を取得しました。各実行は、150 人の同時ワーカーで 10 秒以上、できるだけ多くのリクエストを行うことによって実行されました。負荷生成は、Rust および Tokio ベースの HTTP 負荷テスト ツールである oha を使用して実行されました。
話はこれくらいにして、数字を見てみましょう!
(GitHub でベンチマーク コードを確認してください。)
パフォーマンスは別として、Encore.ts は Node.js との 100% の互換性を維持しながらこれを実現します。
どのようにしてこれが可能ですか?テストの結果、パフォーマンスの 3 つの主要な原因が特定されました。これらはすべて、Encore.ts が内部でどのように動作するかに関係しています。
Node.js は、シングルスレッドのイベント ループを使用して JavaScript コードを実行します。シングルスレッドの性質にもかかわらず、これはノンブロッキング I/O 操作を使用し、基盤となる V8 JavaScript エンジン (Chrome にも搭載) が非常に最適化されているため、実際には非常にスケーラブルです。
しかし、シングルスレッドのイベント ループよりも高速なものがあることをご存知ですか?マルチスレッドのもの。
Encore.ts は 2 つの部分で構成されています:
Encore.ts を使用してバックエンドを作成するときに使用する TypeScript SDK。
Rust で書かれたマルチスレッドの非同期イベント ループを備えた高性能ランタイム (Tokio と Hyper を使用)。
Encore ランタイムは、受信した HTTP リクエストの受け入れや処理など、すべての I/O を処理します。これは、基盤となるハードウェアがサポートする限り多くのスレッドを利用する、完全に独立したイベント ループとして実行されます。
リクエストが完全に処理されデコードされると、Node.js イベント ループに渡され、API ハンドラーから応答を取得してクライアントに書き戻されます。
(先に言っておきます: はい、イベント ループにイベント ループを入れたので、イベント ループ中にイベント ループを行うことができます。)
Encore.ts は、名前が示すように、TypeScript 用にゼロから設計されています。ただし、実際に TypeScript を実行することはできません。まず、すべての型情報を削除して JavaScript にコンパイルする必要があります。これは、実行時の型安全性の達成がはるかに難しく、受信リクエストの検証などが困難になることを意味し、代わりに実行時に API スキーマを定義するための Zod のようなソリューションが普及しています。
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{ // ... }, );
Encore.ts はソース コードを解析して、HTTP ヘッダー、クエリ パラメーターなどを含む、各 API エンドポイントが予期するリクエストとレスポンスのスキーマを理解します。その後、スキーマが処理され、最適化され、Protobuf ファイルとして保存されます。
Encore ランタイムは起動時に、この 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 リクエストを解析する代わりにアプリケーション ビジネス ロジックの実行に集中できるようになり、パフォーマンスがさらに向上します。
注意深い読者は傾向に気づいたかもしれません。パフォーマンスの鍵は、シングルスレッドの 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("user-signups", { deliveryGuarantee: "at-least-once", }); // To publish: await UserSignups.publish({ userID: "123", email: "[email protected]" });
「では、どの Pub/Sub テクノロジーが使用されているのでしょうか?」
— 全部です!
Encore Rust ランタイムには、AWS SQS SNS、GCP Pub/Sub、NSQ などの最も一般的な Pub/Sub テクノロジーの実装が含まれており、さらに多くの実装が計画されています (Kafka、NATS、Azure Service Bus など)。アプリケーションの起動時にランタイム構成でリソースごとに実装を指定することも、Encore の Cloud DevOps 自動化に処理させることもできます。
Encore.ts には、Pub/Sub 以外にも、PostgreSQL データベース、Secret、Cron ジョブなどのインフラストラクチャ統合が含まれています。
これらのインフラストラクチャ統合はすべて Encore.ts Rust ランタイムに実装されています。
これは、.publish() を呼び出すとすぐにペイロードが Rust に渡され、Rust がメッセージのパブリッシュを処理し、必要に応じて再試行するなどの処理を行います。同じことがデータベース クエリ、Pub/Sub メッセージのサブスクライブなどにも当てはまります。
最終結果として、Encore.ts を使用すると、事実上すべての非ビジネス ロジックが JS イベント ループからオフロードされます。
要するに、Encore.ts を使用すると、真のマルチスレッド バックエンドを「無料」で入手できると同時に、すべてのビジネス ロジックを TypeScript で記述することができます。
このパフォーマンスが重要かどうかは、ユースケースによって異なります。小さな趣味のプロジェクトを構築している場合、それは主に学術的なものになります。しかし、本番環境のバックエンドをクラウドに移行する場合、かなり大きな影響を与える可能性があります。
レイテンシーの短縮はユーザー エクスペリエンスに直接影響します。当たり前のことを言うと、バックエンドが高速になるということは、フロントエンドがよりスムーズになることを意味し、それはユーザーの満足度を高めることを意味します。
スループットが高いということは、より少ないサーバーで同じ数のユーザーにサービスを提供できることを意味し、これはクラウド料金の削減に直接対応します。または、逆に、同じ数のサーバーでより多くのユーザーにサービスを提供できるため、パフォーマンスのボトルネックに遭遇することなくさらに拡張できます。
私たちは偏見を持っていますが、Encore は TypeScript で高性能のバックエンドを構築するための非常に優れた、世界最高のソリューションを提供すると考えています。高速でタイプセーフで、Node.js エコシステム全体と互換性があります。
すべてオープンソースなので、コードをチェックアウトして GitHub に貢献できます。
または、試してみて、ご意見をお聞かせください!
免責事項: 提供されるすべてのリソースの一部はインターネットからのものです。お客様の著作権またはその他の権利および利益の侵害がある場合は、詳細な理由を説明し、著作権または権利および利益の証拠を提出して、電子メール [email protected] に送信してください。 できるだけ早く対応させていただきます。
Copyright© 2022 湘ICP备2022001581号-3