Performance & Scaling Toolkit
Техническое руководство из исходного проекта Notty. Примеры, параметры и эксплуатационные ограничения.
Этот документ описывает встроенные инструменты Notty для масштабирования production-нагрузок: кэширование, read replicas, защиту от тяжёлых запросов и интеграцию внешних поисковых движков.
💡 Когда подключать. Single-instance + SQLite/Postgres достаточно для команд до сотен тысяч записей. Описанные ниже механизмы нужны при росте трафика, появлении нескольких инстансов и/или при таблицах с миллионами строк.
1. Кэш-абстракция
packages/server/src/lib/cache предоставляет pluggable кэш с TTL и
тег-инвалидацией. Адаптер по умолчанию — InMemoryCacheAdapter (LRU).
Для горизонтального масштабирования подключите Redis-адаптер через plugin.
Использование
import { cache } from '~/lib/cache';
// Cache-aside: получить из кэша или вычислить через factory
const article = await cache.wrap(
cache.key('content', 'article', id),
() => contentManager.findOne(id),
{ ttlSeconds: 60, tags: ['content:article', `content:article:${id}`] }
);
// Инвалидировать все записи article при записи
await cache.invalidateTags(['content:article']);
Env-переменные
| Переменная | По умолчанию | Описание |
|---|---|---|
NOTTY_CACHE_ENABLED |
true |
false подключает NoopCacheAdapter |
NOTTY_CACHE_MAX_ENTRIES |
10000 |
Максимум записей для in-memory |
NOTTY_CACHE_DEFAULT_TTL |
— | TTL по умолчанию (секунды) |
Подключение Redis-адаптера (через plugin)
import type { NottyPlugin } from '@notty/server';
import { cache } from '~/lib/cache';
import { RedisCacheAdapter } from './redis-adapter';
export default {
name: 'redis-cache',
async setup() {
await cache.registerAdapter(new RedisCacheAdapter(process.env.REDIS_URL!));
},
} satisfies NottyPlugin;
Реализация должна удовлетворять интерфейсу CacheAdapter
(packages/server/src/lib/cache/types.ts).
Метрики
| Метрика | Тип | Лейблы |
|---|---|---|
notty_cache_hits_total |
counter | adapter |
notty_cache_misses_total |
counter | adapter |
notty_cache_evictions_total |
counter | adapter, reason |
notty_cache_entries |
gauge | adapter |
Целевой hit ratio для content reads: > 80%. Низкий hit ratio указывает на слишком короткий TTL или неоптимальные ключи.
2. Read replicas
DatabaseManager поддерживает несколько read replica'ов для PostgreSQL и
MySQL. SELECT-запросы автоматически направляются на replica, INSERT/UPDATE/
DELETE — на primary. При ошибке replica прозрачно делается fallback на primary.
Конфигурация
# Primary (как и было)
DATABASE_URL=postgres://user:pass@primary:5432/notty
# Comma-separated список replicas (round-robin)
DATABASE_REPLICA_URLS=postgres://user:pass@replica-1:5432/notty,postgres://user:pass@replica-2:5432/notty
Read-after-write consistency
Если непосредственно после записи нужно гарантированно прочитать
последнюю версию данных, передайте forcePrimary: true:
import { dbQuery } from '~/lib/db-query';
await contentManager.create(input);
const fresh = await dbQuery('SELECT * FROM articles WHERE id = ?', [id], {
forcePrimary: true,
});
Locking/volatile SELECT запросы (FOR UPDATE, FOR SHARE, nextval,
setval, advisory locks) автоматически остаются на primary, даже если
read replica настроены.
Метрики
notty_db_replica_fallbacks_total{dialect, reason}— частота fallback'ов на primary. Постоянный рост указывает на нестабильную replica.
3. Защита от тяжёлых запросов
Query guardrails
packages/server/src/lib/query-guardrails.ts ограничивает Content API:
- максимум 200 записей за запрос (
MAX_QUERY_LIMIT), - максимум 100k offset (защита от deep pagination),
- максимум 10 populate полей,
- максимум 30 условий фильтрации,
- максимум 10 aggregate функций.
Превышение лимитов возвращает HTTP 400 без выполнения запроса.
Statement timeout (PostgreSQL)
# Прервать запрос дольше 5 секунд
DATABASE_STATEMENT_TIMEOUT_MS=5000
Применяется как statement_timeout в pg pool config — на каждое
соединение, включая replica'ы.
Slow query log
# Запросы дольше 1000ms попадают в notty_db_slow_queries_total и в логи (warning)
DATABASE_SLOW_QUERY_THRESHOLD_MS=1000
Метрика notty_db_slow_queries_total{operation, dialect, target} помогает
найти медленные запросы. target=primary|replica.
4. Search adapters
Встроенный DatabaseSearchAdapter использует LIKE/ILIKE — годится до
сотен тысяч документов. Для бóльших объёмов и расширенных возможностей
(relevance scoring, фасеты, fuzzy) подключите внешний движок:
import { getSearchService } from '~/lib/search';
import { MeilisearchAdapter } from './meilisearch-adapter';
export default {
name: 'meilisearch',
async setup() {
await getSearchService().registerAdapter(
new MeilisearchAdapter(process.env.MEILI_URL!, process.env.MEILI_KEY!)
);
},
};
Адаптер должен реализовать интерфейс SearchAdapter
(packages/types/src/search.ts). Capabilities (relevanceScoring,
highlighting, faceting, fuzzySearch, synonyms) позволяют API
осознанно фолбэчиться на упрощённую реализацию, если движок не
поддерживает фичу.
Reindex большого корпуса
const result = await getSearchService().reindex('article', 500);
console.log(`indexed=${result.indexed} errors=${result.errors} ms=${result.durationMs}`);
reindex работает батчами — память не растёт линейно от размера
content type.
5. Стратегии масштабирования
Single-instance (default)
[ Client ] ─→ [ Notty server ] ─→ [ Postgres ]
│
└─→ in-memory cache, in-process job queue
Подходит для команд до сотен тысяч записей и ~100 RPS.
Read replicas + cache
[ Client ] ─→ [ Notty server ] ─→ [ Primary Postgres ]
│ ↘ [ Replica 1 ] (SELECT)
│ ↘ [ Replica 2 ] (SELECT)
└─→ in-memory cache (per-instance)
DATABASE_REPLICA_URLS + cache snizhaet нагрузку на primary,
но cache остаётся per-instance (warmup time на каждом инстансе).
Horizontal scale + Redis cache + external search
[ LB ] ─→ [ Notty × N ] ─┬→ [ Primary ]
├→ [ Replica × M ]
├→ [ Redis ] (shared cache)
├→ [ Meilisearch ]
└→ [ Object storage (S3) ]
- Stateless server инстансы (никакого локального media).
- Shared Redis для кэша → cache hits сохраняются между инстансами.
- External search движок для миллионов документов.
- Job queue остаётся in-process; для распределённой обработки
замените на Redis-backed адаптер (см.
lib/jobs/queue.ts).
6. Чек-лист перед production
-
DATABASE_URLуказывает на managed PG/MySQL с автобэкапами -
JWT_SECRETсгенерирован (openssl rand -base64 64), не дефолтный -
DATABASE_STATEMENT_TIMEOUT_MS=5000(или меньше) - Prometheus scrape настроен на
/api/metrics - Алёрты на:
rate(notty_db_slow_queries_total[5m]) > 0.1rate(notty_db_replica_fallbacks_total[5m]) > 0.05notty_cache_hits_total / (notty_cache_hits_total + notty_cache_misses_total) < 0.7
- При горизонтальном масштабировании — Redis для кэша и external search
Источник: docs/scaling.md. Снимок документации исходного проекта. Технический справочник сохраняет язык оригинала.