@cryptotaxi247 / netdata-1 / commits / ff448d1e8

improve(go.d/smartctl): add configurable concurrent device scanning (#20569)

Ilya Mashchenko committed Jun 24, 2025 at 23:22 UTC ff448d1e8c137d5b7bb3d6172f22e3887edfb990
7 files changed +212 -10
src/go/plugin/go.d/collector/smartctl/collect.go
+66 -9
@@ -10,6 +10,7 @@ import (
10 "strings"
11 "time"
12
13 + "github.com/sourcegraph/conc/pool"
14 "github.com/tidwall/gjson"
15 )
16
@@ -39,13 +40,7 @@ func (c *Collector) collect() (map[string]int64, error) {
40 if c.forceDevicePoll || c.isTimeToPollDevices(now) {
41 mx := make(map[string]int64)
42
42 - // TODO: make it concurrent
43 - for _, d := range c.scannedDevices {
44 - if err := c.collectScannedDevice(mx, d); err != nil {
45 - c.Warning(err)
46 - continue
47 - }
48 - }
43 + c.collectDevices(mx)
44
45 c.forceDevicePoll = false
46 c.lastDevicePollTime = now
@@ -54,9 +49,62 @@ func (c *Collector) collect() (map[string]int64, error) {
49
50 return c.mx, nil
51 }
52 +func (c *Collector) collectDevices(mx map[string]int64) {
53 + if c.ConcurrentScans > 0 && len(c.scannedDevices) > 1 {
54 + if err := c.collectDevicesConcurrently(mx); err != nil {
55 + c.Warning(err)
56 + }
57 + return
58 + }
59 +
60 + for _, d := range c.scannedDevices {
61 + if err := c.collectScannedDevice(mx, d); err != nil {
62 + c.Warning(err)
63 + continue
64 + }
65 + }
66 +}
67 +
68 +type deviceInfoResult struct {
69 + scanDevice *scanDevice
70 + response *gjson.Result
71 + err error
72 +}
73 +
74 +func (c *Collector) collectDevicesConcurrently(mx map[string]int64) error {
75 + p := pool.New().WithMaxGoroutines(c.ConcurrentScans)
76 + resultsChan := make(chan deviceInfoResult, len(c.scannedDevices))
77 +
78 + for _, dev := range c.scannedDevices {
79 + dev := dev
80 + p.Go(func() {
81 + resp, err := c.exec.deviceInfo(dev.name, dev.typ, c.NoCheckPowerMode)
82 + resultsChan <- deviceInfoResult{
83 + scanDevice: dev,
84 + response: resp,
85 + err: err,
86 + }
87 + })
88 + }
89 +
90 + p.Wait()
91 + close(resultsChan)
92 +
93 + for r := range resultsChan {
94 + if err := c.processDeviceResult(mx, r); err != nil {
95 + c.Warning(err)
96 + continue
97 + }
98 + }
99 +
100 + return nil
101 +}
102 +
103 +func (c *Collector) processDeviceResult(mx map[string]int64, result deviceInfoResult) error {
104 + scanDev := result.scanDevice
105 + resp := result.response
106 + err := result.err
107
58 -func (c *Collector) collectScannedDevice(mx map[string]int64, scanDev *scanDevice) error {
59 - resp, err := c.exec.deviceInfo(scanDev.name, scanDev.typ, c.NoCheckPowerMode)
108 if err != nil {
109 if resp != nil && isDeviceOpenFailedNoSuchDevice(resp) && !scanDev.extra {
110 c.Infof("smartctl reported that device '%s' type '%s' no longer exists", scanDev.name, scanDev.typ)
@@ -86,6 +134,15 @@ func (c *Collector) collectScannedDevice(mx map[string]int64, scanDev *scanDevic
134 return nil
135 }
136
137 +func (c *Collector) collectScannedDevice(mx map[string]int64, scanDev *scanDevice) error {
138 + resp, err := c.exec.deviceInfo(scanDev.name, scanDev.typ, c.NoCheckPowerMode)
139 + return c.processDeviceResult(mx, deviceInfoResult{
140 + scanDevice: scanDev,
141 + response: resp,
142 + err: err,
143 + })
144 +}
145 +
146 func (c *Collector) collectSmartDevice(mx map[string]int64, dev *smartDevice) {
147 px := fmt.Sprintf("device_%s_type_%s_", dev.deviceName(), dev.deviceType())
148
src/go/plugin/go.d/collector/smartctl/collector.go
+2
@@ -36,6 +36,7 @@ func New() *Collector {
36 PollDevicesEvery: confopt.Duration(time.Minute * 5),
37 NoCheckPowerMode: "standby",
38 DeviceSelector: "*",
39 + ConcurrentScans: 0, // Default to sequential
40 },
41 charts: &module.Charts{},
42 forceScan: true,
@@ -53,6 +54,7 @@ type (
54 NoCheckPowerMode string `yaml:"no_check_power_mode,omitempty" json:"no_check_power_mode"`
55 DeviceSelector string `yaml:"device_selector,omitempty" json:"device_selector"`
56 ExtraDevices []ConfigExtraDevice `yaml:"extra_devices,omitempty" json:"extra_devices"`
57 + ConcurrentScans int `yaml:"concurrent_scans,omitempty" json:"concurrent_scans"`
58 }
59 ConfigExtraDevice struct {
60 Name string `yaml:"name" json:"name"`
src/go/plugin/go.d/collector/smartctl/collector_test.go
+119
@@ -288,6 +288,125 @@ func TestCollector_Collect(t *testing.T) {
288 "device_sdc_type_sat_temperature": 27,
289 },
290 },
291 + "success type sata devices concurrent": {
292 + prepareMock: prepareMockOkTypeSata,
293 + prepareConfig: func() Config {
294 + cfg := New().Config
295 + cfg.ConcurrentScans = 2
296 + return cfg
297 + },
298 + wantCharts: 68,
299 + wantMetrics: map[string]int64{
300 + "device_sda_type_sat_ata_smart_error_log_summary_count": 0,
301 + "device_sda_type_sat_attr_current_pending_sector_decoded": 0,
302 + "device_sda_type_sat_attr_current_pending_sector_normalized": 100,
303 + "device_sda_type_sat_attr_current_pending_sector_raw": 0,
304 + "device_sda_type_sat_attr_load_cycle_count_decoded": 360,
305 + "device_sda_type_sat_attr_load_cycle_count_normalized": 100,
306 + "device_sda_type_sat_attr_load_cycle_count_raw": 360,
307 + "device_sda_type_sat_attr_offline_uncorrectable_decoded": 0,
308 + "device_sda_type_sat_attr_offline_uncorrectable_normalized": 100,
309 + "device_sda_type_sat_attr_offline_uncorrectable_raw": 0,
310 + "device_sda_type_sat_attr_power-off_retract_count_decoded": 360,
311 + "device_sda_type_sat_attr_power-off_retract_count_normalized": 100,
312 + "device_sda_type_sat_attr_power-off_retract_count_raw": 360,
313 + "device_sda_type_sat_attr_power_cycle_count_decoded": 12,
314 + "device_sda_type_sat_attr_power_cycle_count_normalized": 100,
315 + "device_sda_type_sat_attr_power_cycle_count_raw": 12,
316 + "device_sda_type_sat_attr_power_on_hours_decoded": 8244,
317 + "device_sda_type_sat_attr_power_on_hours_normalized": 99,
318 + "device_sda_type_sat_attr_power_on_hours_raw": 8244,
319 + "device_sda_type_sat_attr_raw_read_error_rate_decoded": 0,
320 + "device_sda_type_sat_attr_raw_read_error_rate_normalized": 100,
321 + "device_sda_type_sat_attr_raw_read_error_rate_raw": 0,
322 + "device_sda_type_sat_attr_reallocated_event_count_decoded": 0,
323 + "device_sda_type_sat_attr_reallocated_event_count_normalized": 100,
324 + "device_sda_type_sat_attr_reallocated_event_count_raw": 0,
325 + "device_sda_type_sat_attr_reallocated_sector_ct_decoded": 0,
326 + "device_sda_type_sat_attr_reallocated_sector_ct_normalized": 100,
327 + "device_sda_type_sat_attr_reallocated_sector_ct_raw": 0,
328 + "device_sda_type_sat_attr_seek_error_rate_decoded": 0,
329 + "device_sda_type_sat_attr_seek_error_rate_normalized": 100,
330 + "device_sda_type_sat_attr_seek_error_rate_raw": 0,
331 + "device_sda_type_sat_attr_seek_time_performance_decoded": 15,
332 + "device_sda_type_sat_attr_seek_time_performance_normalized": 140,
333 + "device_sda_type_sat_attr_seek_time_performance_raw": 15,
334 + "device_sda_type_sat_attr_spin_retry_count_decoded": 0,
335 + "device_sda_type_sat_attr_spin_retry_count_normalized": 100,
336 + "device_sda_type_sat_attr_spin_retry_count_raw": 0,
337 + "device_sda_type_sat_attr_spin_up_time_decoded": 281,
338 + "device_sda_type_sat_attr_spin_up_time_normalized": 86,
339 + "device_sda_type_sat_attr_spin_up_time_raw": 25788088601,
340 + "device_sda_type_sat_attr_start_stop_count_decoded": 12,
341 + "device_sda_type_sat_attr_start_stop_count_normalized": 100,
342 + "device_sda_type_sat_attr_start_stop_count_raw": 12,
343 + "device_sda_type_sat_attr_temperature_celsius_decoded": 49,
344 + "device_sda_type_sat_attr_temperature_celsius_normalized": 43,
345 + "device_sda_type_sat_attr_temperature_celsius_raw": 240519741489,
346 + "device_sda_type_sat_attr_throughput_performance_decoded": 48,
347 + "device_sda_type_sat_attr_throughput_performance_normalized": 148,
348 + "device_sda_type_sat_attr_throughput_performance_raw": 48,
349 + "device_sda_type_sat_attr_udma_crc_error_count_decoded": 0,
350 + "device_sda_type_sat_attr_udma_crc_error_count_normalized": 100,
351 + "device_sda_type_sat_attr_udma_crc_error_count_raw": 0,
352 + "device_sda_type_sat_attr_unknown_attribute_decoded": 100,
353 + "device_sda_type_sat_attr_unknown_attribute_normalized": 100,
354 + "device_sda_type_sat_attr_unknown_attribute_raw": 100,
355 + "device_sda_type_sat_power_cycle_count": 12,
356 + "device_sda_type_sat_power_on_time": 29678400,
357 + "device_sda_type_sat_smart_status_failed": 0,
358 + "device_sda_type_sat_smart_status_passed": 1,
359 + "device_sda_type_sat_temperature": 49,
360 + "device_sdc_type_sat_ata_smart_error_log_summary_count": 0,
361 + "device_sdc_type_sat_attr_available_reservd_space_decoded": 100,
362 + "device_sdc_type_sat_attr_available_reservd_space_normalized": 100,
363 + "device_sdc_type_sat_attr_available_reservd_space_raw": 100,
364 + "device_sdc_type_sat_attr_command_timeout_decoded": 0,
365 + "device_sdc_type_sat_attr_command_timeout_normalized": 100,
366 + "device_sdc_type_sat_attr_command_timeout_raw": 0,
367 + "device_sdc_type_sat_attr_end-to-end_error_decoded": 0,
368 + "device_sdc_type_sat_attr_end-to-end_error_normalized": 100,
369 + "device_sdc_type_sat_attr_end-to-end_error_raw": 0,
370 + "device_sdc_type_sat_attr_media_wearout_indicator_decoded": 65406,
371 + "device_sdc_type_sat_attr_media_wearout_indicator_normalized": 100,
372 + "device_sdc_type_sat_attr_media_wearout_indicator_raw": 65406,
373 + "device_sdc_type_sat_attr_power_cycle_count_decoded": 13,
374 + "device_sdc_type_sat_attr_power_cycle_count_normalized": 100,
375 + "device_sdc_type_sat_attr_power_cycle_count_raw": 13,
376 + "device_sdc_type_sat_attr_power_on_hours_decoded": 8244,
377 + "device_sdc_type_sat_attr_power_on_hours_normalized": 100,
378 + "device_sdc_type_sat_attr_power_on_hours_raw": 8244,
379 + "device_sdc_type_sat_attr_reallocated_sector_ct_decoded": 0,
380 + "device_sdc_type_sat_attr_reallocated_sector_ct_normalized": 100,
381 + "device_sdc_type_sat_attr_reallocated_sector_ct_raw": 0,
382 + "device_sdc_type_sat_attr_reported_uncorrect_decoded": 0,
383 + "device_sdc_type_sat_attr_reported_uncorrect_normalized": 100,
384 + "device_sdc_type_sat_attr_reported_uncorrect_raw": 0,
385 + "device_sdc_type_sat_attr_temperature_celsius_decoded": 27,
386 + "device_sdc_type_sat_attr_temperature_celsius_normalized": 73,
387 + "device_sdc_type_sat_attr_temperature_celsius_raw": 184684970011,
388 + "device_sdc_type_sat_attr_total_lbas_read_decoded": 76778,
389 + "device_sdc_type_sat_attr_total_lbas_read_normalized": 253,
390 + "device_sdc_type_sat_attr_total_lbas_read_raw": 76778,
391 + "device_sdc_type_sat_attr_total_lbas_written_decoded": 173833,
392 + "device_sdc_type_sat_attr_total_lbas_written_normalized": 253,
393 + "device_sdc_type_sat_attr_total_lbas_written_raw": 173833,
394 + "device_sdc_type_sat_attr_udma_crc_error_count_decoded": 0,
395 + "device_sdc_type_sat_attr_udma_crc_error_count_normalized": 100,
396 + "device_sdc_type_sat_attr_udma_crc_error_count_raw": 0,
397 + "device_sdc_type_sat_attr_unknown_attribute_decoded": 0,
398 + "device_sdc_type_sat_attr_unknown_attribute_normalized": 0,
399 + "device_sdc_type_sat_attr_unknown_attribute_raw": 0,
400 + "device_sdc_type_sat_attr_unknown_ssd_attribute_decoded": 4694419309637,
401 + "device_sdc_type_sat_attr_unknown_ssd_attribute_normalized": 4,
402 + "device_sdc_type_sat_attr_unknown_ssd_attribute_raw": 4694419309637,
403 + "device_sdc_type_sat_power_cycle_count": 13,
404 + "device_sdc_type_sat_power_on_time": 29678400,
405 + "device_sdc_type_sat_smart_status_failed": 0,
406 + "device_sdc_type_sat_smart_status_passed": 1,
407 + "device_sdc_type_sat_temperature": 27,
408 + },
409 + },
410 "success type nvme devices": {
411 prepareMock: prepareMockOkTypeNvme,
412 wantCharts: 4,
src/go/plugin/go.d/collector/smartctl/config_schema.json
+12 -1
@@ -51,6 +51,13 @@
51 "minimum": 1,
52 "default": "*"
53 },
54 + "concurrent_scans": {
55 + "title": "Concurrent scans",
56 + "description": "Number of devices to scan concurrently. Set to 0 for sequential scanning (default), or a positive number for concurrent scanning.",
57 + "type": "integer",
58 + "minimum": 0,
59 + "default": 0
60 + },
61 "extra_devices": {
62 "title": "Extra devices",
63 "description": "Allows manual specification of devices not automatically detected by `smartctl --scan`. Each device entry must include both a name and a type.",
@@ -100,7 +107,8 @@
107 "timeout",
108 "scan_every",
109 "poll_devices_every",
103 - "no_check_power_mode"
110 + "no_check_power_mode",
111 + "concurrent_scans"
112 ]
113 },
114 {
@@ -125,6 +133,9 @@
133 "device_selector": {
134 "ui:help": "Leave blank or use `*` to collect data for all devices."
135 },
136 + "concurrent_scans": {
137 + "ui:help": "Improves performance when monitoring many devices. Set to the number of devices to scan simultaneously, or 0 to disable concurrent scanning."
138 + },
139 "extra_devices": {
140 "items": {
141 "name": {
src/go/plugin/go.d/collector/smartctl/metadata.yaml
+11
@@ -114,6 +114,10 @@ modules:
114 description: "Specifies a pattern to match the 'info name' of devices as reported by `smartctl --scan --json`."
115 default_value: "*"
116 required: false
117 + - name: concurrent_scans
118 + description: "Number of devices to scan concurrently. Set to 0 for sequential scanning (default behavior). Improves performance when monitoring many devices."
119 + default_value: 0
120 + required: false
121 - name: extra_devices
122 description: "Allows manual specification of devices not automatically detected by `smartctl --scan`. Each device entry must include both a name and a type. See \"Configuration Examples\" for details."
123 default_value: "[]"
@@ -142,6 +146,13 @@ modules:
146 jobs:
147 - name: smartctl
148 devices_poll_interval: 60 # Collect S.M.A.R.T statistics every 60 seconds
149 + - name: Concurrent scanning for multiple devices
150 + description: |
151 + This example demonstrates enabling concurrent scanning to improve performance when monitoring many devices.
152 + config: |
153 + jobs:
154 + - name: smartctl
155 + concurrent_scans: 4 # Scan up to 4 devices concurrently
156 - name: Extra devices
157 description: |
158 This example demonstrates using `extra_devices` to manually add a storage device (`/dev/sdc`) not automatically detected by `smartctl --scan`.
src/go/plugin/go.d/collector/smartctl/testdata/config.json
+1
@@ -3,6 +3,7 @@
3 "timeout": 123.123,
4 "scan_every": 123.123,
5 "poll_devices_every": 123.123,
6 + "concurrent_scans": 0,
7 "no_check_power_mode": "ok",
8 "device_selector": "ok",
9 "extra_devices": [
src/go/plugin/go.d/collector/smartctl/testdata/config.yaml
+1
@@ -2,6 +2,7 @@ update_every: 123
2 timeout: 123.123
3 scan_every: 123.123
4 poll_devices_every: 123.123
5 +concurrent_scans: 123
6 no_check_power_mode: "ok"
7 device_selector: "ok"
8 extra_devices: