Производительность, SLA, кэширование и масштабирование
Как AACsearch масштабируется, где находятся слои кэша, какой SLA по тарифам и какие рычаги доступны, если задержка или throughput становятся узким местом.
Эта страница — инженерный справочник: насколько быстр AACsearch, почему и что можно поменять. Клиентскую матрицу тарифов смотри в Тарифы и лимиты; про 429 конкретно — в Операции → Rate limits.
SLA по тарифам
| Тариф | p95 чтения | p95 ack записи | Доступность | Время реакции |
|---|---|---|---|---|
| Free | best-effort | best-effort | 99.0% | Community |
| Starter | 250 мс | 500 мс | 99.5% | Email, 1 раб. дн. |
| Pro | 150 мс | 300 мс | 99.9% | Приоритет, 4 ч |
| Business | 100 мс | 200 мс | 99.9% | Выделенный, 1 ч |
| Enterprise | По договору | По договору | 99.95% | SLA-контракт |
p95 чтения — время от прихода POST /search в API-шлюз до отправки 200-ответа, считается по организации за последние 30 дней. ack записи — время от приёма write-запроса до того, как запись становится видна последующим чтениям (alias-swap latency на reindex'ах не учитывается — см. Reindexing).
Эти цели — SLO, не контрактные SLA. Для Enterprise приоритет — SLA-контракт.
Бюджет латентности на чтение
Запрос POST /search проходит пять хопов. Стандартный бюджет на хоп:
| Хоп | Бюджет | Код-путь |
|---|---|---|
| API gateway + auth | < 5 мс | packages/api/modules/search/lib/access.ts (проверка токена) |
| Сборка tenant-фильтра | < 2 мс | packages/search/lib/search.ts собирает scoped filter_by |
| Policy cache | < 1 мс | packages/search/lib/policy-cache.ts (TTL 60 с, см. ниже) |
Typesense multi_search | 30–100 мс | Зависит от размера коллекции, dim векторов и per_page |
| Шейпинг ответа + лог | < 5 мс | Включает emit аналитического события (fire-and-forget) |
Общий p95-таргет на Pro: 150 мс для запроса per_page=10 к коллекции в 100k документов.
Слои кэша
В AACsearch три слоя кэша, каждый со своей целью и TTL:
1. Policy cache (серверный)
- Что: Разрешённый план организации, feature flags, ограничения scoped-токенов.
- Где:
packages/search/lib/policy-cache.ts. - TTL: 60 секунд, in-process LRU на каждый API-инстанс.
- Зачем: Резолв плана/entitlement требует 2–3 запросов в БД — кэш убирает их с хот-пути.
- Инвалидация: Неявная (TTL). Апгрейд плана вступает в силу за ≤ 60 с. Для мгновенной инвалидации (например, лимит поднят по запросу клиента) — рестарт API-контейнера.
2. InstantSearch adapter cache (браузерный)
- Что: Одинаковые запросы в коротком окне возвращают предыдущий ответ.
- Где:
packages/instantsearch-adapter/src/cache.ts. - TTL: Конфигурируется, по умолчанию выключен (адаптер не навязывает кэширование).
- Зачем: Полезно когда пользователь возвращает фасет в предыдущее значение — экономит round-trip.
- Инвалидация: Ручная через
client.clearCache().
3. CDN cache (на edge, опционально)
- Что:
GET-эндпоинты (схема коллекции, публичный widget-конфиг) можно кэшировать на edge, если перед AACsearch стоит CDN. - TTL: Задаётся через
Cache-Controlзаголовки AACsearch наGET.POST /searchнамеренно не кэшируется. - Зачем: Снижает нагрузку на хот-путь для read-only метаданных.
Не кэшируйте POST /search на edge. Тело ответа tenant-scoped по bearer-токену; edge-кэш по URL утечёт данные между тенантами.
Throughput и масштабирование
Per-API-key rate limit
Поле rateLimitPerMinute у SearchApiKey (по умолчанию 600) применяется на ключ, в минуту, sliding window в packages/api/modules/search/lib/rate-limit.ts. Тариф поднимает потолок, но не дефолт — лимит ставится на ключ явно.
| Тариф | Макс. rateLimitPerMinute на ключ |
|---|---|
| Free | 60 |
| Starter | 300 |
| Pro | 1 200 |
| Business | 6 000 |
| Enterprise | По договору |
Если один виджет разделяет ключ между сотнями браузеров — per-key ограничение это не та абстракция. Вместо «поднимите кэп» выпустите несколько ключей (по окружению, региону, крупному клиенту) — кэп существует чтобы локализовать побег клиентов.
Месячная квота организации
Независимо от per-minute rate limit, у каждой организации есть месячная квота Search Units (maxSearchesPerMonth). Один поиск ИЛИ одна запись документа = один Search Unit. См. Тарифы → Search Units.
Два режима применения:
- Soft cap (по умолчанию для Free/Starter): 80% — warning, 100% —
quota_exceeded429 с grace-read окном (24 ч) перед тем как и записи начнут падать. - Hard cap: Настраивается per-org. Записи падают на 100%; чтения работают (виджеты не ломаются сразу) до истечения grace-окна.
Логика grace-режимов лежит в packages/payments/lib/entitlements.ts. Дашборд показывает оба состояния в Settings → Billing.
Typesense кластер
Коллекции каждой организации живут в шаредном Typesense-кластере на Free/Starter/Pro. Business и Enterprise могут перейти на выделенный кластер (см. Enterprise → Dedicated cluster).
Характеристики шаред-кластера:
| Метрика | Шаред (Pro) | Выделенный (Enterprise) |
|---|---|---|
| Коллекций на кластер | До ~5 000 | Под клиента |
| Документов на коллекцию | Протестировано до 5 млн | Шардируется выше 10 млн |
| Лимит размерности векторов | 1 536 (OpenAI ada / Cohere v3) | До 4 096 |
| Параллельных reindex-job | 2 на организацию | Под клиента |
Выше потолка шаред-кластера alias-swap reindex (см. Reindexing) начинает заметно конкурировать с другими тенантами. Выделенный кластер — рекомендованный путь после 5 млн документов на коллекцию или устойчивых > 1k QPS.
Postgres
Источник истины AACsearch — Postgres (packages/database схема). Для latency-чувствительных путей API не ходит в Postgres на хот-пути поиска — он идёт в Typesense и в policy cache. В Postgres ходят за:
- Резолвом плана/entitlement (кэшируется, см. policy cache).
- Записями аудит-лога (fire-and-forget — никогда не блокирует ответ).
- Оркестрацией reindex (
SearchSyncOutbox). - Подсчётом квоты (
SearchUsageEvent, батчится).
Пулинг коннектов настроен per-app (apps/saas, apps/marketing, packages/api). Дефолтный размер пула — 20 на реплику. Выше ~50 API-реплик стоит поставить PgBouncer в transaction-pooling режиме чтобы не получить exhaustion.
Observability
За чем смотреть при регрессе производительности:
| Сигнал | Где |
|---|---|
| p50 / p95 / p99 search latency | Операции → Observability |
| Доля 429 (rate-limit + quota) | Дашборд → Analytics → Errors |
| Lag reindex (ingest → searchable) | Дашборд → Indexes → История reindex |
| Typesense memory / CPU | Coolify / Grafana (на шаред-кластере: только ops-команде) |
| Насыщение пулов Postgres | Coolify / Grafana |
Runbook'и — в Операции → Мониторинг и Операции → Troubleshooting.
Когда апскейлиться
Триггеры и рекомендованное действие:
| Симптом | Вероятная причина | Действие |
|---|---|---|
| p95 поиска ползёт выше таргета | Размер коллекции у потолка шаред-кластера | Апгрейд тарифа или переход на dedicated |
429 rate_limit_exceeded с одного ключа | Фронт стреляет search на каждое нажатие | Debounce клиента (200 мс); см. Rate limits |
429 quota_exceeded стабильно до конца месяца | Устойчивый рост выше месячного кэпа | Апгрейд тарифа, либо hard cap повыше на Business+ |
| Reindex-job встают в очередь | Параллельно триггернуто несколько reindex | Сериализовать на уровне приложения; alias-swap — по одному на индекс |
| Vector search заметно медленнее текстового | Dim векторов близки к потолку кластера | Снизить dim (1 536 → 768) или dedicated |
Smoke-тесты производительности
В репо лежит базовая нагрузочная утилита packages/loadtest. Запускать против staging до публичного запуска:
cd packages/loadtest && bun run smokeПрофиль по умолчанию: 10 параллельных виртуальных пользователей × 60 с × POST /search против коллекции _demo. Выхлоп — p50/p95/p99 + error rate.
Не запускайте load-тесты против app.aacsearch.com без согласования с ops — per-key rate limit включится и тест начнёт мерить throughput 429-х, а не поиска.
Что ещё почитать
- Тарифы и лимиты — каноническая матрица квот
- Rate limits — flow диагностики 429
- Reindexing — поведение alias-swap
- Observability — метрики, трейсы, логи
- Enterprise → Dedicated cluster — когда уезжать с шаред-инфры