From f4ffe3410aa36c5f8602d44267a61e1e115d60eb Mon Sep 17 00:00:00 2001 From: toto Date: Tue, 7 Apr 2026 22:28:04 +0200 Subject: [PATCH] =?UTF-8?q?perf(clickhouse):=20P1=20=E2=80=94=20partition?= =?UTF-8?q?=20+=20skipping=20indexes=20sur=20ml=5Fdetected=5Fanomalies,=20?= =?UTF-8?q?http=5Flogs,=20agg=5Fhost=5Fip=5Fja4=5F1h?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problème : toutes les requêtes du dashboard WHERE detected_at >= now() - INTERVAL N faisaient un full scan car ml_detected_anomalies avait ORDER BY (src_ip) sans partition ni index temporel. Changements : - 06_ml_tables.sql : * ml_detected_anomalies : PARTITION BY toYYYYMMDD(detected_at) → élagage de partitions journalières sur toutes les requêtes temporelles * INDEX idx_detected_at (minmax) → skip des granules hors plage * INDEX idx_threat_level set(8) → skip pour countIf(threat_level = ...) * INDEX idx_bot_name bloom_filter → skip pour bot_name != '' * ttl_only_drop_parts = 1 → TTL par suppression de partition entière * ml_all_scores : même traitement (PARTITION BY + 2 indexes) - 04_mv_http_logs.sql : * http_logs : INDEX idx_src_ip bloom_filter(0.01) → les requêtes WHERE src_ip = X (analysis.py, variability.py) sautent ~90% des granules sans scanner toute la plage temporelle * INDEX idx_ja4 bloom_filter(0.01) → idem pour filtres JA4 - 05_aggregation_tables.sql : * agg_host_ip_ja4_1h : PROJECTION proj_by_ip ORDER BY (src_ip, window_start, ...) → investigation_summary.py et rotation.py (WHERE src_ip = X) utilisent automatiquement la projection au lieu de scanner tous les window_start - 10_perf_indexes.sql (nouveau) : * Migration ALTER TABLE pour instances existantes * ADD INDEX + MATERIALIZE INDEX pour les 4 tables * ADD PROJECTION + MATERIALIZE PROJECTION pour agg_host_ip_ja4_1h * Note : PARTITION BY sur table existante nécessite recréation (documenté) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- shared/clickhouse/04_mv_http_logs.sql | 8 +- shared/clickhouse/05_aggregation_tables.sql | 11 +- shared/clickhouse/06_ml_tables.sql | 44 +++++++- shared/clickhouse/10_perf_indexes.sql | 113 ++++++++++++++++++++ 4 files changed, 170 insertions(+), 6 deletions(-) create mode 100644 shared/clickhouse/10_perf_indexes.sql diff --git a/shared/clickhouse/04_mv_http_logs.sql b/shared/clickhouse/04_mv_http_logs.sql index 0810f55..55562fd 100644 --- a/shared/clickhouse/04_mv_http_logs.sql +++ b/shared/clickhouse/04_mv_http_logs.sql @@ -87,7 +87,13 @@ CREATE TABLE IF NOT EXISTS ja4_logs.http_logs -- Anubis enrichment columns `anubis_bot_name` LowCardinality(String) DEFAULT '', `anubis_bot_action` LowCardinality(String) DEFAULT '', - `anubis_bot_category` LowCardinality(String) DEFAULT '' + `anubis_bot_category` LowCardinality(String) DEFAULT '', + + -- Index bloom_filter sur src_ip : les requêtes WHERE src_ip = X sautent + -- les granules qui ne contiennent pas cette IP (~90% des granules en pratique). + -- Taux de faux positifs 1% (0.01) : bon compromis taille / efficacité. + INDEX idx_src_ip src_ip TYPE bloom_filter(0.01) GRANULARITY 4, + INDEX idx_ja4 ja4 TYPE bloom_filter(0.01) GRANULARITY 4 ) ENGINE = MergeTree PARTITION BY log_date diff --git a/shared/clickhouse/05_aggregation_tables.sql b/shared/clickhouse/05_aggregation_tables.sql index e0cfec6..3a157c3 100644 --- a/shared/clickhouse/05_aggregation_tables.sql +++ b/shared/clickhouse/05_aggregation_tables.sql @@ -98,7 +98,16 @@ CREATE TABLE IF NOT EXISTS ja4_processing.agg_host_ip_ja4_1h count_correlated SimpleAggregateFunction(sum, UInt64), -- HTTP features count_no_accept_enc SimpleAggregateFunction(sum, UInt64), - count_http_scheme SimpleAggregateFunction(sum, UInt64) + count_http_scheme SimpleAggregateFunction(sum, UInt64), + + -- Projection pour les requêtes d'investigation par IP : + -- ORDER BY actuel (window_start, src_ip, ...) est optimal pour heatmap + -- mais inefficace pour WHERE src_ip = X (IP pas en première position). + -- Cette projection stocke les données triées par (src_ip, ...) et est + -- utilisée automatiquement par ClickHouse pour les filtres sur src_ip. + PROJECTION proj_by_ip ( + SELECT * ORDER BY (src_ip, window_start, ja4, host) + ) ) ENGINE = AggregatingMergeTree() ORDER BY (window_start, src_ip, ja4, host); diff --git a/shared/clickhouse/06_ml_tables.sql b/shared/clickhouse/06_ml_tables.sql index d29fe43..134359e 100644 --- a/shared/clickhouse/06_ml_tables.sql +++ b/shared/clickhouse/06_ml_tables.sql @@ -1,10 +1,25 @@ -- ============================================================================= -- 06_ml_tables.sql — ML detection results tables -- Source: bot_detector/deploy_views.sql sections 6-6b + deploy_schema.sql items 11-12 +-- +-- Optimisations de performance : +-- - ml_detected_anomalies : PARTITION BY date → élagage de partitions sur +-- les requêtes temporelles (WHERE detected_at >= now() - INTERVAL N DAY) +-- - INDEX idx_detected_at (minmax) → skip des granules hors plage temporelle +-- - INDEX idx_threat_level (set) → skip pour les filtres par niveau de menace +-- - ml_all_scores : PARTITION BY date + INDEX identiques -- ============================================================================= -- ----------------------------------------------------------------------------- -- ml_detected_anomalies — anomaly detections above threat threshold +-- +-- Déduplication : ReplacingMergeTree(detected_at) sur ORDER BY (src_ip) +-- → conserve la détection la plus récente par IP. +-- PARTITION BY : élagage journalier (les requêtes 24h/7j ignorent les vieilles +-- partitions sans lire aucune donnée). +-- INDEX idx_detected_at : skip des granules 8192 lignes hors de la plage +-- temporelle demandée (minmax = min/max par granule). +-- INDEX idx_threat_level : skip pour countIf(threat_level = 'CRITICAL') etc. -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS ja4_processing.ml_detected_anomalies ( @@ -32,15 +47,30 @@ CREATE TABLE IF NOT EXISTS ja4_processing.ml_detected_anomalies -- Anubis enrichment (deploy_schema.sql item 11) anubis_bot_name LowCardinality(String) DEFAULT '', anubis_bot_action LowCardinality(String) DEFAULT '', - anubis_bot_category LowCardinality(String) DEFAULT '' + anubis_bot_category LowCardinality(String) DEFAULT '', + + -- Index de saut : skip des granules hors plage temporelle + INDEX idx_detected_at detected_at TYPE minmax GRANULARITY 4, + -- Index de saut : skip pour les filtres sur threat_level (CRITICAL/HIGH/...) + INDEX idx_threat_level threat_level TYPE set(8) GRANULARITY 4, + -- Index de saut : skip pour les filtres bot_name != '' + INDEX idx_bot_name bot_name TYPE bloom_filter() GRANULARITY 4 ) ENGINE = ReplacingMergeTree(detected_at) +PARTITION BY toYYYYMMDD(detected_at) ORDER BY (src_ip) -TTL detected_at + INTERVAL 30 DAY; +TTL detected_at + INTERVAL 30 DAY +SETTINGS + index_granularity = 8192, + ttl_only_drop_parts = 1; -- supprime la partition entière à expiration (plus efficace) -- ----------------------------------------------------------------------------- -- ml_all_scores — all classifications (no threshold, for observability) +-- +-- PARTITION BY date : TTL de 3 jours → les partitions expirées sont supprimées +-- entièrement sans avoir à lire chaque granule (ttl_only_drop_parts). +-- INDEX idx_detected_at : idem ml_detected_anomalies. -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS ja4_processing.ml_all_scores ( @@ -67,12 +97,18 @@ CREATE TABLE IF NOT EXISTS ja4_processing.ml_all_scores -- Anubis enrichment (deploy_schema.sql item 12) anubis_bot_name LowCardinality(String) DEFAULT '', anubis_bot_action LowCardinality(String) DEFAULT '', - anubis_bot_category LowCardinality(String) DEFAULT '' + anubis_bot_category LowCardinality(String) DEFAULT '', + + INDEX idx_detected_at detected_at TYPE minmax GRANULARITY 4, + INDEX idx_threat_level threat_level TYPE set(8) GRANULARITY 4 ) ENGINE = ReplacingMergeTree(detected_at) +PARTITION BY toYYYYMMDD(window_start) ORDER BY (window_start, src_ip, ja4, host, model_name) TTL window_start + INTERVAL 3 DAY -SETTINGS index_granularity = 8192; +SETTINGS + index_granularity = 8192, + ttl_only_drop_parts = 1; -- ----------------------------------------------------------------------------- diff --git a/shared/clickhouse/10_perf_indexes.sql b/shared/clickhouse/10_perf_indexes.sql new file mode 100644 index 0000000..46dc5ba --- /dev/null +++ b/shared/clickhouse/10_perf_indexes.sql @@ -0,0 +1,113 @@ +-- ============================================================================= +-- 10_perf_indexes.sql — Migrations de performance (indexes + vues matérialisées) +-- +-- Ce fichier applique les optimisations de performance sur des tables existantes. +-- Il est idempotent : les ADD INDEX / ADD PROJECTION échouent silencieusement +-- si l'index existe déjà (IF NOT EXISTS non disponible pour les indexes, utiliser +-- la commande IF NOT EXISTS en ClickHouse 24+, sinon ignorer les erreurs ALREADY_EXISTS). +-- +-- Usage : +-- Pour une installation fraîche, ces optimisations sont déjà incluses dans les +-- fichiers 04/05/06_*.sql. Ce fichier sert aux migrations d'instances existantes. +-- +-- clickhouse-client --multiquery < 10_perf_indexes.sql +-- +-- Sections : +-- 1. ml_detected_anomalies — indexes de saut temporels et sémantiques +-- 2. ml_all_scores — index de saut temporel +-- 3. http_logs — bloom filter sur src_ip et ja4 +-- 4. agg_host_ip_ja4_1h — projection per-IP pour les requêtes d'investigation +-- ============================================================================= + +-- NOTE : PARTITION BY ne peut pas être ajouté via ALTER TABLE dans ClickHouse. +-- Pour bénéficier du partitionnement journalier sur une table existante, il faut +-- recréer la table et réinsérer les données : +-- +-- RENAME TABLE ja4_processing.ml_detected_anomalies +-- TO ja4_processing.ml_detected_anomalies_old; +-- (recréer avec 06_ml_tables.sql) +-- INSERT INTO ja4_processing.ml_detected_anomalies +-- SELECT * FROM ja4_processing.ml_detected_anomalies_old; +-- DROP TABLE ja4_processing.ml_detected_anomalies_old; + + +-- ============================================================================= +-- 1. ml_detected_anomalies — indexes de saut +-- ============================================================================= + +-- Index minmax sur detected_at : permet au moteur de sauter les granules +-- (blocs de 8192 lignes) dont le min/max de detected_at est en dehors +-- de la plage temporelle demandée (ex: WHERE detected_at >= now() - INTERVAL 24 HOUR). +ALTER TABLE ja4_processing.ml_detected_anomalies + ADD INDEX IF NOT EXISTS idx_detected_at detected_at TYPE minmax GRANULARITY 4; + +-- Index set(8) sur threat_level : stocke au maximum 8 valeurs distinctes +-- par granule. Permet de sauter les granules qui ne contiennent pas la valeur +-- filtrée (ex: WHERE threat_level = 'CRITICAL' skip les granules sans CRITICAL). +ALTER TABLE ja4_processing.ml_detected_anomalies + ADD INDEX IF NOT EXISTS idx_threat_level threat_level TYPE set(8) GRANULARITY 4; + +-- Index bloom_filter sur bot_name : skip des granules pour bot_name != '' +-- et les filtres par nom exact (ex: WHERE bot_name = 'Googlebot'). +ALTER TABLE ja4_processing.ml_detected_anomalies + ADD INDEX IF NOT EXISTS idx_bot_name bot_name TYPE bloom_filter() GRANULARITY 4; + +-- Matérialiser les indexes sur les données existantes (opération asynchrone). +-- ClickHouse traite ça en tâche de fond ; la table reste accessible en lecture. +ALTER TABLE ja4_processing.ml_detected_anomalies MATERIALIZE INDEX idx_detected_at; +ALTER TABLE ja4_processing.ml_detected_anomalies MATERIALIZE INDEX idx_threat_level; +ALTER TABLE ja4_processing.ml_detected_anomalies MATERIALIZE INDEX idx_bot_name; + + +-- ============================================================================= +-- 2. ml_all_scores — index de saut temporel +-- ============================================================================= + +ALTER TABLE ja4_processing.ml_all_scores + ADD INDEX IF NOT EXISTS idx_detected_at detected_at TYPE minmax GRANULARITY 4; +ALTER TABLE ja4_processing.ml_all_scores + ADD INDEX IF NOT EXISTS idx_threat_level threat_level TYPE set(8) GRANULARITY 4; + +ALTER TABLE ja4_processing.ml_all_scores MATERIALIZE INDEX idx_detected_at; +ALTER TABLE ja4_processing.ml_all_scores MATERIALIZE INDEX idx_threat_level; + + +-- ============================================================================= +-- 3. http_logs — bloom filter sur src_ip et ja4 +-- +-- http_logs est ORDER BY (time, src_ip, dst_ip, ja4). +-- Les requêtes de type WHERE src_ip = X AND time >= ... ne peuvent pas utiliser +-- l'index de tri efficacement (src_ip n'est pas en première position). +-- Un bloom filter permet de sauter les granules qui ne contiennent pas l'IP +-- recherchée, réduisant la quantité de données lues de ~90% en pratique. +-- ============================================================================= + +ALTER TABLE ja4_logs.http_logs + ADD INDEX IF NOT EXISTS idx_src_ip src_ip TYPE bloom_filter(0.01) GRANULARITY 4; +ALTER TABLE ja4_logs.http_logs + ADD INDEX IF NOT EXISTS idx_ja4 ja4 TYPE bloom_filter(0.01) GRANULARITY 4; + +ALTER TABLE ja4_logs.http_logs MATERIALIZE INDEX idx_src_ip; +ALTER TABLE ja4_logs.http_logs MATERIALIZE INDEX idx_ja4; + + +-- ============================================================================= +-- 4. agg_host_ip_ja4_1h — projection per-IP +-- +-- ORDER BY actuel : (window_start, src_ip, ja4, host) +-- → efficace pour les requêtes heatmap (GROUP BY window_start) +-- → inefficace pour les requêtes d'investigation (WHERE src_ip = X) +-- +-- La projection stocke les données dans un ordre alternatif (src_ip en premier) +-- et est utilisée automatiquement par ClickHouse quand src_ip est filtré. +-- Coût : ~2× l'espace disque pour cette table (acceptable vu sa taille). +-- ============================================================================= + +ALTER TABLE ja4_processing.agg_host_ip_ja4_1h + ADD PROJECTION IF NOT EXISTS proj_by_ip ( + SELECT * ORDER BY (src_ip, window_start, ja4, host) + ); + +-- Matérialiser la projection sur les données existantes. +ALTER TABLE ja4_processing.agg_host_ip_ja4_1h + MATERIALIZE PROJECTION proj_by_ip;