Пару месяцев назад мы выпустили Encore.ts — фреймворк для бэкенда с открытым исходным кодом для TypeScript.
Поскольку уже существует множество фреймворков, мы хотели поделиться некоторыми необычными проектными решениями и тем, как они приводят к замечательным показателям производительности.
Бенчмарки производительности
Мы протестировали Encore.ts, Bun, Fastify и Express как с валидацией схемы, так и без нее. Для валидации схемы мы использовали Zod, где это было возможно. В случае Fastify как официально поддерживаемую библиотеку валидации схемы мы использовали Ajv.
Для каждого бенчмарка мы брали лучший результат пяти прогонов. Каждый прогон выполнял как можно больше запросов с одновременно работающими 150 воркерами на протяжение более 10 с. Генерация нагрузки выполнялась с oha — инструментом HTTP для нагрузочного тестирования на базе Rust и Tokio.
Но хватит уже разговоров, посмотрим цифры!
Encore.ts обрабатывает в 9 раз больше запросов в секунду, чем Express.js
Encore.ts имеет задержку ответа на 80 % короче, чем Express.js
Ознакомьтесь с кодом бенчмарков на GitHub.
Помимо производительности, Encore.ts достигает этого, сохраняя 100% совместимость с Node.js.
Как это возможно? В результате тестирования мы определили три основных источника производительности, каждый из которых связан с тем, как Encore.ts работает под капотом.
Буст №1. Цикл событий внутри цикла событий
Node.js запускает код JavaScript, используя однопоточный цикл обработки событий. Несмотря на свою однопоточную природу, на практике он вполне масштабируем, поскольку использует неблокирующие операции ввода-вывода, а движок JavaScript V8 под ним, который также поддерживает Chrome, чрезвычайно оптимизирован.
Но знаете, какой цикл быстрее однопоточного? Многопоточный.
Encore.ts состоит из двух частей:
- TypeScript SDK, который используется при написании бекэндов с помощью Encore.ts.
- Высокопроизводительный рантайм с многопоточным асинхронным циклом событий, написанный на Rust (с использованием Tokio и Hyper).
Рантайм Encore обрабатывает все операции ввода-вывода, например принимает и обрабатывает входящие HTTP-запросы. Это работает как полностью независимый цикл событий, который использует столько потоков, сколько поддерживает оборудование.
Как только запрос полностью обработан и декодирован, он передается в цикл событий Node.js, а затем принимает ответ от обработчика API и записывает его обратно клиенту.
Прежде чем вы это скажете: да, мы добавили цикл событий в ваш цикл событий, чтобы вы могли выполнять цикл событий во время работы цикла событий.
Буст №2. Предварительное вычисление схем запросов
Encore.ts, как следует из названия, с нуля разработан для TypeScript. Но на самом деле запустить TypeScript вы не сможете: сначала его нужно скомпилировать в JavaScript, удалив всю информацию о типах. Это означает, что обеспечить безопасность типов во время выполнения гораздо труднее, что осложняет выполнение таких задач, как валидация входящих запросов. Это приводит к тому, что решения вроде Zod становятся популярными в определении схем API во время выполнения.
Encore.ts работает по-другому. С Encore типобезопасные API определяются через нативные типы 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<BlogPost> {
// ...
},
);
Затем Encore.ts анализирует исходный код, чтобы понять схему запросов и ответов, которую ожидает каждая конечная точка API, включая такие вещи, как заголовки HTTP, параметры запроса и т. д. Затем схемы обрабатываются, оптимизируются и сохраняются в виде файла Protobuf.
Когда среда выполнения Encore запускается, она считывает этот файл Protobuf и предварительно вычисляет декодер запроса и кодер ответа, оптимизированные для каждой конечной точки API, используя точное определение типа, которое ожидает каждая конечная точка API. Фактически, Encore.ts даже обрабатывает валидвацию запросов прямо в Rust. Так гарантируется, что невалидные запросы даже не затрагивают слой JS, а это смягчает многие атаки типа «отказ в обслуживании».
Разбор схемы запроса Encore также оказывается полезным с точки зрения производительности. Рантаймы JavaScript, такие как Deno и Bun, используют архитектуру, аналогичную архитектуре рантайма Encore на основе Rust (на самом деле Deno также использует Rust+Tokio+Hyper), но им не хватает разбора схемы запроса в Encore. В итоге для выполнения им необходимо передавать необработанные HTTP-запросы однопоточному движку JavaScript.
Encore.ts, с другой стороны, выполняет гораздо большую часть обработки запросов внутри Rust и передает только декодированные объекты запроса. Таким образом цикл событий JavaScript освобождается, чтобы сосредоточиться на выполнении бизнес-логики приложения, а не на анализе HTTP-запросов. Это дает еще больший прирост производительности.
Буст №3. Интеграции инфраструктуры
Внимательные читатели, возможно, заметили тенденцию: ключ к производительности — как можно большая разгрузка однопоточного цикла событий 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<UserSignupEvent>("user-signups", {
deliveryGuarantee: "at-least-once",
});
// To publish:
await UserSignups.publish({ userID: "123", email: "[email protected]" });
— Так какую технологию Pub/Sub он использует?
— Все!
Рантайм Encore содержит реализации большинства распространенных технологий Pub/Sub, включая AWS SQS+SNS, GCP Pub/Sub и NSQ, а запланировано еще больше (Kafka, NATS, Azure Service Bus и т. д.). Вы можете указать реализацию для каждого ресурса в конфигурации рантайма при загрузке приложения или позволить автоматизации Encore Cloud DevOps сделать это за вас.
Помимо Pub/Sub, Encore.ts включает в себя интеграцию инфраструктуры для баз данных PostgreSQL, секретов, Cron-заданий и многого другого.
Все эти инфраструктурные интеграции реализованы в рантайме Encore.ts на языке Rust.
Это означает, что как только вы вызываете .publish()
, полезная нагрузка передается Rust, который позаботится о публикации сообщения, при необходимости повторив попытку и так далее. То же самое происходит с запросами к базе данных, подпиской на сообщения Pub/Sub и многим другим.
В итоге с помощью Encore.ts из цикла событий JS выгружается практически вся логика вне бизнес-логики.
По сути, с Encore.ts вы по-настоящему просто так получаете многопоточный бекэнд, но при этом всю свою бизнес-логику можете писать на TypeScript.
Заключение
Важна ли эта производительность, зависит от вашего случая. Если вы создаете небольшой хобби-проект, он в основном учебный. Но если вы переносите бекэнд продакшна в облако, эта производительность может иметь довольно большое влияние.
Более низкая задержка напрямую влияет на впечатления пользователя. Сформулируем очевидное: ускоренный бекэнд означает ускоренный интерфейс, то есть еще более довольных пользователей.
Более высокая пропускная способность означает, что вы можете обслуживать такое же количество пользователей с меньшим количеством серверов, что напрямую связано с меньшими затратами на облако. Или, наоборот, с тем же количеством серверов можно обслуживать больше пользователей, гарантируя возможность дальнейшего масштабирования без возникновения узких мест в производительности.
Хотя мы предвзяты, но считаем, что Encore предлагает отличное, лучшее в мире решение для создания высокопроизводительных бекэндов на TypeScript. Он быстрый, типобезопасный, и он совместим со всей экосистемой Node.js. И все это с открытым исходным кодом, так что вы можете посмотреть его и внести свой вклад на GitHub. Или просто попробуйте его!
Читайте также:
- Zod — гарант безопасности кода TypeScript
- Нужны ли нам веб-компоненты?
- Освоение безопасной для типов JSON-сериализации в TypeScript
Читайте нас в Telegram, VK и Дзен
Перевод статьи Marcus Kohlberg: Encore.ts — 9x faster than Express.js & 3x faster than Bun + Zod