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
+239
View File
@@ -0,0 +1,239 @@
# PD Guard — модуль безопасности персональных данных
Прокси-модуль между системой-потребителем и внешней языковой моделью (LLM).
Идентифицирует персональные данные (ПДН) в тексте, маскирует их перед отправкой
в модель и восстанавливает исходные значения в ответе. Ни один фрагмент ПДН не
покидает контур в открытом виде.
```
Система-потребитель → Модуль (идентификация → маскирование → LLM → демаскирование) → Потребитель
```
---
## Возможности
- **Идентификация ПДН** — 30+ типов: ФИО, паспорт РФ, загранпаспорт, водительское
удостоверение, военный билет, свидетельство о рождении, полис ОМС, ИНН, СНИЛС,
телефон, email, адрес (страна/индекс/город/улица/дом/квартира), дата рождения,
место рождения, гражданство, банковская карта, CVV, PIN, срок действия карты,
держатель карты, банковские реквизиты (счёт, БИК, ОГРН, ОГРНИП, КПП), доход,
биометрия.
- **Три режима маскирования** — звёздочки, токенизация, синтетические данные.
- **Демаскирование** — восстановление исходного текста по `payload_id`.
- **Гибкая настройка под систему** — список систем-потребителей, перечень типов
ПДН, факт маскирования и демаскирования настраиваются без правки кода.
- **Контекстное маскирование** — типы, опасные только в сочетании с другими ПДН
(PIN + номер карты), маскируются по настраиваемому правилу.
- **Устойчивость к вариациям** — регистр, форматы дат (числом и текстом),
разделяющие слова («серия … номер …»), уменьшительные формы имён.
- **Защита от ложных срабатываний** — «поэт Александр Пушкин» и адрес отделения
банка не считаются данными клиента.
- **Производительность** — адаптивный лимитер конкурентности, ~9 500 RPS на одном
узле при p95 ≈ 14 мс.
---
## Быстрый старт
### Требования
- Java 21
- Maven 3.8+ (или обёртка `./mvnc`)
- Docker + Docker Compose (для контейнерного запуска и мониторинга)
### Сборка и запуск
```bash
# сборка fat-jar
./mvnc package -DskipTests
# запуск
java -jar target/pd-guard-spring-1.0.0.jar
```
Сервис поднимется на `http://localhost:8080`. Проверка готовности:
```bash
curl http://localhost:8080/health # → OK
```
### Запуск через Docker Compose
```bash
# одиночный узел + Prometheus + Grafana
docker compose up -d --build
# кластер: 2 узла + nginx-балансировщик + Redis + мониторинг
docker compose --profile cluster up -d --build
```
---
## API
### Маскирование / демаскирование
`POST /process`
Направление определяется по `payload_id`: неизвестный идентификатор — маскируем,
ранее выданный текст маски — возвращаем исходный текст.
```bash
# маскирование
curl -X POST http://localhost:8080/process \
-H "Content-Type: application/json" \
-H "X-System-Id: crm" \
-d '{"payload":"Клиент Иванов Иван Иванович, паспорт 4509 123456, тел +7 916 123-45-67","payload_id":"doc-1"}'
# демаскирование — тот же payload_id и замаскированный текст
curl -X POST http://localhost:8080/process \
-H "Content-Type: application/json" \
-H "X-System-Id: crm" \
-d '{"payload":"Клиент [FIO_1], паспорт 45** ****56, тел +7 *** ***-**-**","payload_id":"doc-1"}'
```
Заголовки:
| Заголовок | Назначение |
|-----------|------------|
| `X-System-Id` | имя системы-потребителя; нет — применяется `default` |
| `X-System-Key` | общий секрет системы; проверяется, если задан в настройках |
Коды ответа: `200` — успех, `400` — невалидный запрос, `403` — система выключена
или неверный ключ, `429` — перегрузка (с `Retry-After`).
### Прокси к LLM (демо-плечо)
`POST /proxy` — показывает всю цепочку: что ушло в модель, что она вернула и что
получил потребитель с восстановленными значениями.
```bash
curl -X POST http://localhost:8080/proxy \
-H "Content-Type: application/json" \
-d '{"prompt":"Клиент Иванов Иван Иванович, карта 4111 1111 1111 1111. Кратко опишите профиль."}'
```
### Служебные
| Метод | Путь | Назначение |
|-------|------|------------|
| GET | `/health` | проба готовности |
| GET | `/admin/config` | действующие настройки систем |
| GET | `/admin/types` | список поддерживаемых типов ПДН |
| POST | `/admin/reload` | перечитать `config/systems.json` без перезапуска |
| GET | `/actuator/health` | Spring Boot health |
| GET | `/actuator/metrics` | метрики Micrometer |
| GET | `/actuator/prometheus` | метрики в формате Prometheus |
---
## Настройка систем-потребителей
Файл `config/systems.json` (путь задаётся свойством `pdguard.systems-file`).
Перечитывается на лету через `POST /admin/reload`.
```json
{
"default": {
"enabled": true,
"demask": true,
"maskMode": "MASK",
"types": ["*"],
"requireCompanion": ["CVV", "PIN", "DATE", "BIRTH_PLACE", "ADDRESS_COUNTRY",
"ACCOUNT_NUMBER", "BIK", "OGRN", "OGRNIP", "KPP", "INCOME", "BIOMETRIC"]
},
"crm": {
"enabled": true,
"demask": false,
"maskMode": "TOKEN",
"types": ["FIO", "PHONE", "EMAIL", "ADDRESS_CITY", "ADDRESS_STREET",
"ADDRESS_HOUSE", "ADDRESS_FLAT"]
},
"analytics": {
"enabled": true,
"demask": false,
"maskMode": "SYNTHETIC",
"types": ["*"]
}
}
```
Поля политики:
| Поле | Назначение |
|------|------------|
| `enabled` | разрешено ли системе обращаться в модуль |
| `demask` | выполнять ли обратное преобразование |
| `maskMode` | `MASK` (звёздочки), `TOKEN` (токены), `SYNTHETIC` (синтетика) |
| `types` | типы ПДН к маскированию; `"*"` — все известные |
| `requireCompanion` | типы, маскируемые только вместе с ПДН другого типа |
| `key` | общий секрет системы (проверяется через `X-System-Key`) |
Новые типы ПДН добавляются через справочник правил (`RuleRegistry`) без
переписывания ядра.
---
## Логирование и метрики
### Логи
В журнал попадают только идентификатор, типы ПДН и их количество. **Значения ПДН
не логируются ни на одном уровне.**
```
payload_id=doc-1 символов=64 найдено={FIO=1, PASSPORT=1, PHONE=1}
```
### Метрики (Prometheus, `/actuator/prometheus`)
| Метрика | Назначение |
|---------|------------|
| `pdguard.process` | длительность обработки (разрез по направлению и системе) |
| `pdguard.pd.detected` | счётчик найденных ПДН по типу и системе |
| `pdguard.requests.rejected` | отклонённые запросы (перегрузка/невалидные/запрет) |
| `pdguard.concurrency.limit` | текущий потолок конкурентности |
| `pdguard.concurrency.in.flight` | запросов в обработке |
| `pdguard.store.chars` | объём хранилища соответствий |
| `pdguard.tokens.processed` | оценка обработанных токенов (для TPS) |
Готовый дашборд Grafana и конфигурация Prometheus — в каталоге `monitoring/`.
---
## Производительность
Нагрузочное тестирование проведено инструментом k6 (профиль 500 → 1000 → 1500 →
2000 VU, одиночный узел, Docker):
| Метрика | Значение |
|---------|----------|
| Пропускная способность | ~9 500 RPS |
| Задержка p50 | 1.03 мс |
| Задержка p95 | 13.87 мс |
| Ошибки | 0.00 % |
Целевой уровень из задания (latency ≤ 0.5 с при RPS 1000) выполнен с большим
запасом. В кластерном режиме (2 узла + nginx) пропускная способность выше.
---
## Ограничения
- Хранилище соответствий по умолчанию — в памяти (`pdguard.store.backend=memory`),
сбрасывается при перезапуске. Для кластера используется Redis.
- NER-модель второй ступени (`models/rubert-ner`) не входит в репозиторий и
скачивается скриптом `tools/fetch-ner-model.sh`; без неё сервис работает на
правилах.
- Демаскирование доступно только системам с `demask: true` и корректным ключом.
---
## План развития
- Подключение NER-модели для распознавания имён и адресов в свободном тексте.
- Расширение перечня документов, удостоверяющих личность.
- Настраиваемые правила контекстного маскирования через конфиг.
- Шифрование хранилища соответствий.
- Интеграция с CI/CD и автоматический прогон нагрузочных тестов.