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: