feat: метрики использования каждой NER-модели и панели дашборда

- Добавлены счётчики pdguard.ner.model.requests и таймеры
  pdguard.ner.model.duration с тегом model (name/address/legal).
- Гистограммы для таймеров моделей включены в MetricsConfiguration.
- Дашборд дополнен панелями «Обращения к моделям» и «Время моделей».
- Prometheus собирает метрики с обеих нод кластера.
- Целевые значения лимитера: min-concurrent 100, target-latency 900.
- Подавление S2925 (Thread.sleep) в тесте лимитера.
This commit is contained in:
dakocha3
2026-09-23 23:51:07 +03:00
parent af18ae8791
commit c022806bf2
6 changed files with 192 additions and 17 deletions
+140 -10
View File
@@ -2149,6 +2149,136 @@
} }
} }
}, },
{
"type": "timeseries",
"title": "Обращения к моделям",
"id": 49,
"datasource": {
"type": "prometheus",
"uid": "PDGUARD_PROM"
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 74
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "PDGUARD_PROM"
},
"expr": "sum by (model) (rate(pdguard_ner_model_requests_total[$__rate_interval]))",
"refId": "A",
"instant": false,
"legendFormat": "{{model}}"
}
],
"fieldConfig": {
"defaults": {
"unit": "reqps",
"custom": {
"fillOpacity": 45,
"lineWidth": 2,
"showPoints": "never",
"stacking": {
"mode": "normal"
}
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "text",
"value": null
}
]
}
},
"overrides": []
},
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom",
"calcs": [
"mean",
"max"
],
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
}
},
{
"type": "timeseries",
"title": "Время моделей",
"id": 50,
"datasource": {
"type": "prometheus",
"uid": "PDGUARD_PROM"
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 74
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "PDGUARD_PROM"
},
"expr": "histogram_quantile(0.95, sum by (le, model) (rate(pdguard_ner_model_duration_seconds_bucket[$__rate_interval])))",
"refId": "A",
"instant": false,
"legendFormat": "{{model}} p95"
}
],
"fieldConfig": {
"defaults": {
"unit": "s",
"custom": {
"fillOpacity": 12,
"lineWidth": 2,
"showPoints": "never",
"stacking": {
"mode": "none"
}
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "text",
"value": null
}
]
}
},
"overrides": []
},
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom",
"calcs": [
"mean",
"max"
],
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
}
},
{ {
"type": "row", "type": "row",
"title": "Ресурсы узла", "title": "Ресурсы узла",
@@ -2157,7 +2287,7 @@
"h": 1, "h": 1,
"w": 24, "w": 24,
"x": 0, "x": 0,
"y": 74 "y": 90
}, },
"collapsed": false "collapsed": false
}, },
@@ -2173,7 +2303,7 @@
"h": 5, "h": 5,
"w": 4, "w": 4,
"x": 0, "x": 0,
"y": 75 "y": 91
}, },
"targets": [ "targets": [
{ {
@@ -2236,7 +2366,7 @@
"h": 5, "h": 5,
"w": 4, "w": 4,
"x": 4, "x": 4,
"y": 75 "y": 91
}, },
"targets": [ "targets": [
{ {
@@ -2291,7 +2421,7 @@
"h": 5, "h": 5,
"w": 4, "w": 4,
"x": 8, "x": 8,
"y": 75 "y": 91
}, },
"targets": [ "targets": [
{ {
@@ -2346,7 +2476,7 @@
"h": 5, "h": 5,
"w": 4, "w": 4,
"x": 12, "x": 12,
"y": 75 "y": 91
}, },
"targets": [ "targets": [
{ {
@@ -2409,7 +2539,7 @@
"h": 5, "h": 5,
"w": 4, "w": 4,
"x": 16, "x": 16,
"y": 75 "y": 91
}, },
"targets": [ "targets": [
{ {
@@ -2464,7 +2594,7 @@
"h": 5, "h": 5,
"w": 4, "w": 4,
"x": 20, "x": 20,
"y": 75 "y": 91
}, },
"targets": [ "targets": [
{ {
@@ -2523,7 +2653,7 @@
"h": 8, "h": 8,
"w": 12, "w": 12,
"x": 0, "x": 0,
"y": 80 "y": 96
}, },
"targets": [ "targets": [
{ {
@@ -2588,7 +2718,7 @@
"h": 8, "h": 8,
"w": 12, "w": 12,
"x": 12, "x": 12,
"y": 80 "y": 96
}, },
"targets": [ "targets": [
{ {
@@ -2643,4 +2773,4 @@
} }
} }
] ]
} }
+2 -2
View File
@@ -8,6 +8,6 @@ scrape_configs:
- job_name: pd-guard - job_name: pd-guard
metrics_path: /actuator/prometheus metrics_path: /actuator/prometheus
static_configs: static_configs:
- targets: ["pd-guard:8080"] - targets: ["node-a:8080", "node-b:8080"]
labels: labels:
instance: "один узел" instance: "кластер из двух узлов"
@@ -58,6 +58,7 @@ public class MetricsConfiguration {
private static boolean needsHistogram(String name) { private static boolean needsHistogram(String name) {
return "pdguard.process".equals(name) return "pdguard.process".equals(name)
|| "pdguard.ner.duration".equals(name) || "pdguard.ner.duration".equals(name)
|| "pdguard.ner.model.duration".equals(name)
|| "http.server.requests".equals(name); || "http.server.requests".equals(name);
} }
} }
@@ -7,6 +7,7 @@ import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import jakarta.annotation.PreDestroy; import jakarta.annotation.PreDestroy;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
@@ -127,6 +128,12 @@ public class NameCascade {
private final Counter candidates; private final Counter candidates;
private final Timer duration; private final Timer duration;
/** Счётчики обращений к каждой модели второй ступени: тег {@code model} — имя движка. */
private final Map<String, Counter> modelRequests;
/** Время работы каждой модели: тег {@code model} — имя движка. */
private final Map<String, Timer> modelDuration;
@Autowired @Autowired
public NameCascade( public NameCascade(
@Value("${pdguard.ner.name-engine:off}") String nameEngine, @Value("${pdguard.ner.name-engine:off}") String nameEngine,
@@ -166,6 +173,36 @@ public class NameCascade {
Timer.builder("pdguard.ner.duration") Timer.builder("pdguard.ner.duration")
.description("Время работы второй ступени") .description("Время работы второй ступени")
.register(meters); .register(meters);
this.modelRequests = modelCounters(meters);
this.modelDuration = modelTimers(meters);
}
/** Счётчики обращений к каждой модели: тег {@code model} — имя движка. */
private static Map<String, Counter> modelCounters(MeterRegistry meters) {
Map<String, Counter> counters = new HashMap<>();
for (String model : new String[] {"name", "address", "legal"}) {
counters.put(
model,
Counter.builder("pdguard.ner.model.requests")
.description("Обращения к модели второй ступени")
.tag("model", model)
.register(meters));
}
return counters;
}
/** Таймеры времени работы каждой модели: тег {@code model} — имя движка. */
private static Map<String, Timer> modelTimers(MeterRegistry meters) {
Map<String, Timer> timers = new HashMap<>();
for (String model : new String[] {"name", "address", "legal"}) {
timers.put(
model,
Timer.builder("pdguard.ner.model.duration")
.description("Время работы модели второй ступени")
.tag("model", model)
.register(meters));
}
return timers;
} }
/** Конструктор для тестов: движки задаются конфигом, метрики — реестром. */ /** Конструктор для тестов: движки задаются конфигом, метрики — реестром. */
@@ -255,6 +292,8 @@ public class NameCascade {
this.busy = meters.counter(NER_REQUESTS_METRIC, OUTCOME_TAG, "busy"); this.busy = meters.counter(NER_REQUESTS_METRIC, OUTCOME_TAG, "busy");
this.candidates = meters.counter("pdguard.ner.candidates"); this.candidates = meters.counter("pdguard.ner.candidates");
this.duration = Timer.builder("pdguard.ner.duration").register(meters); this.duration = Timer.builder("pdguard.ner.duration").register(meters);
this.modelRequests = modelCounters(meters);
this.modelDuration = modelTimers(meters);
} }
public static NameCascade disabled() { public static NameCascade disabled() {
@@ -315,8 +354,8 @@ public class NameCascade {
continue; continue;
} }
examined++; examined++;
collect(text, m.start(), m.end(), found, nameRecogniser); collect(text, m.start(), m.end(), found, "name", nameRecogniser);
collect(text, m.start(), m.end(), found, addressRecogniser); collect(text, m.start(), m.end(), found, "address", addressRecogniser);
} }
// LLAIM Legal NER ищет реквизиты (ИНН, СНИЛС, паспорт), которые не // LLAIM Legal NER ищет реквизиты (ИНН, СНИЛС, паспорт), которые не
// являются словами с заглавной буквы, — отдельный проход по своим // являются словами с заглавной буквы, — отдельный проход по своим
@@ -328,7 +367,7 @@ public class NameCascade {
continue; continue;
} }
examined++; examined++;
collect(text, lm.start(), lm.end(), found, legalRecogniser); collect(text, lm.start(), lm.end(), found, "legal", legalRecogniser);
} }
} }
candidates.increment(examined); candidates.increment(examined);
@@ -349,6 +388,7 @@ public class NameCascade {
int candidateStart, int candidateStart,
int candidateEnd, int candidateEnd,
List<Span> sink, List<Span> sink,
String modelName,
RuBertRecogniser recogniser) { RuBertRecogniser recogniser) {
if (recogniser == null) { if (recogniser == null) {
return; return;
@@ -356,6 +396,7 @@ public class NameCascade {
int from = Math.max(0, candidateStart - CONTEXT_CHARS); int from = Math.max(0, candidateStart - CONTEXT_CHARS);
int to = Math.min(text.length(), candidateEnd + CONTEXT_CHARS); int to = Math.min(text.length(), candidateEnd + CONTEXT_CHARS);
boolean nameFound = false; boolean nameFound = false;
long started = System.nanoTime();
for (Span span : recogniser.recognise(text, from, to, PRIORITY)) { for (Span span : recogniser.recognise(text, from, to, PRIORITY)) {
if (isAccepted(text, candidateStart, candidateEnd, span)) { if (isAccepted(text, candidateStart, candidateEnd, span)) {
sink.add(span); sink.add(span);
@@ -364,6 +405,8 @@ public class NameCascade {
} }
} }
} }
modelRequests.get(modelName).increment();
modelDuration.get(modelName).record(System.nanoTime() - started, TimeUnit.NANOSECONDS);
// Модель распознала имя в кандидате, но правила могли найти лишь его часть // Модель распознала имя в кандидате, но правила могли найти лишь его часть
// («Жан» вместо «Жан-Поль Дюваль») с более высоким приоритетом и заблокировать // («Жан» вместо «Жан-Поль Дюваль») с более высоким приоритетом и заблокировать
// полное имя при разрешении перекрытий. Убираем такие частичные находки правил, // полное имя при разрешении перекрытий. Убираем такие частичные находки правил,
+2 -2
View File
@@ -40,9 +40,9 @@ pdguard:
ttl-minutes: 30 ttl-minutes: 30
# 32 байта в hex; AES-256 ключ шифрования хранилища # 32 байта в hex; AES-256 ключ шифрования хранилища
encryption-key: "46a38b200c6df557a5fd2c8a57ad3fec6b710b9f3e1fef1451d121a094f63573" encryption-key: "46a38b200c6df557a5fd2c8a57ad3fec6b710b9f3e1fef1451d121a094f63573"
min-concurrent: 8 min-concurrent: 100
max-concurrent: 2000 max-concurrent: 2000
target-latency-ms: 200 target-latency-ms: 900
warmup-iterations: 2000 warmup-iterations: 2000
ner: ner:
name-engine: off name-engine: off
@@ -8,6 +8,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@SuppressWarnings("java:S2925") // Thread.sleep ждёт прохождения окна регулировки лимитера
class AdaptiveConcurrencyLimiterTest { class AdaptiveConcurrencyLimiterTest {
@Test @Test