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:
@@ -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 и автоматический прогон нагрузочных тестов.
|
||||
Reference in New Issue
Block a user