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:
@@ -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 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
// Модель распознала имя в кандидате, но правила могли найти лишь его часть
|
// Модель распознала имя в кандидате, но правила могли найти лишь его часть
|
||||||
// («Жан» вместо «Жан-Поль Дюваль») с более высоким приоритетом и заблокировать
|
// («Жан» вместо «Жан-Поль Дюваль») с более высоким приоритетом и заблокировать
|
||||||
// полное имя при разрешении перекрытий. Убираем такие частичные находки правил,
|
// полное имя при разрешении перекрытий. Убираем такие частичные находки правил,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user