Hace unos meses lanzamos Encore.ts, un marco backend de código abierto para TypeScript.
Dado que ya existen muchos marcos, queríamos compartir algunas de las decisiones de diseño poco comunes que hemos tomado y cómo conducen a cifras de rendimiento notables.
Anteriormente publicamos puntos de referencia que muestran cómo Encore.ts es 9 veces más rápido que Express y 2 veces más rápido que Fastify.
Esta vez hemos comparado Encore.ts con ElysiaJS y Hono, dos marcos TypeScript modernos de alto rendimiento.
Evaluamos cada marco con y sin validación de esquema, utilizando TypeBox para la validación con ElsyiaJS y Hono, ya que es una biblioteca de validación compatible de forma nativa para estos marcos. (Encore.ts tiene su propia validación de tipo incorporada que funciona de un extremo a otro).
Para cada punto de referencia tomamos el mejor resultado de cinco ejecuciones. Cada ejecución se realizó realizando tantas solicitudes como fuera posible con 150 trabajadores simultáneos, más de 10. La generación de carga se realizó con oha, una herramienta de prueba de carga HTTP basada en Rust y Tokio.
¡Basta de hablar, veamos los números!
(Consulta el código de referencia en GitHub.)
Aparte del rendimiento, Encore.ts logra esto manteniendo 100% de compatibilidad con Node.js.
¿Cómo es esto posible? A partir de nuestras pruebas, hemos identificado tres fuentes principales de rendimiento, todas relacionadas con el funcionamiento interno de Encore.ts.
Node.js ejecuta código JavaScript utilizando un bucle de eventos de un solo subproceso. A pesar de su naturaleza de subproceso único, en la práctica es bastante escalable, ya que utiliza operaciones de E/S sin bloqueo y el motor JavaScript V8 subyacente (que también impulsa a Chrome) está extremadamente optimizado.
¿Pero sabes qué es más rápido que un bucle de eventos de un solo subproceso? Uno de subprocesos múltiples.
Encore.ts consta de dos partes:
Un SDK de TypeScript que utiliza al escribir backends usando Encore.ts.
Un tiempo de ejecución de alto rendimiento, con un bucle de eventos asincrónico de subprocesos múltiples escrito en Rust (usando Tokio e Hyper).
Encore Runtime maneja todas las E/S, como aceptar y procesar solicitudes HTTP entrantes. Esto se ejecuta como un bucle de eventos completamente independiente que utiliza tantos subprocesos como admita el hardware subyacente.
Una vez que la solicitud se ha procesado y decodificado por completo, se entrega al bucle de eventos de Node.js y luego toma la respuesta del controlador de API y la escribe de nuevo en el cliente.
(Antes de decirlo: Sí, colocamos un bucle de eventos en su bucle de eventos, para que pueda realizar un bucle de eventos mientras lo hace).
Encore.ts, como su nombre indica, está diseñado desde cero para TypeScript. Pero en realidad no se puede ejecutar TypeScript: primero debe compilarse en JavaScript, eliminando toda la información de tipo. Esto significa que la seguridad de tipos en tiempo de ejecución es mucho más difícil de lograr, lo que dificulta hacer cosas como validar solicitudes entrantes, lo que lleva a que soluciones como Zod se vuelvan populares para definir esquemas API en tiempo de ejecución.
Encore.ts funciona de manera diferente. Con Encore, usted define API con seguridad de tipos utilizando tipos nativos de TypeScript:
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 luego analiza el código fuente para comprender el esquema de solicitud y respuesta que espera cada punto final de API, incluidos elementos como encabezados HTTP, parámetros de consulta, etc. Luego, los esquemas se procesan, optimizan y almacenan como un archivo Protobuf.
Cuando se inicia Encore Runtime, lee este archivo Protobuf y precalcula un decodificador de solicitud y un codificador de respuesta, optimizados para cada punto final de API, utilizando la definición de tipo exacta que espera cada punto final de API. De hecho, Encore.ts incluso maneja la validación de solicitudes directamente en Rust, lo que garantiza que las solicitudes no válidas nunca tengan que tocar la capa JS, lo que mitiga muchos ataques de denegación de servicio.
La comprensión de Encore del esquema de solicitud también resulta beneficiosa desde una perspectiva de rendimiento. Los tiempos de ejecución de JavaScript como Deno y Bun usan una arquitectura similar a la del tiempo de ejecución basado en Rust de Encore (de hecho, Deno también usa Rust Tokio Hyper), pero carecen de la comprensión de Encore del esquema de solicitud. Como resultado, deben entregar las solicitudes HTTP no procesadas al motor JavaScript de subproceso único para su ejecución.
Encore.ts, por otro lado, maneja mucho más procesamiento de solicitudes dentro de Rust y solo entrega los objetos de solicitud decodificados. Al manejar una mayor parte del ciclo de vida de las solicitudes en Rust multiproceso, el bucle de eventos de JavaScript se libera para centrarse en ejecutar la lógica empresarial de la aplicación en lugar de analizar las solicitudes HTTP, lo que genera un aumento de rendimiento aún mayor.
Los lectores atentos podrían haber notado una tendencia: la clave para el rendimiento es descargar la mayor cantidad de trabajo posible del bucle de eventos de JavaScript de un solo subproceso.
Ya hemos visto cómo Encore.ts descarga la mayor parte del ciclo de vida de solicitud/respuesta a Rust. Entonces, ¿qué más queda por hacer?
Bueno, las aplicaciones backend son como sándwiches. Tienes la capa superior crujiente, donde manejas las solicitudes entrantes. En el centro tienes tus deliciosos toppings (es decir, tu lógica de negocio, claro). En la parte inferior tiene su capa de acceso a datos, donde consulta bases de datos, llama a otros puntos finales de API, etc.
No podemos hacer mucho con respecto a la lógica empresarial; después de todo, ¡queremos escribir eso en TypeScript! - pero no tiene mucho sentido que todas las operaciones de acceso a datos acaparen nuestro bucle de eventos JS. Si los moviéramos a Rust, liberaríamos aún más el bucle de eventos para poder concentrarnos en ejecutar el código de nuestra aplicación.
Así que eso es lo que hicimos.
Con Encore.ts, puedes declarar recursos de infraestructura directamente en tu código fuente.
Por ejemplo, para definir un tema de 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]" });
"Entonces, ¿qué tecnología Pub/Sub utiliza?"
— ¡Todos ellos!
El tiempo de ejecución de Encore Rust incluye implementaciones para las tecnologías Pub/Sub más comunes, incluidas AWS SQS SNS, GCP Pub/Sub y NSQ, y hay más planeadas (Kafka, NATS, Azure Service Bus, etc.). Puede especificar la implementación por recurso en la configuración del tiempo de ejecución cuando se inicia la aplicación, o dejar que la automatización Cloud DevOps de Encore se encargue de ello por usted.
Más allá de Pub/Sub, Encore.ts incluye integraciones de infraestructura para bases de datos PostgreSQL, Secrets, Cron Jobs y más.
Todas estas integraciones de infraestructura se implementan en Encore.ts Rust Runtime.
Esto significa que tan pronto como llamas a .publish(), la carga útil se entrega a Rust, quien se encarga de publicar el mensaje, reintentando si es necesario, y así sucesivamente. Lo mismo ocurre con las consultas de bases de datos, la suscripción a mensajes de Pub/Sub y más.
El resultado final es que con Encore.ts, prácticamente toda la lógica no empresarial se descarga del bucle de eventos de JS.
En esencia, con Encore.ts obtienes un backend verdaderamente multiproceso "gratis", y al mismo tiempo puedes escribir toda tu lógica de negocios en TypeScript.
La importancia o no de este rendimiento depende de su caso de uso. Si estás construyendo un pequeño proyecto de pasatiempo, es en gran medida académico. Pero si envías un backend de producción a la nube, puede tener un impacto bastante grande.
La menor latencia tiene un impacto directo en la experiencia del usuario. Para decir lo obvio: un backend más rápido significa una interfaz más ágil, lo que significa usuarios más felices.
Un mayor rendimiento significa que puede atender a la misma cantidad de usuarios con menos servidores, lo que se corresponde directamente con facturas de nube más bajas. O, por el contrario, puede atender a más usuarios con la misma cantidad de servidores, lo que garantiza que puede escalar aún más sin encontrar cuellos de botella en el rendimiento.
Aunque somos parciales, creemos que Encore ofrece una solución excelente y la mejor de todos los mundos para crear backends de alto rendimiento en TypeScript. Es rápido, tiene seguridad de escritura y es compatible con todo el ecosistema Node.js.
Y todo es de código abierto, por lo que puedes consultar el código y contribuir en GitHub.
¡O simplemente pruébalo y cuéntanos lo que piensas!
Descargo de responsabilidad: Todos los recursos proporcionados provienen en parte de Internet. Si existe alguna infracción de sus derechos de autor u otros derechos e intereses, explique los motivos detallados y proporcione pruebas de los derechos de autor o derechos e intereses y luego envíelos al correo electrónico: [email protected]. Lo manejaremos por usted lo antes posible.
Copyright© 2022 湘ICP备2022001581号-3