Il y a quelques mois, nous avons publié Encore.ts, un framework backend Open Source pour TypeScript.
Comme il existe déjà de nombreux frameworks, nous souhaitions partager certaines des décisions de conception inhabituelles que nous avons prises et comment elles conduisent à des performances remarquables.
Nous avons déjà publié des tests de performance montrant qu'Encore.ts est 9 fois plus rapide qu'Express et 2 fois plus rapide que Fastify.
Cette fois, nous avons comparé Encore.ts à ElysiaJS et Hono, deux frameworks TypeScript modernes hautes performances.
Nous avons comparé chaque framework avec et sans validation de schéma, en utilisant TypeBox pour la validation avec ElsyiaJS et Hono car il s'agit d'une bibliothèque de validation prise en charge nativement pour ces frameworks. (Encore.ts possède sa propre validation de type intégrée qui fonctionne de bout en bout.)
Pour chaque benchmark, nous avons pris le meilleur résultat sur cinq runs. Chaque exécution a été effectuée en effectuant autant de requêtes que possible avec 150 workers simultanés, sur 10 s. La génération de charge a été réalisée avec oha, un outil de test de charge HTTP basé sur Rust et Tokio.
Assez parlé, voyons les chiffres !
(Consultez le code de référence sur GitHub.)
Outre les performances, Encore.ts y parvient tout en maintenant 100 % de compatibilité avec Node.js.
Comment est-ce possible ? À partir de nos tests, nous avons identifié trois sources principales de performances, toutes liées au fonctionnement d'Encore.ts sous le capot.
Node.js exécute du code JavaScript à l'aide d'une boucle d'événements à thread unique. Malgré sa nature monothread, il est assez évolutif en pratique, car il utilise des opérations d'E/S non bloquantes et le moteur JavaScript V8 sous-jacent (qui alimente également Chrome) est extrêmement optimisé.
Mais vous savez ce qui est plus rapide qu'une boucle d'événement à un seul thread ? Un multithread.
Encore.ts se compose de deux parties :
Un SDK TypeScript que vous utilisez lors de l'écriture de backends à l'aide d'Encore.ts.
Un runtime hautes performances, avec une boucle d'événements asynchrone multithread écrite en Rust (en utilisant Tokio et Hyper).
Encore Runtime gère toutes les E/S, comme l'acceptation et le traitement des requêtes HTTP entrantes. Cela fonctionne comme une boucle d'événements complètement indépendante qui utilise autant de threads que le matériel sous-jacent le prend en charge.
Une fois que la requête a été entièrement traitée et décodée, elle est transmise à la boucle d'événements Node.js, puis prend la réponse du gestionnaire d'API et la réécrit au client.
(Avant de le dire : oui, nous avons mis une boucle d'événement dans votre boucle d'événement, afin que vous puissiez faire une boucle d'événement pendant que vous faites une boucle d'événement.)
Encore.ts, comme son nom l'indique, est conçu dès le départ pour TypeScript. Mais vous ne pouvez pas réellement exécuter TypeScript : il doit d'abord être compilé en JavaScript, en supprimant toutes les informations de type. Cela signifie que la sécurité des types au moment de l'exécution est beaucoup plus difficile à atteindre, ce qui rend difficile la validation des requêtes entrantes, ce qui conduit à ce que des solutions comme Zod deviennent populaires pour définir des schémas d'API au moment de l'exécution.
Encore.ts fonctionne différemment. Avec Encore, vous définissez des API de type sécurisé à l'aide de types TypeScript natifs :
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 analyse ensuite le code source pour comprendre le schéma de demande et de réponse attendu par chaque point de terminaison d'API, y compris des éléments tels que les en-têtes HTTP, les paramètres de requête, etc. Les schémas sont ensuite traités, optimisés et stockés sous forme de fichier Protobuf.
Lorsque Encore Runtime démarre, il lit ce fichier Protobuf et précalcule un décodeur de requête et un encodeur de réponse, optimisés pour chaque point de terminaison d'API, en utilisant la définition de type exacte attendue par chaque point de terminaison d'API. En fait, Encore.ts gère même la validation des requêtes directement dans Rust, garantissant que les requêtes non valides n'auront jamais à toucher la couche JS, atténuant ainsi de nombreuses attaques par déni de service.
La compréhension qu'Encore a du schéma de requête s'avère également bénéfique du point de vue des performances. Les environnements d'exécution JavaScript comme Deno et Bun utilisent une architecture similaire à celle du moteur d'exécution basé sur Rust d'Encore (en fait, Deno utilise également Rust Tokio Hyper), mais ne comprennent pas encore le schéma de requête. En conséquence, ils doivent transmettre les requêtes HTTP non traitées au moteur JavaScript monothread pour exécution.
Encore.ts, en revanche, gère une bien plus grande partie du traitement des requêtes dans Rust et ne remet que les objets de requête décodés. En gérant une bien plus grande partie du cycle de vie des requêtes dans Rust multithread, la boucle d'événements JavaScript est libérée pour se concentrer sur l'exécution de la logique métier de l'application au lieu de l'analyse des requêtes HTTP, ce qui entraîne une amélioration encore plus importante des performances.
Les lecteurs attentifs auront peut-être remarqué une tendance : la clé de la performance est de décharger autant de travail que possible de la boucle d'événements JavaScript monothread.
Nous avons déjà examiné comment Encore.ts décharge la majeure partie du cycle de vie des requêtes/réponses vers Rust. Alors, que reste-t-il à faire ?
Eh bien, les applications backend sont comme des sandwichs. Vous disposez de la couche supérieure croustillante, où vous gérez les demandes entrantes. Au centre, vous avez vos délicieuses garnitures (c'est-à-dire votre logique métier, bien sûr). En bas, vous avez votre couche d'accès aux données, où vous interrogez des bases de données, appelez d'autres points de terminaison d'API, etc.
Nous ne pouvons pas faire grand-chose concernant la logique métier – nous voulons l'écrire en TypeScript, après tout ! - mais cela ne sert pas à grand-chose que toutes les opérations d'accès aux données monopolisent notre boucle d'événements JS. Si nous les déplacions vers Rust, nous libérerions davantage la boucle d'événements pour pouvoir nous concentrer sur l'exécution de notre code d'application.
C'est donc ce que nous avons fait.
Avec Encore.ts, vous pouvez déclarer les ressources d'infrastructure directement dans votre code source.
Par exemple, pour définir un sujet 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]" });
"Alors, quelle technologie Pub/Sub utilise-t-il ?"
— Tous !
Le runtime Encore Rust inclut des implémentations pour les technologies Pub/Sub les plus courantes, notamment AWS SQS SNS, GCP Pub/Sub et NSQ, et d'autres sont prévues (Kafka, NATS, Azure Service Bus, etc.). Vous pouvez spécifier l'implémentation ressource par ressource dans la configuration d'exécution au démarrage de l'application, ou laisser l'automatisation Cloud DevOps d'Encore la gérer pour vous.
Au-delà de Pub/Sub, Encore.ts inclut des intégrations d'infrastructure pour les bases de données PostgreSQL, les secrets, les tâches Cron, etc.
Toutes ces intégrations d'infrastructure sont implémentées dans le runtime Encore.ts Rust.
Cela signifie que dès que vous appelez .publish(), la charge utile est transmise à Rust qui se charge de publier le message, en réessayant si nécessaire, et ainsi de suite. La même chose s'applique aux requêtes de base de données, à l'abonnement aux messages Pub/Sub, etc.
Le résultat final est qu'avec Encore.ts, pratiquement toute la logique non métier est déchargée de la boucle d'événements JS.
Essentiellement, avec Encore.ts, vous obtenez un véritable backend multithread "gratuitement", tout en étant capable d'écrire toute votre logique métier en TypeScript.
L'importance ou non de ces performances dépend de votre cas d'utilisation. Si vous construisez un petit projet de loisir, il est en grande partie académique. Mais si vous transférez un backend de production vers le cloud, cela peut avoir un impact assez important.
Une latence plus faible a un impact direct sur l'expérience utilisateur. Pour énoncer une évidence : un backend plus rapide signifie un frontend plus vif, ce qui signifie des utilisateurs plus satisfaits.
Un débit plus élevé signifie que vous pouvez servir le même nombre d'utilisateurs avec moins de serveurs, ce qui correspond directement à des factures cloud inférieures. Ou, à l'inverse, vous pouvez servir davantage d'utilisateurs avec le même nombre de serveurs, ce qui vous permet d'évoluer davantage sans rencontrer de goulots d'étranglement en termes de performances.
Bien que nous soyons partiaux, nous pensons qu'Encore offre une solution assez excellente, la meilleure du monde, pour créer des backends hautes performances en TypeScript. C'est rapide, c'est sécurisé et c'est compatible avec l'ensemble de l'écosystème Node.js.
Et tout est Open Source, vous pouvez donc consulter le code et contribuer sur GitHub.
Ou essayez-le et dites-nous ce que vous en pensez !
Clause de non-responsabilité: Toutes les ressources fournies proviennent en partie d'Internet. En cas de violation de vos droits d'auteur ou d'autres droits et intérêts, veuillez expliquer les raisons détaillées et fournir une preuve du droit d'auteur ou des droits et intérêts, puis l'envoyer à l'adresse e-mail : [email protected]. Nous nous en occuperons pour vous dans les plus brefs délais.
Copyright© 2022 湘ICP备2022001581号-3