Skip to content

Commit 1f0337f

Browse files
craig[bot]sean-
andcommitted
Merge #146966
146966: pkg/storage/disk: Add support for monitoring disk stats for unknown dev r=sean- a=sean- Previously, crdb couldn't monitor stores running on unknown devices. Now the code attempts to resolve the underlying device based on the fstype. Any failures fallback to the current behavior. Epic: none Release note: None Fixes: #146336 Co-authored-by: Sean Chittenden <[email protected]>
2 parents 0c796a2 + 5601568 commit 1f0337f

File tree

8 files changed

+476
-6
lines changed

8 files changed

+476
-6
lines changed

pkg/storage/disk/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ go_test(
4949
"linux_parse_test.go",
5050
"monitor_test.go",
5151
"monitor_tracer_test.go",
52+
"platform_linux_test.go",
5253
],
5354
data = glob(["testdata/**"]),
5455
embed = [":disk"],

pkg/storage/disk/monitor.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,5 +321,5 @@ func getDeviceIDFromPath(fs vfs.FS, path string) (DeviceID, error) {
321321
if err != nil {
322322
return DeviceID{}, errors.Wrapf(err, "fstat(%s)", path)
323323
}
324-
return deviceIDFromFileInfo(finfo), nil
324+
return deviceIDFromFileInfo(finfo, path), nil
325325
}

pkg/storage/disk/platform_darwin.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func newStatsCollector(fs vfs.FS) (*darwinCollector, error) {
2626
return &darwinCollector{}, nil
2727
}
2828

29-
func deviceIDFromFileInfo(finfo fs.FileInfo) DeviceID {
29+
func deviceIDFromFileInfo(finfo fs.FileInfo, path string) DeviceID {
3030
statInfo := finfo.Sys().(*sysutil.StatT)
3131
id := DeviceID{
3232
major: unix.Major(uint64(statInfo.Dev)),

pkg/storage/disk/platform_default.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,6 @@ func newStatsCollector(fs vfs.FS) (*defaultCollector, error) {
2424
return &defaultCollector{}, nil
2525
}
2626

27-
func deviceIDFromFileInfo(fs.FileInfo) DeviceID {
27+
func deviceIDFromFileInfo(fs.FileInfo, string) DeviceID {
2828
return DeviceID{}
2929
}

pkg/storage/disk/platform_linux.go

Lines changed: 221 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,20 @@
88
package disk
99

1010
import (
11+
"bufio"
12+
"bytes"
13+
"context"
14+
"fmt"
1115
"io"
1216
"io/fs"
17+
"os"
18+
"os/exec"
19+
"path/filepath"
20+
"regexp"
21+
"strings"
1322
"time"
1423

24+
"github.com/cockroachdb/cockroach/pkg/util/log"
1525
"github.com/cockroachdb/cockroach/pkg/util/sysutil"
1626
"github.com/cockroachdb/errors"
1727
"github.com/cockroachdb/pebble/vfs"
@@ -63,11 +73,219 @@ func newStatsCollector(fs vfs.FS) (*linuxStatsCollector, error) {
6373
}, nil
6474
}
6575

66-
func deviceIDFromFileInfo(finfo fs.FileInfo) DeviceID {
76+
func deviceIDFromFileInfo(finfo fs.FileInfo, path string) DeviceID {
77+
ctx := context.TODO()
6778
statInfo := finfo.Sys().(*sysutil.StatT)
79+
major := unix.Major(statInfo.Dev)
80+
minor := unix.Minor(statInfo.Dev)
81+
82+
// Per /usr/include/linux/major.h and Documentation/admin-guide/devices.rst:
83+
switch major {
84+
case 0: // UNNAMED_MAJOR
85+
86+
// Perform additional lookups for unknown device types
87+
var statfs sysutil.StatfsT
88+
err := sysutil.Statfs(path, &statfs)
89+
if err != nil {
90+
maybeWarnf(ctx, "unable to statfs(2) path %q (%d:%d): %v", path, major, minor, err)
91+
return DeviceID{major, minor}
92+
}
93+
94+
switch statfs.Type {
95+
case 0x2fc12fc1: // ZFS_SUPER_MAGIC from include/sys/fs/zfs.h
96+
major, minor, err = deviceIDForZFS(path)
97+
if err != nil {
98+
maybeWarnf(ctx, "zfs: unable to find device ID for %q: %v", path, err)
99+
} else {
100+
maybeInfof(ctx, "zfs: mapping %q to diskstats device %d:%d", path, major, minor)
101+
}
102+
103+
id := DeviceID{
104+
major: major,
105+
minor: minor,
106+
}
107+
return id
108+
109+
case 0x58465342: // XFS_SUPER_MAGIC from linux/magic.h "XFSB"
110+
maybeWarnf(ctx, "xfs: unable to find device ID for %q: %v", path, err)
111+
112+
default:
113+
maybeWarnf(ctx, "unsupported file system type %x for path (%d:%d) %q", statfs.Type, major, minor, path)
114+
}
115+
116+
case 259: // BLOCK_EXT_MAJOR=259
117+
118+
// NOTE: Major device 259 is the happy path for ext4 and xfs filesystems: no
119+
// additional handling is required.
120+
121+
maybeInfof(ctx, "mapping %q to diskstats device %d:%d", path, major, minor)
122+
123+
default:
124+
maybeWarnf(ctx, "unsupported device type %d:%d for store at %q", major, minor, path)
125+
}
126+
68127
id := DeviceID{
69-
major: unix.Major(statInfo.Dev),
70-
minor: unix.Minor(statInfo.Dev),
128+
major: major,
129+
minor: minor,
71130
}
72131
return id
73132
}
133+
134+
type _ZPoolName string
135+
136+
func deviceIDForZFS(path string) (uint32, uint32, error) {
137+
zpoolName, err := zfsGetPoolName(path)
138+
if err != nil {
139+
return 0, 0, errors.Newf("unable to find the zpool for %q: %v", path, err) // nolint:errwrap
140+
}
141+
142+
// If there are multiple devices for a zpool, an error is returned along with
143+
// a device name. Continue resolving the device's major:minor numbers,
144+
// despite the multiple drives.
145+
devName, err := zpoolGetDevice(zpoolName)
146+
if err != nil && devName == "" {
147+
return 0, 0, errors.Newf("unable to find the device for pool %q: %v", zpoolName, err) // nolint:errwrap
148+
}
149+
150+
major, minor, err := getDeviceID(devName)
151+
if err != nil {
152+
return 0, 0, errors.Newf("unable to find the device numbers for device %q: %v", devName, err) // nolint:errwrap
153+
}
154+
155+
return major, minor, nil
156+
}
157+
158+
func zfsGetPoolName(path string) (_ZPoolName, error) {
159+
out, err := exec.Command("df", "--no-sync", "--output=source,fstype", path).Output()
160+
if err != nil {
161+
return "", errors.Newf("unable to exec df(1): %v", err) // nolint:errwrap
162+
}
163+
164+
return zfsParseDF(out)
165+
}
166+
167+
func zfsParseDF(df []byte) (_ZPoolName, error) {
168+
lines := strings.Split(strings.TrimSpace(string(df)), "\n")
169+
if len(lines) < 2 {
170+
return "", fmt.Errorf("unexpected df(1) output: %q", df)
171+
}
172+
173+
fields := strings.Fields(lines[1])
174+
if len(fields) != 2 {
175+
return "", fmt.Errorf("unexpected df(1) fields (expected 2, got %d): %q", len(fields), lines[1])
176+
}
177+
178+
if fields[1] != "zfs" {
179+
return "", fmt.Errorf("unexpected df(1) fields (expected 2, got %d): %q", len(fields), lines[1])
180+
}
181+
182+
// Need to accept inputs formed like "data1" and "data1/crdb-logs"
183+
poolName := strings.Split(fields[0], "/")[0]
184+
185+
return _ZPoolName(poolName), nil
186+
}
187+
188+
func zpoolGetDevice(poolName _ZPoolName) (string, error) {
189+
ctx := context.TODO()
190+
191+
out, err := exec.Command("zpool", "status", "-pPL", string(poolName)).Output()
192+
if err != nil {
193+
return "", errors.Newf("unable to find the devices attached to pool %q: %v", poolName, err) // nolint:errwrap
194+
}
195+
196+
return zpoolParseStatus(ctx, poolName, out)
197+
}
198+
199+
func zpoolParseStatus(ctx context.Context, poolName _ZPoolName, output []byte) (string, error) {
200+
scanner := bufio.NewScanner(bytes.NewReader(output))
201+
var devName string
202+
var devCount int
203+
for scanner.Scan() {
204+
line := strings.TrimSpace(scanner.Text())
205+
fields := strings.Fields(line)
206+
if len(fields) >= 2 && fields[1] == "ONLINE" && strings.HasPrefix(fields[0], "/dev/") {
207+
devCount++
208+
if devName == "" {
209+
devName = stripDevicePartition(fields[0])
210+
} else {
211+
maybeWarnf(ctx, "unsupported configuration: multiple devices (i.e. %q, %q) detected for zpool %q", devName, fields[0], string(poolName))
212+
}
213+
}
214+
}
215+
216+
switch {
217+
case devCount == 1:
218+
return devName, nil
219+
case devCount > 1:
220+
return devName, errors.Newf("unsupported configuration: %d devices detected for zpool %q", devCount, string(poolName))
221+
default:
222+
return "", fmt.Errorf("no device found for zpool %q", poolName)
223+
}
224+
}
225+
226+
var (
227+
nvmePartitionRegex = regexp.MustCompile(`^(nvme\d+n\d+)(p\d+)?$`)
228+
scsiPartitionRegex = regexp.MustCompile(`^(ram|loop|fd|(h|s|v|xv)d[a-z])(\d+)?$`)
229+
)
230+
231+
// stripDevicePartition removes partition suffix from a device path.
232+
func stripDevicePartition(devicePath string) string {
233+
base := filepath.Base(devicePath)
234+
235+
nvmeMatches := nvmePartitionRegex.FindStringSubmatch(base)
236+
if len(nvmeMatches) == 3 {
237+
return nvmeMatches[1]
238+
}
239+
240+
scsiMatches := scsiPartitionRegex.FindStringSubmatch(base)
241+
if len(scsiMatches) >= 3 {
242+
return scsiMatches[1]
243+
}
244+
245+
// If no match, return original device path
246+
return devicePath
247+
}
248+
249+
// getDeviceID takes a block device name (e.g., nvme5n1) and returns its major and minor numbers.
250+
func getDeviceID(devPath string) (uint32, uint32, error) {
251+
devName := filepath.Base(devPath)
252+
devFilePath := fmt.Sprintf("/sys/block/%s/dev", devName)
253+
data, err := os.ReadFile(devFilePath)
254+
if err != nil {
255+
return 0, 0, errors.Newf("unable to read %q: %v", devFilePath, err) // nolint:errwrap
256+
}
257+
258+
return parseDeviceID(devFilePath, data)
259+
}
260+
261+
func parseDeviceID(devFilePath string, data []byte) (uint32, uint32, error) {
262+
devStr := strings.TrimSpace(string(data))
263+
parts := strings.Split(devStr, ":")
264+
if len(parts) != 2 {
265+
return 0, 0, fmt.Errorf("unexpected device string format in %q: %s", devFilePath, devStr)
266+
}
267+
268+
var maj, min uint32
269+
_, err := fmt.Sscanf(devStr, "%d:%d", &maj, &min)
270+
if err != nil {
271+
return 0, 0, errors.Newf("failed parsing device numbers: %v", err) // nolint:errwrap
272+
}
273+
274+
return maj, min, nil
275+
}
276+
277+
// maybeWarnf is a convenience function to prevent panicing during bootstrap
278+
// from using logging before it is setup.
279+
func maybeWarnf(ctx context.Context, format string, args ...interface{}) {
280+
if active, _ := log.IsActive(); active {
281+
log.Ops.WarningfDepth(ctx, 1, format, args...)
282+
}
283+
}
284+
285+
// maybeInfof is a convenience function to prevent panicing during bootstrap
286+
// from using logging before it is setup.
287+
func maybeInfof(ctx context.Context, format string, args ...interface{}) {
288+
if active, _ := log.IsActive(); active {
289+
log.Ops.InfofDepth(ctx, 1, format, args...)
290+
}
291+
}

0 commit comments

Comments
 (0)