20
static int check_for_new_mountpoints_every = 15;
21
static int cleanup_mount_points = 1;
22
23
+// ----------------------------------------------------------------------------
24
+// ZFS dataset deduplication
25
+//
26
+// ZFS datasets without quotas report the same capacity as their parent pool,
27
+// causing duplicate metrics and alert floods. This heuristic excludes datasets
28
+// that mirror pool capacity while keeping datasets with quotas.
29
+//
30
+// To avoid calling statvfs() on every dataset on every collection cycle,
31
+// we cache both pool capacities and dataset exclusion decisions.
32
+// Cache entries are refreshed every ZFS_DATASET_RECHECK_SECONDS.
33
+
34
+#define ZFS_DATASET_RECHECK_SECONDS 300 // recheck every 5 minutes
35
+
36
+// Combined cache for ZFS pools and datasets
37
+// Key: mount_source (e.g., "tank" for pool, "tank/home" for dataset)
38
+struct zfs_cache_entry {
39
+ time_t last_checked; // when this entry was last checked
40
+ bool is_pool; // true = pool, false = dataset
41
+ uint64_t pool_capacity; // only for pools: capacity in bytes
42
+ bool excluded; // only for datasets: true if excluded from monitoring
43
+};
44
+
45
+static DICTIONARY *zfs_cache = NULL;
46
+static SIMPLE_PATTERN *excluded_zfs_datasets_pattern = NULL;
47
+static int zfs_datasets_heuristic = CONFIG_BOOLEAN_YES;
48
+
49
static inline void mountinfo_reload(int force) {
50
static time_t last_loaded = 0;
51
time_t now = now_realtime_sec();
139
140
// a copy of basic mountinfo fields
141
struct basic_mountinfo {
116
- char *persistent_id;
117
- char *root;
142
+ char *persistent_id;
143
+ char *root;
144
char *mount_point_stat_path;
119
- char *mount_point;
120
- char *filesystem;
145
+ char *mount_point;
146
+ char *mount_source;
147
+ char *filesystem;
148
149
struct basic_mountinfo *next;
150
};
161
bmi->root = strdupz(mi->root);
162
bmi->mount_point_stat_path = strdupz(mi->mount_point_stat_path);
163
bmi->mount_point = strdupz(mi->mount_point);
164
+ bmi->mount_source = mi->mount_source ? strdupz(mi->mount_source) : NULL;
165
bmi->filesystem = strdupz(mi->filesystem);
166
}
167
186
freez(bmi->root);
187
freez(bmi->mount_point_stat_path);
188
freez(bmi->mount_point);
189
+ freez(bmi->mount_source);
190
freez(bmi->filesystem);
191
192
freez(bmi);
328
m->collected++;
329
}
330
331
+// ----------------------------------------------------------------------------
332
+// ZFS helper functions
333
+
334
+// Extract pool name from ZFS mount_source
335
+// "tank" → "tank", "tank/data/set" → "tank"
336
+static const char *extract_zfs_pool_name(const char *mount_source, char *buf, size_t buf_size) {
337
+ if (!mount_source || !mount_source[0] || buf_size == 0)
338
+ return NULL;
339
+
340
+ const char *slash = strchr(mount_source, '/');
341
+ if (slash) {
342
+ size_t len = slash - mount_source;
343
+ if (len >= buf_size)
344
+ len = buf_size - 1;
345
+ memcpy(buf, mount_source, len);
346
+ buf[len] = '\0';
347
+ } else {
348
+ strncpyz(buf, mount_source, buf_size - 1);
349
+ }
350
+
351
+ return buf;
352
+}
353
+
354
+static inline bool is_zfs_filesystem(struct mountinfo *mi) {
355
+ return mi && mi->filesystem && strcmp(mi->filesystem, "zfs") == 0;
356
+}
357
+
358
// Check if a ZFS filesystem entry is a dataset (not a pool)
359
+// Dataset = has '/' in mount_source (e.g., "tank/home")
360
+// Pool = no '/' in mount_source (e.g., "tank")
361
static inline bool is_zfs_dataset(struct mountinfo *mi) {
304
- if(!mi || !mi->filesystem || !mi->mount_source || !mi->mount_source[0])
305
- return false;
362
+ return is_zfs_filesystem(mi) &&
363
+ mi->mount_source &&
364
+ mi->mount_source[0] &&
365
+ strchr(mi->mount_source, '/') != NULL;
366
+}
367
307
- if(strcmp(mi->filesystem, "zfs") != 0)
368
+// Cached LXC detection result (checked once at first call, can't change at runtime)
369
+static bool zfs_inside_lxc_container = false;
370
+
371
+// Collect ZFS pool capacities for the heuristic
372
+// Called on every collection cycle but only updates pools (quick operation)
373
+static void zfs_collect_pool_capacities(void) {
374
+ // LXC detection - only once (can't change at runtime)
375
+ static bool lxc_checked = false;
376
+ if (!lxc_checked) {
377
+ zfs_inside_lxc_container = is_lxcfs_proc_mounted();
378
+ lxc_checked = true;
379
+ }
380
+
381
+ if (!zfs_datasets_heuristic || !zfs_cache)
382
+ return;
383
+
384
+ time_t now = now_realtime_sec();
385
+
386
+ for (struct mountinfo *mi = disk_mountinfo_root; mi; mi = mi->next) {
387
+ if (!is_zfs_filesystem(mi))
388
+ continue;
389
+
390
+ if (!mi->mount_source || !mi->mount_source[0])
391
+ continue;
392
+
393
+ // Only process pool mounts (no '/' in mount_source), not datasets
394
+ if (strchr(mi->mount_source, '/'))
395
+ continue;
396
+
397
+ // Check if this pool entry needs refresh
398
+ const DICTIONARY_ITEM *existing = dictionary_get_and_acquire_item(zfs_cache, mi->mount_source);
399
+ if (existing) {
400
+ struct zfs_cache_entry *entry = dictionary_acquired_item_value(existing);
401
+ if (entry->is_pool && now - entry->last_checked < ZFS_DATASET_RECHECK_SECONDS) {
402
+ dictionary_acquired_item_release(zfs_cache, existing);
403
+ continue; // still fresh
404
+ }
405
+ dictionary_acquired_item_release(zfs_cache, existing);
406
+ }
407
+
408
+ // Get capacity from the pool mount
409
+ struct statvfs buff;
410
+ if (statvfs(mi->mount_point_stat_path, &buff) != 0)
411
+ continue;
412
+
413
+ unsigned long bsize = buff.f_frsize ? buff.f_frsize : buff.f_bsize;
414
+ uint64_t capacity = (uint64_t)buff.f_blocks * bsize;
415
+
416
+ struct zfs_cache_entry new_entry = {
417
+ .last_checked = now,
418
+ .is_pool = true,
419
+ .pool_capacity = capacity,
420
+ .excluded = false
421
+ };
422
+ dictionary_set(zfs_cache, mi->mount_source, &new_entry, sizeof(new_entry));
423
+ }
424
+}
425
+
426
+// Check if a ZFS dataset has a cached exclusion decision that's still valid
427
+// Returns: true if cached as excluded (skip statvfs), false otherwise
428
+static bool zfs_dataset_cached_excluded(const char *filesystem, const char *mount_point, const char *mount_source) {
429
+ // Quick checks that don't need cache
430
+ if (!filesystem || strcmp(filesystem, "zfs") != 0)
431
return false;
309
-
310
- // For ZFS, the mount_source contains the dataset name (e.g., "tank" or "tank/install")
311
- // Pools have no slash, datasets have at least one slash
312
- return strchr(mi->mount_source, '/') != NULL;
432
+ if (!zfs_datasets_heuristic || !zfs_cache)
433
+ return false;
434
+ if (zfs_inside_lxc_container)
435
+ return false;
436
+ if (mount_point && strcmp(mount_point, "/") == 0)
437
+ return false;
438
+ if (!mount_source || !mount_source[0] || !strchr(mount_source, '/'))
439
+ return false; // not a dataset
440
+
441
+ const DICTIONARY_ITEM *item = dictionary_get_and_acquire_item(zfs_cache, mount_source);
442
+ if (!item)
443
+ return false; // not in cache
444
+
445
+ struct zfs_cache_entry *entry = dictionary_acquired_item_value(item);
446
+
447
+ // Only use cache for datasets (not pools), and only if still fresh
448
+ time_t now = now_realtime_sec();
449
+ bool cached_excluded = !entry->is_pool &&
450
+ entry->excluded &&
451
+ (now - entry->last_checked < ZFS_DATASET_RECHECK_SECONDS);
452
+
453
+ dictionary_acquired_item_release(zfs_cache, item);
454
+
455
+ return cached_excluded;
456
}
457
315
-static inline void do_disk_space_stats(struct mountinfo *mi, int update_every) {
316
- // Skip ZFS datasets, only monitor ZFS pools
317
- // This prevents alert floods when a pool fills up
318
- if (is_zfs_dataset(mi)) {
319
- return;
458
+// Determine if a ZFS mount should be excluded and cache the decision
459
+// Returns: true if should exclude, false if should keep
460
+static bool should_exclude_zfs(const char *filesystem, const char *mount_point, const char *mount_source, struct statvfs *buff) {
461
+ // 1. Not ZFS → keep
462
+ if (!filesystem || strcmp(filesystem, "zfs") != 0)
463
+ return false;
464
+
465
+ // 2. Root mount → always keep
466
+ if (mount_point && strcmp(mount_point, "/") == 0)
467
+ return false;
468
+
469
+ // 3. Inside LXC container → keep all (container sees virtualized mounts)
470
+ if (zfs_inside_lxc_container)
471
+ return false;
472
+
473
+ // 4. Not a dataset (it's a pool) → keep
474
+ // Dataset = has '/' in mount_source, Pool = no '/'
475
+ if (!mount_source || !mount_source[0] || !strchr(mount_source, '/'))
476
+ return false;
477
+
478
+ // 5. Heuristic disabled → use pattern matching (default "!*" excludes nothing)
479
+ if (!zfs_datasets_heuristic)
480
+ return excluded_zfs_datasets_pattern && simple_pattern_matches(excluded_zfs_datasets_pattern, mount_point);
481
+
482
+ if (!zfs_cache)
483
+ return false;
484
+
485
+ // 6. Heuristic enabled - use capacity logic
486
+ char pool_name_buf[256];
487
+ const char *pool_name = extract_zfs_pool_name(mount_source, pool_name_buf, sizeof(pool_name_buf));
488
+ if (!pool_name || !pool_name[0])
489
+ return false; // can't determine pool → keep
490
+
491
+ // Get pool info from cache
492
+ const DICTIONARY_ITEM *pool_item = dictionary_get_and_acquire_item(zfs_cache, pool_name);
493
+ if (!pool_item)
494
+ return false; // unknown pool → keep
495
+
496
+ struct zfs_cache_entry *pool_entry = dictionary_acquired_item_value(pool_item);
497
+
498
+ // 7. Verify it's a pool entry with valid capacity
499
+ if (!pool_entry->is_pool || !pool_entry->pool_capacity) {
500
+ dictionary_acquired_item_release(zfs_cache, pool_item);
501
+ return false;
502
}
503
504
+ // 8. Check if pool cache entry is still fresh (pool may have been unmounted)
505
+ // Use 2x interval: if entry missed one refresh cycle, pool is likely unmounted
506
+ time_t now = now_realtime_sec();
507
+ if (now - pool_entry->last_checked >= ZFS_DATASET_RECHECK_SECONDS * 2) {
508
+ dictionary_acquired_item_release(zfs_cache, pool_item);
509
+ return false;
510
+ }
511
+
512
+ uint64_t pool_capacity = pool_entry->pool_capacity;
513
+ dictionary_acquired_item_release(zfs_cache, pool_item);
514
+
515
+ // 9. Calculate this dataset's capacity
516
+ unsigned long bsize = buff->f_frsize ? buff->f_frsize : buff->f_bsize;
517
+ uint64_t dataset_capacity = (uint64_t)buff->f_blocks * bsize;
518
+
519
+ // 10. Determine exclusion: dataset capacity >= pool capacity → no quota → exclude
520
+ // Datasets without quotas report same capacity as pool.
521
+ // Datasets with quotas report smaller capacity (capped by quota).
522
+ bool excluded = (dataset_capacity >= pool_capacity);
523
+
524
+ // 11. Cache the decision for this dataset
525
+ struct zfs_cache_entry dataset_entry = {
526
+ .last_checked = now,
527
+ .is_pool = false,
528
+ .pool_capacity = 0,
529
+ .excluded = excluded
530
+ };
531
+ dictionary_set(zfs_cache, mount_source, &dataset_entry, sizeof(dataset_entry));
532
+
533
+ return excluded;
534
+}
535
+
536
+static inline void do_disk_space_stats(struct mountinfo *mi, int update_every) {
537
const char *disk = mi->persistent_id;
538
539
static SIMPLE_PATTERN *excluded_mountpoints = NULL;
570
SIMPLE_PATTERN_EXACT,
571
true);
572
573
+ // ZFS dataset exclusion pattern (used when heuristic is disabled)
574
+ // Always create so the option appears in config for users to customize
575
+ excluded_zfs_datasets_pattern = simple_pattern_create(
576
+ inicfg_get(&netdata_config, CONFIG_SECTION_DISKSPACE, "exclude zfs datasets on paths", "!*"),
577
+ NULL,
578
+ SIMPLE_PATTERN_EXACT,
579
+ true);
580
+
581
dict_mountpoints = dictionary_create_advanced(DICT_OPTION_FIXED_SIZE, &dictionary_stats_category_collectors, sizeof(struct mount_point_metadata));
582
dictionary_register_delete_callback(dict_mountpoints, mountpoint_delete_cb, NULL);
583
}
697
goto cleanup;
698
}
699
700
+ // Check if this ZFS dataset has a cached exclusion decision (skip statvfs entirely)
701
+ if (zfs_dataset_cached_excluded(mi->filesystem, mi->mount_point, mi->mount_source))
702
+ goto cleanup;
703
+
704
usec_t start_time = now_monotonic_high_precision_usec();
705
struct statvfs buff_statvfs;
706
720
if ((now_monotonic_high_precision_usec() - start_time) > slow_timeout)
721
m->slow = true;
722
723
+ // Check if this ZFS mount should be excluded (capacity-based heuristic)
724
+ if (should_exclude_zfs(mi->filesystem, mi->mount_point, mi->mount_source, &buff_statvfs))
725
+ goto cleanup;
726
+
727
m->shown_error = false;
728
729
struct basic_mountinfo bmi;
760
}
761
m->shown_error = false;
762
763
+ // Check if this ZFS mount should be excluded (same logic as fast path)
764
+ if (should_exclude_zfs(mi->filesystem, mi->mount_point, mi->mount_source, &buff_statvfs))
765
+ goto cleanup;
766
+
767
calculate_values_and_show_charts(mi, m, &buff_statvfs, update_every);
768
769
cleanup:
870
dictionary_destroy(dict_mountpoints);
871
dict_mountpoints = NULL;
872
873
+ // Free ZFS deduplication resources
874
+ dictionary_destroy(zfs_cache);
875
+ zfs_cache = NULL;
876
+
877
+ simple_pattern_free(excluded_zfs_datasets_pattern);
878
+ excluded_zfs_datasets_pattern = NULL;
879
+
880
rrd_collector_finished();
881
worker_unregister();
882
1126
if(check_for_new_mountpoints_every < update_every)
1127
check_for_new_mountpoints_every = update_every;
1128
1129
+ // ZFS dataset deduplication configuration
1130
+ zfs_datasets_heuristic = inicfg_get_boolean(
1131
+ &netdata_config,
1132
+ CONFIG_SECTION_DISKSPACE,
1133
+ "zfs datasets heuristic",
1134
+ CONFIG_BOOLEAN_YES);
1135
+
1136
+ if (zfs_datasets_heuristic) {
1137
+ // Heuristic mode: create cache for pool capacities and dataset exclusion decisions
1138
+ zfs_cache = dictionary_create_advanced(
1139
+ DICT_OPTION_FIXED_SIZE,
1140
+ &dictionary_stats_category_collectors,
1141
+ sizeof(struct zfs_cache_entry));
1142
+ }
1143
+
1144
netdata_mutex_init(&slow_mountinfo_mutex);
1145
1146
struct slow_worker_data slow_worker_data = { .update_every = update_every };
1172
free_basic_mountinfo_list(slow_mountinfo_tmp_root);
1173
slow_mountinfo_tmp_root = NULL;
1174
1175
+ // Collect ZFS pool capacities for the heuristic (Pass 1)
1176
+ zfs_collect_pool_capacities();
1177
+
1178
struct mountinfo *mi;
1179
for(mi = disk_mountinfo_root; mi; mi = mi->next) {
1180
if(unlikely(mi->flags & (MOUNTINFO_IS_DUMMY | MOUNTINFO_IS_BIND)))