| 1 | <!-- markdownlint-disable-file MD013 MD043 --> |
| 2 | |
| 3 | # `netflow-plugin` |
| 4 | |
| 5 | Rust NetFlow/IPFIX/sFlow ingestion and query plugin. |
| 6 | |
| 7 | It stores flow entries in journal tiers under the Netdata cache directory and exposes |
| 8 | `flows:netflow`. |
| 9 | |
| 10 | ## Offline Function test mode |
| 11 | |
| 12 | Fixture harnesses can execute the Function query path directly against an existing |
| 13 | NetFlow backend directory: |
| 14 | |
| 15 | ```sh |
| 16 | netflow-plugin --test flows:netflow --dir <flows-dir> [--timeout <seconds>] [--no-persist] < payload.json |
| 17 | ``` |
| 18 | |
| 19 | Requirements: |
| 20 | |
| 21 | - `<flows-dir>` is the NetFlow backend root containing the `raw`, `1m`, `5m`, and `1h` tier directories. |
| 22 | - stdin is the JSON Function request body (non-empty, maximum 16 MiB). |
| 23 | - `--request` is not supported and fails with usage output. |
| 24 | - `--timeout <seconds>` controls the offline Function execution timeout. It |
| 25 | defaults to `30`; use `--timeout 0` to map to a very large finite timeout for |
| 26 | long-running fixture comparisons. |
| 27 | - stdout contains only the raw JSON Function response. |
| 28 | - errors are written to stderr and return non-zero. |
| 29 | |
| 30 | Use `--no-persist` for shared fixture datasets. It prevents the test run from writing |
| 31 | facet state or sidecar files under `<flows-dir>` while keeping facet data in memory for |
| 32 | the Function response. Without `--no-persist`, the plugin may refresh facet state under |
| 33 | the backend directory, matching normal runtime behavior. |
| 34 | |
| 35 | Function output includes volatile fields such as collection timestamps and runtime |
| 36 | statistics. Test harnesses should normalize those fields before comparing fixture |
| 37 | outputs. |
| 38 | |
| 39 | ## Configuration |
| 40 | |
| 41 | When running under Netdata, config is loaded from `netflow.yaml` in: |
| 42 | |
| 43 | - `${NETDATA_USER_CONFIG_DIR}/netflow.yaml` (preferred) |
| 44 | - `${NETDATA_STOCK_CONFIG_DIR}/netflow.yaml` (fallback) |
| 45 | |
| 46 | Top-level toggle: |
| 47 | |
| 48 | - `enabled: true|false` controls the netflow plugin itself. |
| 49 | |
| 50 | If `journal.journal_dir` is relative (default: `flows`), it is resolved against |
| 51 | `NETDATA_CACHE_DIR`. With the standard cache directory this becomes: |
| 52 | |
| 53 | - `/var/cache/netdata/flows/raw` |
| 54 | - `/var/cache/netdata/flows/1m` |
| 55 | - `/var/cache/netdata/flows/5m` |
| 56 | - `/var/cache/netdata/flows/1h` |
| 57 | |
| 58 | If `enrichment.geoip` does not define explicit MMDB paths, the plugin auto-detects |
| 59 | packaged databases in this order: |
| 60 | |
| 61 | - `${NETDATA_CACHE_DIR}/topology-ip-intel` |
| 62 | - `${NETDATA_STOCK_DATA_DIR}/topology-ip-intel` |
| 63 | |
| 64 | `netdata-plugin-netflow` now ships a stock MMDB seed set under |
| 65 | `${NETDATA_STOCK_DATA_DIR}/topology-ip-intel`. Freshly downloaded data written by |
| 66 | `topology-ip-intel-downloader` stays in `${NETDATA_CACHE_DIR}/topology-ip-intel` and |
| 67 | overrides the stock copy automatically. |
| 68 | |
| 69 | Important: |
| 70 | |
| 71 | - packaged installs ship the stock MMDB payload |
| 72 | - source installs from a Git checkout do not include the generated stock MMDBs by default |
| 73 | - local/source installs should run `topology-ip-intel-downloader` if they want a local cache copy |
| 74 | - packaged 32-bit installs still ship the stock MMDB payload, but do not include `topology-ip-intel-downloader` |
| 75 | |
| 76 | ### `protocols.decapsulation_mode` |
| 77 | |
| 78 | Controls packet decapsulation for datalink payload parsing: |
| 79 | |
| 80 | - `none` (default): keep outer header view. |
| 81 | - `srv6`: enable SRv6 decapsulation for supported payloads. |
| 82 | - `vxlan`: enable VXLAN decapsulation for supported payloads. |
| 83 | |
| 84 | ### `protocols.timestamp_source` |
| 85 | |
| 86 | Controls which timestamp is written as `_SOURCE_REALTIME_TIMESTAMP` for decoded flows: |
| 87 | |
| 88 | - `input` (default): packet receive time at ingestion. |
| 89 | - `netflow_packet`: NetFlow/IPFIX packet export timestamp. |
| 90 | - `netflow_first_switched`: first-switched timestamp from flow fields when available. |
| 91 | |
| 92 | ### `enrichment` (Akvorado-style static metadata and sampling) |
| 93 | |
| 94 | Optional enrichment is applied at ingestion time. When enabled, it follows Akvorado core behavior |
| 95 | for static metadata and sampling defaults/overrides: |
| 96 | |
| 97 | - metadata lookup by exporter prefix + interface index |
| 98 | - sampling override/default by exporter prefix |
| 99 | - exporter/interface classifier rules (`exporter_classifiers`, `interface_classifiers`) |
| 100 | - classifier cache TTL (`classifier_cache_duration`, default `5m`, minimum `1s`) |
| 101 | - routing static lookup for AS/mask/next-hop/path/community fields (`routing_static`) |
| 102 | - routing dynamic lookup from BMP updates (`routing_dynamic.bmp`) |
| 103 | - BMP decode-error tolerance is configurable (`routing_dynamic.bmp.max_consecutive_decode_errors`) |
| 104 | - network attribute lookup by source/destination IP prefix (`networks`) |
| 105 | - GeoIP lookup from local MMDB files (`geoip`) |
| 106 | - periodic remote network source refresh (`network_sources`) |
| 107 | - network attribute merge order: GeoIP base, then remote `network_sources`, then static `networks` |
| 108 | - static `networks` supernet/subnet inheritance (more specific prefixes override non-empty fields) |
| 109 | - AS and net provider order controls (`asn_providers`, `net_providers`) |
| 110 | - flows are dropped when metadata/sampling requirements are not met |
| 111 | - when both static and dynamic routes match, dynamic routes are preferred |
| 112 | |
| 113 | Example: |
| 114 | |
| 115 | ```yaml |
| 116 | enrichment: |
| 117 | classifier_cache_duration: 5m |
| 118 | default_sampling_rate: |
| 119 | 192.0.2.0/24: 1000 |
| 120 | override_sampling_rate: |
| 121 | 192.0.2.128/25: 4000 |
| 122 | metadata_static: |
| 123 | exporters: |
| 124 | 192.0.2.0/24: |
| 125 | name: edge-router |
| 126 | region: eu |
| 127 | role: peering |
| 128 | tenant: tenant-a |
| 129 | site: par |
| 130 | group: blue |
| 131 | default: |
| 132 | name: Default0 |
| 133 | description: Default interface |
| 134 | speed: 1000 |
| 135 | if_indexes: |
| 136 | 10: |
| 137 | name: Gi10 |
| 138 | description: 10th interface |
| 139 | speed: 1000 |
| 140 | provider: transit-a |
| 141 | connectivity: transit |
| 142 | boundary: external |
| 143 | geoip: |
| 144 | asn_database: |
| 145 | - /usr/share/GeoIP/GeoLite2-ASN.mmdb |
| 146 | geo_database: |
| 147 | - /usr/share/GeoIP/GeoLite2-City.mmdb |
| 148 | optional: true |
| 149 | networks: |
| 150 | 198.51.100.0/24: |
| 151 | name: customer-a |
| 152 | role: customer |
| 153 | site: par1 |
| 154 | region: eu-west |
| 155 | country: FR |
| 156 | state: Ile-de-France |
| 157 | city: Paris |
| 158 | tenant: tenant-a |
| 159 | asn: 64500 |
| 160 | 203.0.113.0/24: transit-a |
| 161 | network_sources: |
| 162 | amazon: |
| 163 | url: "https://ip-ranges.amazonaws.com/ip-ranges.json" |
| 164 | method: GET |
| 165 | tls: |
| 166 | enable: true |
| 167 | verify: true |
| 168 | ca_file: "" |
| 169 | cert_file: "" |
| 170 | key_file: "" |
| 171 | interval: 10m |
| 172 | timeout: 30s |
| 173 | transform: | |
| 174 | (.prefixes + .ipv6_prefixes)[] | |
| 175 | { prefix: (.ip_prefix // .ipv6_prefix), tenant: "amazon", region: .region, role: .service|ascii_downcase } |
| 176 | headers: |
| 177 | X-Example: "value" |
| 178 | asn_providers: [flow, routing, geoip] |
| 179 | net_providers: [flow, routing] |
| 180 | routing_static: |
| 181 | prefixes: |
| 182 | 198.51.100.0/24: |
| 183 | asn: 64600 |
| 184 | as_path: [64550, 64600] |
| 185 | communities: [123456, 654321] |
| 186 | large_communities: |
| 187 | - asn: 64600 |
| 188 | local_data1: 7 |
| 189 | local_data2: 8 |
| 190 | next_hop: 203.0.113.9 |
| 191 | routing_dynamic: |
| 192 | bmp: |
| 193 | enabled: true |
| 194 | listen: "0.0.0.0:10179" |
| 195 | receive_buffer: 0 # bytes; 0 keeps kernel default |
| 196 | max_consecutive_decode_errors: 8 |
| 197 | rds: ["0", "65000:100", "192.0.2.1:42"] |
| 198 | collect_asns: true |
| 199 | collect_as_paths: true |
| 200 | collect_communities: true |
| 201 | keep: 5m |
| 202 | bioris: |
| 203 | enabled: false |
| 204 | ris_instances: [] |
| 205 | timeout: 200ms |
| 206 | refresh: 30m |
| 207 | refresh_timeout: 10s |
| 208 | ``` |
| 209 | |
| 210 | `routing_dynamic.bioris` is implemented as stream-based route ingestion: |
| 211 | |
| 212 | - the plugin periodically discovers routers with `GetRouters` |
| 213 | - it consumes `DumpRIB` streams per router/AFI for baseline reconciliation |
| 214 | - it keeps `ObserveRIB` streams per router/AFI for incremental updates between refreshes |
| 215 | - enrichment keeps lookup local/in-memory (no per-flow remote RPC on the ingestion hot path) |
| 216 | |
| 217 | `network_sources.*.transform` accepts jq expressions (compiled/executed via `jaq`) and |
| 218 | should emit objects with fields compatible with: |
| 219 | |
| 220 | - `prefix` (required) |
| 221 | - `name`, `role`, `site`, `region`, `country`, `state`, `city`, `tenant`, `asn`, `asn_name` (optional) |
| 222 | |
| 223 | `network_sources.*.tls` follows Akvorado-style source TLS controls: |
| 224 | |
| 225 | - `enable` toggles TLS settings for this source. |
| 226 | - `verify` must remain `true`; disabling TLS certificate verification is not supported. |
| 227 | - `skip_verify` is accepted only for compatibility with earlier drafts and must remain `false`. |
| 228 | - `ca_file` sets a custom CA bundle. |
| 229 | - `cert_file` and `key_file` set optional client identity (if `key_file` is empty, `cert_file` is reused). |
| 230 | |
| 231 | Example: |
| 232 | |
| 233 | ```yaml |
| 234 | enabled: true |
| 235 | |
| 236 | listener: |
| 237 | listen: "0.0.0.0:2055" |
| 238 | |
| 239 | protocols: |
| 240 | v5: true |
| 241 | v7: true |
| 242 | v9: true |
| 243 | ipfix: true |
| 244 | sflow: true |
| 245 | decapsulation_mode: srv6 |
| 246 | timestamp_source: input |
| 247 | |
| 248 | journal: |
| 249 | journal_dir: flows |
| 250 | tiers: |
| 251 | raw: |
| 252 | size_of_journal_files: 200GB |
| 253 | duration_of_journal_files: 24h |
| 254 | minute_1: |
| 255 | size_of_journal_files: 40GB |
| 256 | duration_of_journal_files: 14d |
| 257 | minute_5: |
| 258 | size_of_journal_files: 30GB |
| 259 | duration_of_journal_files: 30d |
| 260 | hour_1: |
| 261 | size_of_journal_files: 20GB |
| 262 | duration_of_journal_files: 365d |
| 263 | query_max_groups: 50000 |
| 264 | ``` |
| 265 | |
| 266 | Standalone CLI runs still accept the legacy uniform retention flags |
| 267 | `--netflow-retention-size-of-journal-files` and |
| 268 | `--netflow-retention-duration-of-journal-files`. They apply the same value to |
| 269 | all tiers and exist only for standalone/CLI compatibility; YAML configuration is |
| 270 | per-tier. |
| 271 | |
| 272 | `query_max_groups` caps the number of distinct group keys a single |
| 273 | aggregation query may build before extra groups are folded into a synthetic |
| 274 | `__overflow__` bucket. The response carries a warning when this happens. The |
| 275 | limit protects the query worker from accidentally wide group-by combinations |
| 276 | exhausting memory. |
| 277 | |
| 278 | Journal rotation size is not user-configured. The plugin derives it per tier: |
| 279 | |
| 280 | - if `size_of_journal_files` is set, rotation size is `clamp(size / 20, 5MB, 200MB)` |
| 281 | - `size_of_journal_files` must be at least `100MB` |
| 282 | - if `size_of_journal_files` is omitted or explicitly set to `null`, the plugin |
| 283 | uses a fixed internal rotation size of `100MB` |
| 284 | - the internal time-based rotation cadence remains `1h` |
| 285 | |
| 286 | The plugin also exposes internal memory charts to help diagnose resident growth |
| 287 | in production: |
| 288 | |
| 289 | - `netflow.memory_resident_bytes` |
| 290 | - `rss`, `hwm` |
| 291 | - `rss_anon`, `rss_file`, `rss_shmem`, `anon_huge_pages` |
| 292 | - `netflow.memory_resident_mapping_bytes` |
| 293 | - resident heap bytes |
| 294 | - anonymous non-heap mappings |
| 295 | - raw journal mmap bytes |
| 296 | - `1m` journal mmap bytes |
| 297 | - `5m` journal mmap bytes |
| 298 | - `1h` journal mmap bytes |
| 299 | - GeoIP ASN MMDB resident bytes |
| 300 | - GeoIP geo/country MMDB resident bytes |
| 301 | - other file-backed mappings |
| 302 | - shmem mappings |
| 303 | - `netflow.memory_allocator_bytes` |
| 304 | - `heap_in_use`, `heap_free`, `heap_arena` |
| 305 | - `mmap_in_use`, `releasable` |
| 306 | - `netflow.memory_accounted_bytes` |
| 307 | - facet runtime archived/active/contribution/published/path buckets |
| 308 | - materialized tier indexes |
| 309 | - open tier rows |
| 310 | - GeoIP ASN MMDB resident bytes |
| 311 | - GeoIP geo/country MMDB resident bytes |
| 312 | - unaccounted process RSS remainder |
| 313 | - `netflow.memory_tier_index_bytes` |
| 314 | - tier row storage |
| 315 | - tier field dictionaries |
| 316 | - tier lookup tables |
| 317 | - tier schema/index metadata |
| 318 | - `netflow.decoder_scopes` |
| 319 | - NetFlow v9 parser scopes |
| 320 | - IPFIX parser scopes |
| 321 | - legacy parser scopes |
| 322 | - persisted decoder namespaces |
| 323 | - hydrated namespace-source mappings |
| 324 | |
| 325 | These charts are intended for debugging memory explosions under high-cardinality |
| 326 | traffic, not for billing or hard enforcement decisions. |
| 327 | |
| 328 | `journal.tiers` configures retention independently for: |
| 329 | |
| 330 | - `raw` |
| 331 | - `minute_1` (aliases: `1m`, `minute-1`, `minute1`) |
| 332 | - `minute_5` (aliases: `5m`, `minute-5`, `minute5`) |
| 333 | - `hour_1` (aliases: `1h`, `hour-1`, `hour1`) |
| 334 | |
| 335 | If a tier is omitted, it uses the built-in tier default (`10GB / 7d`). There |
| 336 | are no top-level journal retention knobs; set retention on each tier you want |
| 337 | to tune. |
| 338 | |
| 339 | To make a tier time-only, set `size_of_journal_files: null`. |
| 340 | To make a tier size-only, set `duration_of_journal_files: null`. |
| 341 | |
| 342 | ## Performance benchmarking |
| 343 | |
| 344 | The plugin ships two complementary benchmarks: |
| 345 | |
| 346 | - `cargo test -p netflow-plugin --manifest-path src/crates/Cargo.toml --release ingest::bench_tests::bench_ingestion_protocol_matrix -- --ignored --nocapture` |
| 347 | unpaced full UDP→journal max throughput per protocol, plus decode-only and |
| 348 | post-decode phases |
| 349 | - `cargo test -p netflow-plugin --manifest-path src/crates/Cargo.toml --release ingest::resource_bench_tests::bench_resource_envelope_child -- --ignored --nocapture` |
| 350 | paced post-decode resource envelope at a configurable rate, controlled via |
| 351 | env vars: `NETFLOW_RESOURCE_BENCH_PROTOCOL`, `NETFLOW_RESOURCE_BENCH_PROFILE`, |
| 352 | `NETFLOW_RESOURCE_BENCH_LAYER`, `NETFLOW_RESOURCE_BENCH_FLOWS_PER_SEC`, |
| 353 | `NETFLOW_RESOURCE_BENCH_WARMUP_SECS`, `NETFLOW_RESOURCE_BENCH_MEASURE_SECS` |
| 354 | |
| 355 | The resource-envelope benchmark scope: |
| 356 | |
| 357 | - pre-decoded flow records pushed into the ingest pipeline (UDP receive and |
| 358 | protocol decode are excluded; measure decode separately with the protocol |
| 359 | matrix benchmark) |
| 360 | - full pipeline active: raw journal + 1-minute + 5-minute + 1-hour tier |
| 361 | accumulation, real disk-backed journals |
| 362 | - enrichment is NOT loaded (no GeoIP MMDB, no static metadata, no classifiers, |
| 363 | no static networks); cardinality fields are pre-populated by the harness |
| 364 | - reports achieved flows/s, CPU% of one core, peak/final RSS, real disk read |
| 365 | and write bytes/s from `/proc/self/io` |
| 366 | |
| 367 | `cpu_percent_of_one_core` is the sum of user+system ticks across all threads |
| 368 | of the test process during the measurement window, divided by wall time, as a |
| 369 | percent of one core. 100% means one core's worth of CPU was consumed; values |
| 370 | above 100% are normal for multi-threaded saturation. |
| 371 | |
| 372 | Reference measurements: |
| 373 | |
| 374 | - CPU: `12th Gen Intel(R) Core(TM) i9-12900K` |
| 375 | - storage: ext4 on `Seagate FireCuda 530` |
| 376 | - methodology: release mode, `5s` warmup, `15s` measurement window, |
| 377 | disk-backed journals, post-decode paced ingest, all-tiers-batched layer |
| 378 | |
| 379 | ### Paced post-decode resource envelope (3A) |
| 380 | |
| 381 | Per protocol, per cardinality, at 10 offered rates from 100 to 60 000 flows/s. |
| 382 | Cardinality is synthetic: low-cardinality cycles 256 unique records, high- |
| 383 | cardinality cycles 4 096 unique records. Real exporter data sits between the |
| 384 | two. |
| 385 | |
| 386 | Low cardinality, NetFlow v9: |
| 387 | |
| 388 | | offered | achieved | CPU | disk write | RAM peak | |
| 389 | |---:|---:|---:|---:|---:| |
| 390 | | 100 | 80 | 0.3% | 95 KiB/s | 13 MiB | |
| 391 | | 1 000 | 1 000 | 1.3% | 804 KiB/s | 23 MiB | |
| 392 | | 10 000 | 10 000 | 12.6% | 7.7 MiB/s | 75 MiB | |
| 393 | | 30 000 | 30 000 | 35.7% | 22.9 MiB/s | 83 MiB | |
| 394 | | 60 000 | 60 000 | 70.3% | 45.6 MiB/s | 98 MiB | |
| 395 | |
| 396 | Low cardinality, IPFIX: |
| 397 | |
| 398 | | offered | achieved | CPU | disk write | RAM peak | |
| 399 | |---:|---:|---:|---:|---:| |
| 400 | | 100 | 61 | 0.1% | 75 KiB/s | 13 MiB | |
| 401 | | 1 000 | 975 | 1.0% | 730 KiB/s | 24 MiB | |
| 402 | | 10 000 | 9 996 | 11.5% | 6.8 MiB/s | 61 MiB | |
| 403 | | 30 000 | 29 988 | 32.9% | 20.4 MiB/s | 83 MiB | |
| 404 | | 60 000 | 59 977 | 64.1% | 40.8 MiB/s | 78 MiB | |
| 405 | |
| 406 | Low cardinality, sFlow: |
| 407 | |
| 408 | | offered | achieved | CPU | disk write | RAM peak | |
| 409 | |---:|---:|---:|---:|---:| |
| 410 | | 100 | 99 | 0.2% | 107 KiB/s | 13 MiB | |
| 411 | | 1 000 | 985 | 1.5% | 847 KiB/s | 22 MiB | |
| 412 | | 10 000 | 9 989 | 16.9% | 8.4 MiB/s | 75 MiB | |
| 413 | | 30 000 | 29 967 | 46.2% | 25.2 MiB/s | 84 MiB | |
| 414 | | 60 000 | 59 984 | 87.1% | 50.3 MiB/s | 80 MiB | |
| 415 | |
| 416 | High cardinality, NetFlow v9 (saturates around 30 000 flows/s): |
| 417 | |
| 418 | | offered | achieved | CPU | disk write | RAM peak | |
| 419 | |---:|---:|---:|---:|---:| |
| 420 | | 100 | 80 | 0.5% | 409 KiB/s | 25 MiB | |
| 421 | | 1 000 | 1 000 | 4.3% | 2.0 MiB/s | 58 MiB | |
| 422 | | 10 000 | 10 000 | 36.8% | 7.2 MiB/s | 104 MiB | |
| 423 | | 30 000 | 29 331 | 98.0% | 24.9 MiB/s | 119 MiB | |
| 424 | | 60 000 | 26 475 | 98.8% | 30.3 MiB/s | 247 MiB | |
| 425 | |
| 426 | High cardinality, IPFIX (saturates around 30-40 000 flows/s): |
| 427 | |
| 428 | | offered | achieved | CPU | disk write | RAM peak | |
| 429 | |---:|---:|---:|---:|---:| |
| 430 | | 100 | 60 | 0.2% | 214 KiB/s | 22 MiB | |
| 431 | | 1 000 | 961 | 3.2% | 2.0 MiB/s | 61 MiB | |
| 432 | | 10 000 | 9 970 | 28.0% | 7.7 MiB/s | 121 MiB | |
| 433 | | 30 000 | 29 985 | 84.8% | 23.0 MiB/s | 121 MiB | |
| 434 | | 60 000 | 28 835 | 98.5% | 36.8 MiB/s | 193 MiB | |
| 435 | |
| 436 | High cardinality, sFlow (saturates around 30 000 flows/s): |
| 437 | |
| 438 | | offered | achieved | CPU | disk write | RAM peak | |
| 439 | |---:|---:|---:|---:|---:| |
| 440 | | 100 | 100 | 0.6% | 604 KiB/s | 29 MiB | |
| 441 | | 1 000 | 999 | 4.7% | 3.2 MiB/s | 77 MiB | |
| 442 | | 10 000 | 9 990 | 35.3% | 9.9 MiB/s | 113 MiB | |
| 443 | | 30 000 | 29 257 | 98.3% | 30.9 MiB/s | 129 MiB | |
| 444 | | 60 000 | 30 227 | 98.6% | 29.1 MiB/s | 122 MiB | |
| 445 | |
| 446 | ### Unpaced full UDP→journal protocol matrix (3B) |
| 447 | |
| 448 | Single-threaded peak throughput at native fixture cardinality: |
| 449 | |
| 450 | | protocol | full ingest (decode + journal) | decode only | post-decode only | |
| 451 | |---|---:|---:|---:| |
| 452 | | NetFlow v9 | 99 000 flows/s | 811 000 flows/s | 116 000 flows/s | |
| 453 | | IPFIX | 107 000 flows/s | 807 000 flows/s | 124 000 flows/s | |
| 454 | | sFlow | 88 000 flows/s | 2 392 000 flows/s | 99 000 flows/s | |
| 455 | |
| 456 | ### Interpretation |
| 457 | |
| 458 | - The post-decode ingest hot path is currently single-threaded. CPU pins at |
| 459 | ~98-99% of one core at saturation; it does not scale further with more cores. |
| 460 | - Low-cardinality saturation is above 60 000 flows/s for NetFlow v9 and IPFIX, |
| 461 | and around 70 000 flows/s for sFlow on this host. The matrix above does not |
| 462 | reach those ceilings on purpose; extrapolate from the CPU% column. |
| 463 | - High-cardinality saturation is around 30 000 flows/s post-decode for all |
| 464 | three protocols. Above the knee, achieved rate stays at the plateau while |
| 465 | offered rate grows. |
| 466 | - Adding decode (~10 µs/flow) on top of post-decode ingest brings the practical |
| 467 | full-path ceiling to roughly 22-25 000 flows/s at high cardinality on this |
| 468 | host, with the four-tier pipeline running and no enrichment. UDP socket |
| 469 | receive is not measured; at these flow rates packet rate (1-5k pps) is well |
| 470 | below typical socket limits. |
| 471 | - Disk reads stay near zero because the benchmark isolates the ingest path |
| 472 | from query workloads. The journals themselves are indexed and rewrite pages |
| 473 | during normal operation; this benchmark does not exercise that overhead at |
| 474 | steady state because it only runs for the warmup + measurement window. |
| 475 | - Higher cardinality raises steady memory and per-flow encoding cost, lowering |
| 476 | the throughput ceiling. |
| 477 | - These numbers are specific to this host. They do not include GeoIP/MMDB or |
| 478 | any other enrichment; loading enrichment adds per-lookup CPU cost on top. |
| 479 | |
| 480 | ## plugins.d protocol |
| 481 | |
| 482 | When stdout is not a TTY (normal `plugins.d` runtime), `netflow-plugin` emits |
| 483 | `PLUGIN_KEEPALIVE` periodically to avoid parser inactivity timeouts. |