Разделы документации
ОбзорБыстрый стартРедакции и возможностиМодели и поляРедактор контентаМедиатекаЛокализацияПубликация и работа командыAPI, SDK и генерация типовРасширения и инструментыРабочие проектыЗадания, вебхуки и наблюдаемостьАудит и управление даннымиСоветники и доверие к плагинамКорпоративный входПространства, квоты и масштабированиеCommerce и PortalПрава и безопасностьРазвёртывание и обновленияЛицензии и установка пакетовТекущие ограниченияПомощь и диагностикаДанные в кабинетеCore CMSDeveloper PlatformProduction UseWorkflowOperationsComplianceAI AssistantsPlugin TrustEnterprise IdentityEnterprise ScaleEnterprise DeploymentCommerce BundlePortal BundleNotty CMS DocumentationAuth & SecurityContent ModelingDeploymentEcosystem & Packaging ConventionsEditions and First-party ModulesExtensibilityGetting StartedMedia ManagementModule Extraction PathDraft & PublishUpgrade GuideWebhooks & IntegrationsCookbook: Blog with Next.jsCookbook: Custom PluginCookbook: Multilingual SiteOperations DocsBackup AutomationDeployment BlueprintsRunbook — Восстановление БД из бэкапаRunbook — Плановый деплойRunbook — Реакция на инцидентRunbook — Откат релизаRunbook — Горизонтальное масштабированиеRunbook — Ротация секретовRunbook — Major upgradeSecrets ManagementNotty CMS — Capability MapNotty Configuration ModelGenerated App ContractComponents and Dynamic ZonesMiddleware SystemPerformance & Scaling ToolkitDisaster Recovery PlaybookDistribution Model
Документация / Технический справочник

Performance & Scaling Toolkit

Техническое руководство из исходного проекта Notty. Примеры, параметры и эксплуатационные ограничения.

По функциям модуляОбновлено 2026-09-30

Этот документ описывает встроенные инструменты 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 на каждом инстансе).

[ 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.1
    • rate(notty_db_replica_fallbacks_total[5m]) > 0.05
    • notty_cache_hits_total / (notty_cache_hits_total + notty_cache_misses_total) < 0.7
  • При горизонтальном масштабировании — Redis для кэша и external search

Источник: docs/scaling.md. Снимок документации исходного проекта. Технический справочник сохраняет язык оригинала.