- Utilisation de 2 requêtes séparées + fusion en Python - 1ère requête: ml_detected_anomalies pour les détections récentes - 2ème requête: view_dashboard_entities avec IN clause pour les user-agents - La clause IN permet d'utiliser l'index ClickHouse (splitByChar ne l'utilise pas) - PREWHERE optimise les performances de requête Problème résolu: - unique_ua était toujours à 0 car la jointure LEFT JOIN ne fonctionnait pas - La solution avec IN clause fonctionne car elle utilise l'index sur entity_value Testé avec 141.98.11.0/24: - 5 IPs, 8 détections, 65 user-agents uniques - 141.98.11.209: 68 user-agents différents Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
8.3 KiB
8.3 KiB
🔍 Configuration de ml_detected_anomalies
📊 Structure de la table
Requête: SHOW CREATE TABLE mabase_prod.ml_detected_anomalies
CREATE TABLE mabase_prod.ml_detected_anomalies
(
`detected_at` DateTime,
`src_ip` IPv6,
`ja4` String,
`host` String,
`bot_name` String,
`anomaly_score` Float32,
`threat_level` String,
`model_name` String,
`recurrence` UInt32,
`asn_number` String,
`asn_org` String,
`asn_detail` String,
`asn_domain` String,
`country_code` String,
`asn_label` String,
`hits` UInt64,
`hit_velocity` Float32,
`fuzzing_index` Float32,
`post_ratio` Float32,
`port_exhaustion_ratio` Float32,
`max_keepalives` UInt32,
`orphan_ratio` Float32,
`tcp_jitter_variance` Float32,
`tcp_shared_count` UInt32,
`true_window_size` UInt64,
`window_mss_ratio` Float32,
`alpn_http_mismatch` UInt8,
`is_alpn_missing` UInt8,
`sni_host_mismatch` UInt8,
`header_count` UInt16,
`has_accept_language` UInt8,
`has_cookie` UInt8,
`has_referer` UInt8,
`modern_browser_score` UInt8,
`is_headless` UInt8,
`ua_ch_mismatch` UInt8,
`header_order_shared_count` UInt32,
`ip_id_zero_ratio` Float32,
`request_size_variance` Float32,
`multiplexing_efficiency` Float32,
`mss_mobile_mismatch` UInt8,
`correlated` UInt8,
`reason` String,
`asset_ratio` Float32,
`direct_access_ratio` Float32,
`is_ua_rotating` UInt8,
`distinct_ja4_count` UInt32,
`src_port_density` Float32,
`ja4_asn_concentration` Float32,
`ja4_country_concentration` Float32,
`is_rare_ja4` UInt8,
`header_order_confidence` Float32,
`distinct_header_orders` UInt32,
`temporal_entropy` Float32,
`path_diversity_ratio` Float32,
`url_depth_variance` Float32,
`anomalous_payload_ratio` Float32
)
ENGINE = ReplacingMergeTree(detected_at)
ORDER BY src_ip
TTL detected_at + toIntervalDay(30)
SETTINGS index_granularity = 8192
⚙️ Configuration détaillée
1. Moteur de stockage
ENGINE = ReplacingMergeTree(detected_at)
- Type:
ReplacingMergeTree - Version column:
detected_at - Comportement: Garde la dernière version des lignes dupliquées lors des merges
2. Clé de tri (ORDER BY)
ORDER BY src_ip
- Clé primaire:
src_ip(IPv6) - Optimisation: Les requêtes par IP sont très rapides
- Impact: Les requêtes par date (
detected_at) nécessitent un scan complet
3. Politique de rétention (TTL)
TTL detected_at + toIntervalDay(30)
- Durée actuelle: 30 jours
- Comportement: Les lignes sont supprimées 30 jours après
detected_at - Application: Automatique pendant les opérations de merge
4. Partitionnement
-- Aucun partitionnement explicite
- Statut: Non partitionnée (tuple())
- Impact: Toutes les données dans une seule partition
- Conséquence:
- ✅ Requêtes plus simples
- ❌ OPTIMIZE FINAL plus lent sur grandes tables
- ❌ Impossible de DROPper une partition ancienne
5. Index
SETTINGS index_granularity = 8192
- Granularité: 8192 lignes par marque d'index
- Standard: Valeur par défaut de ClickHouse
📈 Statistiques actuelles
Requête: SELECT count(), min(detected_at), max(detected_at) FROM ml_detected_anomalies
| Métrique | Valeur |
|---|---|
| Total lignes | 57,338 |
| Donnée la plus ancienne | 2026-03-13 20:30:19 |
| Donnée la plus récente | 2026-03-15 17:57:10 |
| Période couverte | ~2 jours |
| TTL actuel | 30 jours |
🔍 Analyse du problème: 212.30.36.0/24
Incident dans api/incidents/clusters
{
"subnet": "212.30.36.0/24",
"unique_ips": 10,
"total_detections": 10,
"first_seen": "2026-03-15T03:55:28",
"last_seen": "2026-03-15T03:55:28"
}
Données dans ml_detected_anomalies
- Âge: ~15 heures (bien dans les 30 jours)
- Statut: Devrait être présent ✅
Pourquoi "Subnet non trouvé" ?
Hypothèses:
-
IPv6 vs IPv4 ⚠️
- La table stocke
src_ipen IPv6 - Les IPs IPv4 sont stockées comme
::ffff:x.x.x.x - Notre requête utilise
replaceRegexpAll(toString(src_ip), '^::ffff:', '') - Vérifier: Est-ce que le nettoyage IPv4 fonctionne correctement ?
- La table stocke
-
ReplacingMergeTree ⚠️
- Les lignes marquées pour suppression peuvent encore être visibles
- Vérifier: Y a-t-il des lignes dupliquées avec
detected_atdifférents ?
-
Données réellement absentes ❌
- Les 10 détections de
212.30.36.0/24ont été supprimées - Cause possible: Bug dans bot_detector_ai ou nettoyage prématuré
- Les 10 détections de
🧪 Tests de diagnostic
Test 1: Vérifier format IPv4
SELECT
src_ip,
toString(src_ip) AS ip_string,
replaceRegexpAll(toString(src_ip), '^::ffff:', '') AS clean_ip
FROM mabase_prod.ml_detected_anomalies
WHERE detected_at >= now() - INTERVAL 1 HOUR
LIMIT 10;
Test 2: Chercher le subnet spécifique
SELECT
count(),
min(detected_at),
max(detected_at)
FROM mabase_prod.ml_detected_anomalies
WHERE
detected_at >= now() - INTERVAL 30 DAY
AND splitByChar('.', replaceRegexpAll(toString(src_ip), '^::ffff:', ''))[1] = '212'
AND splitByChar('.', replaceRegexpAll(toString(src_ip), '^::ffff:', ''))[2] = '30'
AND splitByChar('.', replaceRegexpAll(toString(src_ip), '^::ffff:', ''))[3] = '36';
Test 3: Vérifier les IPs du subnet
SELECT
replaceRegexpAll(toString(src_ip), '^::ffff:', '') AS clean_ip,
count() AS detections,
min(detected_at) AS first_seen,
max(detected_at) AS last_seen
FROM mabase_prod.ml_detected_anomalies
WHERE
detected_at >= now() - INTERVAL 30 DAY
AND splitByChar('.', replaceRegexpAll(toString(src_ip), '^::ffff:', ''))[1] = '212'
AND splitByChar('.', replaceRegexpAll(toString(src_ip), '^::ffff:', ''))[2] = '30'
AND splitByChar('.', replaceRegexpAll(toString(src_ip), '^::ffff:', ''))[3] = '36'
GROUP BY clean_ip
ORDER BY detections DESC
LIMIT 20;
✅ Recommandations
1. Augmenter la rétention (déjà documenté)
-- Passer de 30 à 90 jours
ALTER TABLE mabase_prod.ml_detected_anomalies
MODIFY TTL detected_at + INTERVAL 90 DAY;
2. Ajouter le partitionnement (optionnel)
-- Recréer la table avec partitionnement mensuel
CREATE TABLE mabase_prod.ml_detected_anomalies_new
(
-- ... mêmes colonnes ...
)
ENGINE = ReplacingMergeTree(detected_at)
PARTITION BY toYYYYMM(detected_at) -- Partition par mois
ORDER BY src_ip
TTL detected_at + INTERVAL 90 DAY
SETTINGS index_granularity = 8192;
-- Migrer les données
INSERT INTO ml_detected_anomalies_new SELECT * FROM ml_detected_anomalies;
-- Renommer
RENAME TABLE ml_detected_anomalies TO ml_detected_anomalies_old,
ml_detected_anomalies_new TO ml_detected_anomalies;
-- Drop l'ancienne table après vérification
DROP TABLE ml_detected_anomalies_old;
3. Ajouter un index sur detected_at (optionnel)
-- Ajouter un index secondaire pour les requêtes temporelles
ALTER TABLE mabase_prod.ml_detected_anomalies
ADD INDEX idx_detected_at detected_at TYPE minmax GRANULARITY 8192;
4. Corriger le bug 212.30.36.0/24
Action immédiate:
-- Vérifier si les données existent
SELECT count()
FROM mabase_prod.ml_detected_anomalies
WHERE
detected_at >= toDateTime('2026-03-15 03:00:00')
AND detected_at <= toDateTime('2026-03-15 05:00:00')
AND splitByChar('.', replaceRegexpAll(toString(src_ip), '^::ffff:', ''))[1] = '212'
AND splitByChar('.', replaceRegexpAll(toString(src_ip), '^::ffff:', ''))[2] = '30'
AND splitByChar('.', replaceRegexpAll(toString(src_ip), '^::ffff:', ''))[3] = '36';
Si count = 0: Les données ont été supprimées prématurément (bug bot_detector_ai)
Si count > 0: Il y a un bug dans la requête SQL de l'API subnet
📚 Fichiers à modifier
| Fichier | Modification | Statut |
|---|---|---|
deploy_dashboard_entities_view.sql |
TTL: 30 → 90 jours | ✅ Fait |
deploy_user_agents_view.sql |
TTL: 7 → 90 jours | ✅ Fait |
update_retention_policy.sql |
Script d'application | ✅ Créé |
ml_detected_anomalies |
TTL: 30 → 90 jours | ⏳ À appliquer |
Dernière mise à jour: 2026-03-15
Version: 1.0