fix: устойчивость Redis-кластера под нагрузкой и код-ревью замечания

Redis timeout 200ms давал ложные срабатывания под пиковой нагрузкой на общем
хосте — подняли до 800ms и добавили cpu/mem лимиты сервисам в compose, чтобы
соседи не выедали CPU у Redis. Добавили метрику и WARN на случай, когда
демаскирование не находит соответствие ни по id, ни по отпечатку маски (раньше
тихо превращалось в повторное маскирование без единого следа в логах).

Кластерные узлы (node-a/node-b) получили обе NER-модели (WikiNEuRal для имён,
ruBERT для адресов) — раньше конфиг ссылался на несуществующие свойства и
вторая ступень молча не работала. lb (nginx) и volume для prometheus/grafana
данных зафиксированы в compose.

Плюс код-ревью фиксы: утечка нативных ONNX-ресурсов при ошибке загрузки модели
(BLOCKER), неверный HTTP-статус при сбое обработки, generic Exception в
LlmClient заменён на конкретные, лишние same-package импорты убраны.
This commit is contained in:
Максименко Никита Владимирович
2026-09-22 21:40:18 +03:00
parent e3fbc4140e
commit 6afe3442f2
17 changed files with 948 additions and 63 deletions
+58 -46
View File
@@ -1,41 +1,16 @@
# Основной вариант — JVM: прогретая, она вдвое экономнее native по процессору и
# втрое быстрее по задержке. Холодное окно закрывает прогрев на старте.
# docker compose up pd-guard
# `docker compose up` поднимает кластер целиком: redis, node-a, node-b за
# nginx-балансировщиком на 8080, плюс Prometheus на 9090 и Grafana на 3000.
#
# Наблюдение поднимается вместе со всем стеком: `docker compose up` даёт сервис,
# Prometheus на 9090 и Grafana на 3000 с уже заведённым дашбордом.
# Маскирование детерминировано и работает на любом узле, а вот обратный шаг
# требует общего состояния — отсюда общий Redis между node-a и node-b.
#
# Несколько узлов: маскирование детерминировано и работает на любом узле, а вот
# обратный шаг требует общего состояния — иначе запрос попадёт не на тот узел.
# docker compose --profile cluster up
# Лимиты CPU/RAM: сервер — 4 vCPU. Без лимитов node-a/node-b/prometheus/grafana
# под нагрузкой отжимали CPU у Redis, тот не укладывался в таймаут команд, узлы
# теряли общее состояние и демаскирование съезжало на резервный путь. Лимиты —
# это потолок (docker compose без swarm не умеет в гарантированные reservations),
# но они не дают соседям выесть Redis подчистую.
services:
pd-guard:
image: pd-guard-spring:jvm
build:
context: .
dockerfile: src/main/docker/Dockerfile
ports:
- "8080:8080"
environment:
PDGUARD_MAX_CONCURRENT: "2000"
PDGUARD_STORE_TTL_MINUTES: "30"
# Прогоны на старте, чтобы первые запросы не шли по непрогретому коду.
PDGUARD_WARMUP_ITERATIONS: "2000"
# Вторая ступень распознавания. Модель в репозиторий не входит:
# ./tools/fetch-ner-model.sh. Пока её нет, ступень сама выключится и
# сервис работает на одних правилах.
PDGUARD_NER_ENGINE: rubert
PDGUARD_NER_MODEL: /deployments/models/rubert-ner
volumes:
- ./config:/deployments/config:ro
- ./models:/deployments/models:ro
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"]
interval: 10s
timeout: 2s
retries: 3
prometheus:
image: prom/prometheus:v2.54.1
command:
@@ -46,8 +21,9 @@ services:
- "9090:9090"
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
depends_on:
- pd-guard
- prometheus-data:/prometheus
cpus: 0.5
mem_limit: 512m
grafana:
image: grafana/grafana:11.2.0
@@ -65,11 +41,11 @@ services:
volumes:
- ./monitoring/grafana/provisioning:/etc/grafana/provisioning:ro
- ./monitoring/grafana/dashboards:/var/lib/grafana/dashboards:ro
depends_on:
- prometheus
- grafana-data:/var/lib/grafana
cpus: 0.3
mem_limit: 512m
redis:
profiles: ["cluster"]
image: redis:7-alpine
command: ["redis-server", "--save", "", "--appendonly", "no", "--maxmemory", "1gb", "--maxmemory-policy", "allkeys-lru"]
healthcheck:
@@ -77,26 +53,62 @@ services:
interval: 5s
timeout: 2s
retries: 5
cpus: 1.0
mem_limit: 1200m
node-a: &node
profiles: ["cluster"]
build:
context: .
dockerfile: src/main/docker/Dockerfile
ports:
- "8081:8080"
image: pd-guard-spring:jvm
cpus: 1.0
mem_limit: 2200m
environment:
PDGUARD_STORE_BACKEND: redis
SPRING_DATA_REDIS_HOST: redis
PDGUARD_MAX_CONCURRENT: "2000"
PDGUARD_WARMUP_ITERATIONS: "2000"
# Вторая ступень распознавания — две модели под разные задачи (см.
# NameCascade.java): WikiNEuRal размечает имена, ruBERT — составляющие
# адреса. Модели в репозиторий не входят: ./tools/fetch-ner-model.sh.
# Пока движок не задан, соответствующая часть ступени выключена и
# сервис работает на одних правилах.
PDGUARD_NER_NAME_ENGINE: wikineural
PDGUARD_NER_NAME_MODEL: /deployments/models/wikineural-ner
PDGUARD_NER_ADDRESS_ENGINE: rubert
PDGUARD_NER_ADDRESS_MODEL: /deployments/models/rubert-ner
# ВРЕМЕННО для разового разбора формата тестовых payload'ов — пишет сырые ПД
# в логи узла. LOGGING_LEVEL_* не подходит: relaxed binding из env приводит
# имя логгера к нижнему регистру и не совпадает с ru.pdguard.core.Pipeline
# (заглавная P), поэтому уровень задан через -D, где регистр сохраняется.
# Выключить (убрать переменную) перед официальным нагрузочным прогоном.
JAVA_OPTS: "-Dspring.config.additional-location=optional:file:/deployments/config/ -Dlogging.level.ru.pdguard.core.Pipeline=DEBUG"
volumes:
- ./config:/deployments/config:ro
- ./models:/deployments/models:ro
depends_on:
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"]
interval: 10s
timeout: 2s
retries: 3
node-b:
<<: *node
lb:
image: nginx:1.27-alpine
cpus: 0.3
mem_limit: 128m
ports:
- "8082:8080"
- "8080:80"
volumes:
- ./nginx/lb.conf:/etc/nginx/nginx.conf:ro
depends_on:
node-a:
condition: service_healthy
node-b:
condition: service_healthy
volumes:
prometheus-data:
grafana-data: