Há alguns meses, lançamos o Encore.ts — uma estrutura de back-end de código aberto para TypeScript.
Como já existem muitas estruturas por aí, gostaríamos de compartilhar algumas das decisões de design incomuns que tomamos e como elas levam a números de desempenho notáveis.
Já publicamos benchmarks mostrando como o Encore.ts é 9x mais rápido que o Express e 2x mais rápido que o Fastify.
Desta vez, comparamos Encore.ts com ElysiaJS e Hono, duas estruturas TypeScript modernas de alto desempenho.
Comparamos cada estrutura com e sem validação de esquema, usando TypeBox para validação com ElsyiaJS e Hono, pois é uma biblioteca de validação com suporte nativo para essas estruturas. (Encore.ts tem sua própria validação de tipo integrada que funciona de ponta a ponta.)
Para cada benchmark obtivemos o melhor resultado de cinco execuções. Cada execução foi realizada fazendo tantas solicitações quanto possível com 150 trabalhadores simultâneos, ao longo de 10s. A geração de carga foi realizada com oha, uma ferramenta de teste de carga HTTP baseada em Rust e Tokio.
Chega de conversa, vamos ver os números!
(Confira o código de benchmark no GitHub.)
Além do desempenho, Encore.ts consegue isso enquanto mantém 100% de compatibilidade com Node.js.
Como isso é possível? Em nossos testes, identificamos três fontes principais de desempenho, todas relacionadas ao modo como o Encore.ts funciona nos bastidores.
Node.js executa código JavaScript usando um loop de eventos de thread único. Apesar de sua natureza de thread único, isso é bastante escalonável na prática, uma vez que usa operações de E/S sem bloqueio e o mecanismo JavaScript V8 subjacente (que também alimenta o Chrome) é extremamente otimizado.
Mas você sabe o que é mais rápido que um loop de eventos de thread único? Um multithread.
Encore.ts consiste em duas partes:
Um SDK TypeScript que você usa ao escrever back-ends usando Encore.ts.
Um tempo de execução de alto desempenho, com um loop de eventos assíncrono e multithread escrito em Rust (usando Tokio e Hyper).
O Encore Runtime lida com todas as E/S, como aceitar e processar solicitações HTTP recebidas. Isso é executado como um loop de eventos completamente independente que utiliza tantos threads quanto o hardware subjacente suporta.
Depois que a solicitação for totalmente processada e decodificada, ela será entregue ao loop de eventos do Node.js e, em seguida, pegará a resposta do manipulador da API e a gravará de volta no cliente.
(Antes que você diga: sim, colocamos um loop de eventos em seu loop de eventos, para que você possa fazer um loop de eventos enquanto faz um loop de eventos.)
Encore.ts, como o nome sugere, foi projetado desde o início para TypeScript. Mas você não pode realmente executar o TypeScript: primeiro ele precisa ser compilado para JavaScript, eliminando todas as informações de tipo. Isso significa que a segurança do tipo em tempo de execução é muito mais difícil de alcançar, o que torna difícil fazer coisas como validar solicitações recebidas, fazendo com que soluções como Zod se tornem populares para definir esquemas de API em tempo de execução.
Encore.ts funciona de maneira diferente. Com o Encore, você define APIs de tipo seguro usando tipos TypeScript nativos:
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 então analisa o código-fonte para entender o esquema de solicitação e resposta que cada endpoint da API espera, incluindo coisas como cabeçalhos HTTP, parâmetros de consulta e assim por diante. Os esquemas são então processados, otimizados e armazenados como um arquivo Protobuf.
Quando o Encore Runtime é iniciado, ele lê este arquivo Protobuf e pré-calcula um decodificador de solicitação e um codificador de resposta, otimizado para cada endpoint de API, usando a definição de tipo exata que cada endpoint de API espera. Na verdade, o Encore.ts ainda lida com a validação de solicitações diretamente no Rust, garantindo que solicitações inválidas nunca precisem tocar na camada JS, mitigando muitos ataques de negação de serviço.
A compreensão do esquema de solicitação da Encore também se mostra benéfica do ponto de vista do desempenho. Tempos de execução de JavaScript como Deno e Bun usam uma arquitetura semelhante à do tempo de execução baseado em Rust do Encore (na verdade, Deno também usa Rust Tokio Hyper), mas carecem do entendimento do esquema de solicitação do Encore. Como resultado, eles precisam entregar as solicitações HTTP não processadas ao mecanismo JavaScript de thread único para execução.
Encore.ts, por outro lado, lida com muito mais processamento de solicitação dentro do Rust e apenas entrega os objetos de solicitação decodificados. Ao lidar com muito mais ciclo de vida da solicitação em Rust multithread, o loop de eventos JavaScript é liberado para se concentrar na execução da lógica de negócios do aplicativo em vez de analisar solicitações HTTP, gerando um aumento de desempenho ainda maior.
Leitores atentos devem ter notado uma tendência: a chave para o desempenho é descarregar o máximo de trabalho possível do loop de eventos JavaScript de thread único.
Já vimos como Encore.ts descarrega a maior parte do ciclo de vida de solicitação/resposta para Rust. Então, o que mais há para fazer?
Bem, os aplicativos de back-end são como sanduíches. Você tem a camada superior, onde lida com as solicitações recebidas. No centro você tem suas deliciosas coberturas (ou seja, sua lógica de negócio, claro). Na parte inferior você tem sua camada de acesso a dados, onde você consulta bancos de dados, chama outros endpoints de API e assim por diante.
Não podemos fazer muito sobre a lógica de negócios - afinal, queremos escrever isso em TypeScript! - mas não faz muito sentido ter todas as operações de acesso a dados sobrecarregando nosso loop de eventos JS. Se os movêssemos para Rust, liberaríamos ainda mais o loop de eventos para podermos nos concentrar na execução do código do nosso aplicativo.
Então foi isso que fizemos.
Com Encore.ts, você pode declarar recursos de infraestrutura diretamente em seu código-fonte.
Por exemplo, para definir um tópico do 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]" });
"Então, qual tecnologia Pub/Sub ele usa?"
— Todos eles!
O tempo de execução do Encore Rust inclui implementações para as tecnologias Pub/Sub mais comuns, incluindo AWS SQS SNS, GCP Pub/Sub e NSQ, com mais planejadas (Kafka, NATS, Azure Service Bus, etc.). Você pode especificar a implementação por recurso na configuração do tempo de execução quando o aplicativo é inicializado ou deixar que a automação Cloud DevOps do Encore cuide disso para você.
Além do Pub/Sub, Encore.ts inclui integrações de infraestrutura para bancos de dados PostgreSQL, Secrets, Cron Jobs e muito mais.
Todas essas integrações de infraestrutura são implementadas no Encore.ts Rust Runtime.
Isso significa que assim que você chamar .publish(), a carga útil será entregue ao Rust, que se encarrega de publicar a mensagem, tentando novamente se necessário, e assim por diante. A mesma coisa acontece com consultas de banco de dados, assinatura de mensagens do Pub/Sub e muito mais.
O resultado final é que, com o Encore.ts, praticamente toda a lógica não comercial é descarregada do loop de eventos JS.
Em essência, com Encore.ts você obtém um back-end verdadeiramente multithread "de graça", ao mesmo tempo em que é capaz de escrever toda a sua lógica de negócios em TypeScript.
Se esse desempenho é importante ou não, depende do seu caso de uso. Se você estiver construindo um pequeno projeto de hobby, ele será em grande parte acadêmico. Mas se você estiver enviando um back-end de produção para a nuvem, isso pode ter um impacto muito grande.
A latência mais baixa tem um impacto direto na experiência do usuário. Para afirmar o óbvio: um back-end mais rápido significa um front-end mais rápido, o que significa usuários mais felizes.
Maior rendimento significa que você pode atender o mesmo número de usuários com menos servidores, o que corresponde diretamente a contas de nuvem mais baixas. Ou, inversamente, você pode atender mais usuários com o mesmo número de servidores, garantindo que você possa escalar ainda mais sem encontrar gargalos de desempenho.
Embora sejamos tendenciosos, achamos que o Encore oferece uma solução excelente e o melhor de todos os mundos para construir back-ends de alto desempenho em TypeScript. É rápido, tem tipo seguro e é compatível com todo o ecossistema Node.js.
E é tudo Open Source, então você pode conferir o código e contribuir no GitHub.
Ou experimente e diga-nos o que você pensa!
Isenção de responsabilidade: Todos os recursos fornecidos são parcialmente provenientes da Internet. Se houver qualquer violação de seus direitos autorais ou outros direitos e interesses, explique os motivos detalhados e forneça prova de direitos autorais ou direitos e interesses e envie-a para o e-mail: [email protected]. Nós cuidaremos disso para você o mais rápido possível.
Copyright© 2022 湘ICP备2022001581号-3