Дело было вечером... Сидел я, развлекался переписыванием фронта маркетплейса с React на Preact с Brotli и нативным CSS, чтобы протестировать максимальную оптимизацию. В попытках добиться предельной производительности и скорости решил провести эксперимент по переписыванию бэка на Rust с сжатием БД в Redis — но это совсем другая история. В общем, эксперименты привели меня к идее сделать SSR на Rust, и по бенчмаркам вышел на 95,000+ RPS на M4. Это уже само по себе неплохо, ниже расскажу подробнее.
Архитектура Rusty-SSR
Rust позволяет более гибко управлять потоками и памятью. В основе Rusty-SSR — пул V8 изолятов, привязка потоков к ядрам и многоуровневый кэш.
1. Пул V8 изолятов для многопоточности
Вместо отдельных процессов используются легковесные V8 изоляты в одном процессе Rust, по одному на поток.
// Инициализация пула
let pool = V8Pool::new(V8PoolConfig {
num_threads: num_cpus::get(), // Используем все ядра
queue_capacity: 512, // Очередь для backpressure
..Default::default()
});
Это позволяет избежать блокировок: если один изолят занят, другие продолжают работу.
2. Привязка потоков к ядрам (Thread Pinning)
Контекстные переключения снижают производительность. Чтобы минимизировать их, каждый поток привязывается к конкретному ядру.
// В рабочем потоке
if let Some(core_id) = cores.get(idx) {
if core_affinity::set_for_current(*core_id) {
tracing::debug!("Worker {} pinned to core {:?}", id, core_id.id);
}
}
Это помогает держать кэш процессора (L1/L2) в актуальном состоянии. В облаке эффект может варьироваться, поэтому рекомендуется проверять на профайлинге.
3. Многоуровневый кэш
Кэширование минимизирует рендеринг. Вместо простой HashMap с блокировками реализован двухуровневый подход:
Hot Cache (L1): Thread-local, для быстрого доступа без синхронизации.
Cold Cache (L2): DashMap для общего доступа между потоками.
Размер кэша задается в элементах (страницах), TTL — в секундах (например, cache_ttl_secs(300)). Метрики доступны через engine.cache_metrics() (hit-rate, hot/cold hits и т.д.).
Префетчинг данных
Для ускорения использованы SSE-инструкции для предварительной загрузки данных в кэш CPU. Это как подогреть кофе заранее, чтобы не ждать.
#[repr(align(64))] // Выравнивание по кэш-линии
pub struct HotCache {
ultra_hot: [Option<HotEntry>; 8],
hot_map: HashMap<u64, HotEntry>,
// ...
}
Внутренняя структура Hot Cache
Hot Cache разделен на ultra-hot массив (8 элементов, для сверхбыстрого доступа) и HashMap (128 элементов). Записи промотируются по LRU-принципу.
let html: Arc<str> = Arc::from(rendered_html.as_str());
cache.insert(url, Arc::clone(&html)); // Клонируется только Arc
Zero-Copy с Arc
HTML хранится как Arc<str>, чтобы избежать копирования между потоками.
let html: Arc<str> = Arc::from(rendered_html.as_str());
cache.insert(url, Arc::clone(&html)); // Клонируется только Arc
Это экономит память для больших страниц.
Оптимизация DashMap
Cold Cache использует DashMap с 128 шардами для снижения contention при многопоточности. Тестирование показало +19% к пропускной способности по сравнению с дефолтными 16 шардами.
Шарды |
Пропускная способность |
Улучшение |
|---|---|---|
16 (default) |
51M elem/s |
baseline |
32 |
57M elem/s |
+12% |
64 |
59M elem/s |
+16% |
128 |
60.6M elem/s |
+19% |
256 |
60.3M elem/s |
+18% |
Надежность и готовность к продакшену
Очередь с таймаутом (request_timeout) предотвращает блокировки.
Обработка ошибок при загрузке бандла.
Полная очистка кэша, включая thread-local.
Бенчмарки
Тесты на Apple M4 (10 ядер), с использованием wrk --latency -t10 -c400/1000 -d30s на loopback, демо-HTML из репозитория, прогретый кэш.
Метрика |
Значение |
Комментарий |
|---|---|---|
Throughput |
95,363 req/s |
Высокая пропускная способность |
Latency p50 |
0.46 ms |
Средняя задержка |
Latency p99 |
4.60 ms |
Под нагрузкой |
В данный момент использую всю эту историю на своем портфолио https://portfolio-production-b677.up.railway.app/. Оно пока сырое и преимущественно под десктоп, но как бенчмарк тоже можно использовать — в нем сложный контент с анимациями и Three.js, но загрузка чрезвычайно быстрая. Портфолио на самом дешевом Redis-пакете.
В реальных сценариях производительность зависит от сети, базы данных и браузера. Но даже небольшое улучшение может снизить затраты на инфраструктуру, а это полезно как минимум для экологии :)

В реальных сценариях производительность зависит от сети, базы данных и браузера. Но даже небольое улучшение может снизить затраты на инфраструктуру а это полезно как минимум для экологии )
Заключение
Rust предлагает инструменты для эффективных веб-серверов. Это мой опыт, который может быть полезен. Код открыт под MIT. Если пробуете, делитесь в комментариях — интересно услышать фидбек.
Ссылки:
Комментарии (3)

Dhwtj
10.12.2025 14:40Вы не обижайтесь, если что
Просто вы написали бенчмарки на статическом HTML. А самое быстрое решение я привёл. Возможно, ваше решение быстрее для каких-то других случаев.

babaiiika Автор
10.12.2025 14:40да, я не обижаюсь ).
Твой пример классный, спасибо. Hello-world на Axum даст ещё больше RPS, но мои бенчи гонялись на полном SSR через V8: полный HTML (демо/портфолиом навароченое/) с прогретым двухуровневым кешем, wrk --latency -t10 -c400/1000 -d30s
Dhwtj
Функционал одинаковый. Но мой быстрее