Vor ein paar Monaten haben wir Encore.ts veröffentlicht – ein Open-Source-Backend-Framework für TypeScript.
Da es bereits viele Frameworks gibt, wollten wir einige der ungewöhnlichen Designentscheidungen, die wir getroffen haben, und wie sie zu bemerkenswerten Leistungszahlen führen, mitteilen.
Wir haben bereits Benchmarks veröffentlicht, die zeigen, dass Encore.ts 9x schneller als Express und 2x schneller als Fastify ist.
Dieses Mal haben wir Encore.ts mit ElysiaJS und Hono verglichen, zwei modernen Hochleistungs-TypeScript-Frameworks.
Wir haben jedes Framework sowohl mit als auch ohne Schemavalidierung einem Benchmarking unterzogen und dabei TypeBox für die Validierung mit ElsyiaJS und Hono verwendet, da es sich um eine nativ unterstützte Validierungsbibliothek für diese Frameworks handelt. (Encore.ts verfügt über eine eigene integrierte Typvalidierung, die durchgängig funktioniert.)
Für jeden Benchmark haben wir das beste Ergebnis aus fünf Durchläufen erzielt. Jeder Lauf wurde durchgeführt, indem so viele Anfragen wie möglich mit 150 gleichzeitigen Arbeitern über einen Zeitraum von mehr als 10 Sekunden gestellt wurden. Die Lastgenerierung wurde mit oha durchgeführt, einem auf Rust und Tokio basierenden HTTP-Lasttest-Tool.
Genug geredet, schauen wir uns die Zahlen an!
(Schauen Sie sich den Benchmark-Code auf GitHub an.)
Abgesehen von der Leistung erreicht Encore.ts dies unter Beibehaltung der 100-prozentigen Kompatibilität mit Node.js.
Wie ist das möglich? Bei unseren Tests haben wir drei Hauptleistungsquellen identifiziert, die alle damit zusammenhängen, wie Encore.ts unter der Haube funktioniert.
Node.js führt JavaScript-Code mithilfe einer Single-Threaded-Ereignisschleife aus. Trotz seiner Single-Thread-Natur ist dies in der Praxis recht skalierbar, da es nicht blockierende I/O-Operationen verwendet und die zugrunde liegende V8-JavaScript-Engine (die auch Chrome antreibt) extrem optimiert ist.
Aber wissen Sie, was schneller ist als eine Single-Thread-Ereignisschleife? Eine mit mehreren Threads.
Encore.ts besteht aus zwei Teilen:
Ein TypeScript SDK, das Sie beim Schreiben von Backends mit Encore.ts verwenden.
Eine Hochleistungslaufzeit mit einer asynchronen Multithread-Ereignisschleife, geschrieben in Rust (unter Verwendung von Tokio und Hyper).
Die Encore-Runtime verarbeitet alle E/A-Vorgänge wie das Akzeptieren und Verarbeiten eingehender HTTP-Anfragen. Dies läuft als völlig unabhängige Ereignisschleife, die so viele Threads nutzt, wie die zugrunde liegende Hardware unterstützt.
Sobald die Anfrage vollständig verarbeitet und dekodiert wurde, wird sie an die Node.js-Ereignisschleife übergeben und nimmt dann die Antwort vom API-Handler entgegen und schreibt sie zurück an den Client.
(Bevor Sie es sagen: Ja, wir fügen eine Ereignisschleife in Ihre Ereignisschleife ein, sodass Sie eine Ereignisschleife ausführen können, während Sie eine Ereignisschleife ausführen.)
Encore.ts wurde, wie der Name schon sagt, von Grund auf für TypeScript entwickelt. Sie können TypeScript jedoch nicht wirklich ausführen: Es muss zunächst in JavaScript kompiliert werden, indem alle Typinformationen entfernt werden. Dies bedeutet, dass die Typsicherheit zur Laufzeit viel schwieriger zu erreichen ist, was beispielsweise die Validierung eingehender Anforderungen erschwert, was dazu führt, dass Lösungen wie Zod stattdessen für die Definition von API-Schemas zur Laufzeit beliebt werden.
Encore.ts funktioniert anders. Mit Encore definieren Sie typsichere APIs mithilfe nativer TypeScript-Typen:
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 analysiert dann den Quellcode, um das Anforderungs- und Antwortschema zu verstehen, das jeder API-Endpunkt erwartet, einschließlich Dinge wie HTTP-Header, Abfrageparameter usw. Die Schemata werden dann verarbeitet, optimiert und als Protobuf-Datei gespeichert.
Wenn die Encore-Runtime gestartet wird, liest sie diese Protobuf-Datei und berechnet vorab einen Anforderungsdecoder und einen Antwortencoder, die für jeden API-Endpunkt optimiert sind, und zwar unter Verwendung der genauen Typdefinition, die jeder API-Endpunkt erwartet. Tatsächlich übernimmt Encore.ts sogar die Anforderungsvalidierung direkt in Rust und stellt so sicher, dass ungültige Anforderungen niemals die JS-Ebene berühren müssen, wodurch viele Denial-of-Service-Angriffe abgemildert werden.
Encores Verständnis des Anforderungsschemas erweist sich auch aus Leistungssicht als vorteilhaft. JavaScript-Laufzeiten wie Deno und Bun verwenden eine ähnliche Architektur wie die auf Rust basierende Laufzeit von Encore (tatsächlich verwendet Deno auch Rust Tokio Hyper), ihnen fehlt jedoch das Verständnis von Encore für das Anforderungsschema. Daher müssen sie die unverarbeiteten HTTP-Anfragen zur Ausführung an die Single-Threaded-JavaScript-Engine übergeben.
Encore.ts hingegen übernimmt einen viel größeren Teil der Anforderungsverarbeitung innerhalb von Rust und übergibt nur die dekodierten Anforderungsobjekte. Durch die Abwicklung eines größeren Teils des Anforderungslebenszyklus in Multithread-Rust wird die JavaScript-Ereignisschleife entlastet und kann sich auf die Ausführung der Anwendungsgeschäftslogik statt auf das Parsen von HTTP-Anforderungen konzentrieren, was zu einer noch größeren Leistungssteigerung führt.
Aufmerksamen Lesern ist vielleicht ein Trend aufgefallen: Der Schlüssel zur Leistung liegt darin, so viel Arbeit wie möglich aus der Single-Threaded-JavaScript-Ereignisschleife zu entlasten.
Wir haben uns bereits angeschaut, wie Encore.ts den Großteil des Anforderungs-/Antwortlebenszyklus an Rust verlagert. Was gibt es also noch zu tun?
Nun, Backend-Anwendungen sind wie Sandwiches. Sie haben die oberste Schicht, in der Sie eingehende Anfragen bearbeiten. Im Mittelpunkt stehen Ihre leckeren Toppings (also natürlich Ihre Geschäftslogik). Unten befindet sich Ihre knusprige Datenzugriffsebene, in der Sie Datenbanken abfragen, andere API-Endpunkte aufrufen und so weiter.
An der Geschäftslogik können wir nicht viel machen – das wollen wir schließlich in TypeScript schreiben! – aber es macht wenig Sinn, wenn alle Datenzugriffsvorgänge unsere JS-Ereignisschleife belasten. Wenn wir diese nach Rust verschieben würden, würden wir die Ereignisschleife weiter entlasten, um uns auf die Ausführung unseres Anwendungscodes konzentrieren zu können.
Das haben wir also getan.
Mit Encore.ts können Sie Infrastrukturressourcen direkt in Ihrem Quellcode deklarieren.
Um beispielsweise ein Pub/Sub-Thema zu definieren:
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]" });
"Welche Pub/Sub-Technologie wird also verwendet?"
— Alle!
Die Encore Rust-Laufzeitumgebung umfasst Implementierungen für die gängigsten Pub/Sub-Technologien, einschließlich AWS SQS SNS, GCP Pub/Sub und NSQ, wobei weitere geplant sind (Kafka, NATS, Azure Service Bus usw.). Sie können die Implementierung auf Ressourcenbasis in der Laufzeitkonfiguration beim Hochfahren der Anwendung angeben oder die Cloud-DevOps-Automatisierung von Encore dies für Sie erledigen lassen.
Über Pub/Sub hinaus umfasst Encore.ts Infrastrukturintegrationen für PostgreSQL-Datenbanken, Secrets, Cron-Jobs und mehr.
Alle diese Infrastrukturintegrationen werden in der Encore.ts Rust Runtime implementiert.
Das bedeutet, dass, sobald Sie .publish() aufrufen, die Nutzlast an Rust übergeben wird, der dafür sorgt, dass die Nachricht veröffentlicht wird, es bei Bedarf erneut versucht und so weiter. Das Gleiche gilt für Datenbankabfragen, das Abonnieren von Pub/Sub-Nachrichten und mehr.
Das Endergebnis ist, dass mit Encore.ts praktisch die gesamte Nicht-Geschäftslogik aus der JS-Ereignisschleife entlastet wird.
Im Wesentlichen erhalten Sie mit Encore.ts ein echtes Multithread-Backend „kostenlos“, während Sie dennoch in der Lage sind, Ihre gesamte Geschäftslogik in TypeScript zu schreiben.
Ob diese Leistung wichtig ist oder nicht, hängt von Ihrem Anwendungsfall ab. Wenn Sie ein kleines Hobbyprojekt erstellen, ist es weitgehend akademisch. Aber wenn Sie ein Produktions-Backend in die Cloud verlagern, kann das ziemlich große Auswirkungen haben.
Eine geringere Latenz hat einen direkten Einfluss auf die Benutzererfahrung. Um das Offensichtliche auszudrücken: Ein schnelleres Backend bedeutet ein schnelleres Frontend und damit zufriedenere Benutzer.
Höherer Durchsatz bedeutet, dass Sie die gleiche Anzahl von Benutzern mit weniger Servern bedienen können, was direkt zu niedrigeren Cloud-Rechnungen führt. Oder Sie können umgekehrt mehr Benutzer mit der gleichen Anzahl von Servern bedienen und so eine weitere Skalierung ohne Leistungsengpässe sicherstellen.
Obwohl wir voreingenommen sind, glauben wir, dass Encore eine ziemlich hervorragende und beste Lösung für die Erstellung leistungsstarker Backends in TypeScript bietet. Es ist schnell, typsicher und mit dem gesamten Node.js-Ökosystem kompatibel.
Und es ist alles Open Source, sodass Sie sich den Code ansehen und auf GitHub beitragen können.
Oder probieren Sie es einfach aus und teilen Sie uns Ihre Meinung mit!
Haftungsausschluss: Alle bereitgestellten Ressourcen stammen teilweise aus dem Internet. Wenn eine Verletzung Ihres Urheberrechts oder anderer Rechte und Interessen vorliegt, erläutern Sie bitte die detaillierten Gründe und legen Sie einen Nachweis des Urheberrechts oder Ihrer Rechte und Interessen vor und senden Sie ihn dann an die E-Mail-Adresse: [email protected] Wir werden die Angelegenheit so schnell wie möglich für Sie erledigen.
Copyright© 2022 湘ICP备2022001581号-3