···3838 "github.com/sourcegraph/zoekt"
3939 "github.com/sourcegraph/zoekt/build"
4040 "github.com/sourcegraph/zoekt/debugserver"
4141+ "github.com/sourcegraph/zoekt/internal/mountinfo"
4142 "github.com/sourcegraph/zoekt/internal/profiler"
4243 "github.com/sourcegraph/zoekt/internal/tracer"
4344 "github.com/sourcegraph/zoekt/query"
···180181181182 mustRegisterDiskMonitor(*index)
182183183183- mmapLogger := sglog.Scoped("zoekt_webserver_proc_metrics_memory_map", "")
184184- mustRegisterMemoryMapMetrics(mmapLogger)
184184+ metricsLogger := sglog.Scoped("metricsRegistration", "")
185185+186186+ mustRegisterMemoryMapMetrics(metricsLogger)
187187+ mountinfo.MustRegisterNewMountPointInfoMetric(metricsLogger, map[string]string{"indexDir": *index})
185188186189 // Do not block on loading shards so we can become partially available
187190 // sooner. Otherwise on large instances zoekt can be unavailable on the
···510513)
511514512515func mustRegisterMemoryMapMetrics(logger sglog.Logger) {
516516+ logger = logger.Scoped("memoryMapMetrics", "")
517517+513518 // The memory map metrics are collected via /proc, which
514519 // is only available on linux-based operating systems.
515520
-1
indexfile_unix.go
···1313// limitations under the License.
14141515//go:build linux || darwin
1616-// +build linux darwin
17161817package zoekt
1918
+214
internal/mountinfo/mountinfo.go
···11+package mountinfo
22+33+import (
44+ "errors"
55+ "fmt"
66+ "os"
77+ "path/filepath"
88+ "runtime"
99+ "strings"
1010+1111+ "github.com/prometheus/client_golang/prometheus"
1212+ "github.com/prometheus/client_golang/prometheus/promauto"
1313+ sglog "github.com/sourcegraph/log"
1414+ "golang.org/x/sys/unix"
1515+)
1616+1717+// defaultSysMountPoint is the common mount point for the sysfs pseudo-filesystem.
1818+const defaultSysMountPoint = "/sys"
1919+2020+// MustRegisterNewMountPointInfoMetric registers a Prometheus metric named "mount_point_info" that
2121+// contains the names of the block storage devices that back each of the requested mounts.
2222+//
2323+// Mounts is a set of name -> file path mappings (example: {"indexDir": "/home/.zoekt"}).
2424+//
2525+// The metric "mount_point_info" has a constant value of 1 and two labels:
2626+// - mount_name: caller-provided name for the given mount (example: "indexDir")
2727+// - device: name of the block device that backs the given mount file path (example: "sdb")
2828+//
2929+// This metric only works on Linux-based operating systems that have access to the sysfs pseudo-filesystem.
3030+// On all other operating systems, this metric will not emit any values.
3131+func MustRegisterNewMountPointInfoMetric(logger sglog.Logger, mounts map[string]string) {
3232+ logger = logger.Scoped("mountPointInfo", "registration logic for mount_point_info Prometheus metric")
3333+3434+ metric := promauto.NewGaugeVec(prometheus.GaugeOpts{
3535+ Name: "mount_point_info",
3636+ Help: "An info metric with a constant '1' value that contains mount_name, device mappings",
3737+ }, []string{"mount_name", "device"})
3838+3939+ // This device discovery logic relies on the sysfs pseudo-filesystem, which only exists
4040+ // on linux.
4141+ //
4242+ // See https://en.wikipedia.org/wiki/Sysfs for more information.
4343+ if runtime.GOOS != "linux" {
4444+ return
4545+ }
4646+4747+ for name, filePath := range mounts {
4848+ // for each <mountName>:<mountFilePath> pairing,
4949+ // discover the name of the block device that stores <mountFilePath>.
5050+ discoveryLogger := logger.Scoped("deviceNameDiscovery", "").With(
5151+ sglog.String("mountName", name),
5252+ sglog.String("mountFilePath", filePath),
5353+ )
5454+5555+ device, err := discoverDeviceName(discoveryLogger, discoverDeviceNameConfig{}, filePath)
5656+ if err != nil {
5757+ discoveryLogger.Warn("skipping metric registration",
5858+ sglog.String("reason", "failed to discover device name"),
5959+ sglog.Error(err),
6060+ )
6161+6262+ continue
6363+ }
6464+6565+ discoveryLogger.Debug("discovered device name",
6666+ sglog.String("deviceName", device),
6767+ )
6868+6969+ metric.WithLabelValues(name, device).Set(1)
7070+ }
7171+}
7272+7373+type discoverDeviceNameConfig struct {
7474+ // sysfsMountPoint is the location of the sysfs mount point.
7575+ // If empty, defaultSysMountPoint will be used instead.
7676+ sysfsMountPoint string
7777+7878+ // getDeviceNumber, if non-nil, is the function that will be used to find
7979+ // the number of the block device that stores the specified file.
8080+ // If getDeviceNumber is nil, mountinfo.getDeviceNumber will be used instead.
8181+ getDeviceNumber func(filePath string) (major uint32, minor uint32, err error)
8282+}
8383+8484+// discoverDeviceName returns the name of the block device that filePath is
8585+// stored on.
8686+func discoverDeviceName(logger sglog.Logger, config discoverDeviceNameConfig, filePath string) (string, error) {
8787+ // Note: It's quite involved to implement the device discovery logic for
8888+ // every possible kind of storage device (e.x. logical volumes, NFS, etc.) See
8989+ // https://unix.stackexchange.com/a/11312 for more information.
9090+ //
9191+ // As a result, this logic will only work correctly for filePaths that are either:
9292+ // - stored directly on a block device
9393+ // - stored on a block device's partition
9494+ //
9595+ // For all other device types, this logic will either:
9696+ // - return an incorrect device name
9797+ // - return an error
9898+ //
9999+ // This logic was implemented from information gathered from the following sources (amongst others):
100100+ // - "The Linux Programming Interface" by Michael Kerrisk: Chapter 14
101101+ // - "Linux Kernel Development" by Robert Love: Chapters 13, 17
102102+ // - https://man7.org/linux/man-pages/man5/sysfs.5.html
103103+ // - https://en.wikipedia.org/wiki/Sysfs
104104+ // - https://unix.stackexchange.com/a/11312
105105+ // - https://www.kernel.org/doc/ols/2005/ols2005v1-pages-321-334.pdf
106106+107107+ getDeviceNumber := getDeviceNumber
108108+ if config.getDeviceNumber != nil {
109109+ getDeviceNumber = config.getDeviceNumber
110110+ }
111111+112112+ sysfsMountPoint := defaultSysMountPoint
113113+ if config.sysfsMountPoint != "" {
114114+ sysfsMountPoint = config.sysfsMountPoint
115115+ }
116116+117117+ sysfsMountPoint = filepath.Clean(sysfsMountPoint)
118118+119119+ // the provided sysfs mountpoint could itself be a symlink, so we
120120+ // resolve it immediately so that future file path
121121+ // evaluations / massaging doesn't break
122122+ sysfsMountPoint, err := filepath.EvalSymlinks(sysfsMountPoint)
123123+ if err != nil {
124124+ return "", fmt.Errorf("verifying sysfs mountpoint %q: failed to resolve symlink %w", sysfsMountPoint, err)
125125+ }
126126+127127+ major, minor, err := getDeviceNumber(filePath)
128128+ if err != nil {
129129+ return "", fmt.Errorf("discovering device number: %w", err)
130130+ }
131131+132132+ // Represent the number in <major>:<minor> format.
133133+ deviceNumber := fmt.Sprintf("%d:%d", major, minor)
134134+135135+ logger.Debug(
136136+ "discovered device number",
137137+ sglog.String("deviceNumber", deviceNumber),
138138+ )
139139+140140+ // /sys/dev/block/<device_number> symlinks to /sys/devices/.../block/.../<deviceName>
141141+ symlink := filepath.Join(sysfsMountPoint, "dev", "block", deviceNumber)
142142+143143+ devicePath, err := filepath.EvalSymlinks(symlink)
144144+ if err != nil {
145145+ return "", fmt.Errorf("discovering device path: failed to evaluate sysfs symlink %q: %w", symlink, err)
146146+ }
147147+148148+ devicePath, err = filepath.Abs(devicePath)
149149+ if err != nil {
150150+ return "", fmt.Errorf("discovering device path: failed to massage device path %q to absolute path: %w", devicePath, err)
151151+ }
152152+153153+ logger.Debug("discovered device path",
154154+ sglog.String("devicePath", devicePath),
155155+ )
156156+157157+ // Check to see if devicePath points to a disk partition. If so, we need to find the parent
158158+ // device.
159159+160160+ // massage the sysfs folder name to ensure that it always ends in a '/'
161161+ // so that strings.HasPrefix does what we expect when checking to see if
162162+ // we're still under the /sys sub-folder
163163+ sysFolderPrefix := strings.TrimSuffix(sysfsMountPoint, string(os.PathSeparator))
164164+ sysFolderPrefix = sysFolderPrefix + string(os.PathSeparator)
165165+166166+ for {
167167+ if !strings.HasPrefix(devicePath, sysFolderPrefix) {
168168+ // ensure that we're still under the /sys/ sub-folder
169169+ return "", fmt.Errorf("validating device path: device path %q isn't a subpath of %q", devicePath, sysFolderPrefix)
170170+ }
171171+172172+ _, err := os.Stat(filepath.Join(devicePath, "partition"))
173173+ if errors.Is(err, os.ErrNotExist) {
174174+ break
175175+ }
176176+177177+ parent := filepath.Dir(devicePath)
178178+179179+ logger.Debug("changing device path",
180180+ sglog.String("reason", "oldDevicePath represents a disk partition"),
181181+182182+ sglog.String("oldDevicePath", devicePath),
183183+ sglog.String("newDevicePath", parent),
184184+ )
185185+186186+ devicePath = parent
187187+ }
188188+189189+ // If this device is a block device, its device path should have a symlink
190190+ // to the block subsystem.
191191+192192+ subsystemPath, err := filepath.EvalSymlinks(filepath.Join(devicePath, "subsystem"))
193193+ if err != nil {
194194+ return "", fmt.Errorf("validating device path: failed to discover subsystem that device (path %q) is part of: %w", devicePath, err)
195195+ }
196196+197197+ if filepath.Base(subsystemPath) != "block" {
198198+ return "", fmt.Errorf("validating device path: device (path %q) is not part of the block subsystem", devicePath)
199199+ }
200200+201201+ device := filepath.Base(devicePath)
202202+ return filepath.Base(device), nil
203203+}
204204+205205+func getDeviceNumber(filePath string) (major uint32, minor uint32, err error) {
206206+ var stat unix.Stat_t
207207+ err = unix.Stat(filePath, &stat)
208208+ if err != nil {
209209+ return 0, 0, fmt.Errorf("failed to stat %q: %w", filePath, err)
210210+ }
211211+212212+ major, minor = unix.Major(uint64(stat.Dev)), unix.Minor(uint64(stat.Dev))
213213+ return major, minor, nil
214214+}
+29
internal/mountinfo/mountinfo_linux_test.go
···11+//go:build linux
22+33+package mountinfo
44+55+import (
66+ "log"
77+ "os"
88+ "testing"
99+1010+ "github.com/sourcegraph/log/logtest"
1111+)
1212+1313+func Test_DeviceName_SmokeTest(t *testing.T) {
1414+ // A simple smoke test to verify that we can find the storage device
1515+ // for the current working directory.
1616+ logger := logtest.Scoped(t)
1717+1818+ filePath, err := os.Getwd()
1919+ if err != nil {
2020+ log.Fatalf("getting current working directory: %s", err)
2121+ }
2222+2323+ device, err := discoverDeviceName(logger, discoverDeviceNameConfig{}, filePath)
2424+ if err != nil {
2525+ t.Fatalf("discovering device name for file path %q: %s", filePath, err)
2626+ }
2727+2828+ t.Logf("discovered device name %q for file path %q", device, filePath)
2929+}
+167
internal/mountinfo/mountinfo_test.go
···11+package mountinfo
22+33+import (
44+ "archive/tar"
55+ "compress/gzip"
66+ "io"
77+ "os"
88+ "path/filepath"
99+ "testing"
1010+1111+ "github.com/google/go-cmp/cmp"
1212+ "github.com/sourcegraph/log/logtest"
1313+)
1414+1515+func Test_DeviceName_Snapshots(t *testing.T) {
1616+ // This test uses sysfs snapshots from real linux machines to ensure
1717+ // that the device discovery logic returns the expected device name.
1818+1919+ for _, test := range []struct {
2020+ name string
2121+2222+ sysfsTarballFile string
2323+2424+ deviceMajor uint32
2525+ deviceMinor uint32
2626+2727+ expectedDeviceName string
2828+ }{
2929+ {
3030+ name: "should find the name of the block device that backs a partition (vda1 -> vda)",
3131+3232+ // ( lsblk output from the snapshotted machine)
3333+ // ~ # lsblk
3434+ // NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
3535+ // nbd0 43:0 0 0B 0 disk
3636+ // nbd1 43:32 0 0B 0 disk
3737+ // nbd2 43:64 0 0B 0 disk
3838+ // nbd3 43:96 0 0B 0 disk
3939+ // nbd4 43:128 0 0B 0 disk
4040+ // nbd5 43:160 0 0B 0 disk
4141+ // nbd6 43:192 0 0B 0 disk
4242+ // nbd7 43:224 0 0B 0 disk
4343+ // vda 254:0 0 59.6G 0 disk
4444+ // └─vda1 254:1 0 59.6G 0 part /etc/hosts # test targets this partition
4545+ // /etc/hostname
4646+ // /etc/resolv.conf
4747+ // /data/index
4848+ // nbd8 43:256 0 0B 0 disk
4949+ // nbd9 43:288 0 0B 0 disk
5050+ // nbd10 43:320 0 0B 0 disk
5151+ // nbd11 43:352 0 0B 0 disk
5252+ // nbd12 43:384 0 0B 0 disk
5353+ // nbd13 43:416 0 0B 0 disk
5454+ // nbd14 43:448 0 0B 0 disk
5555+ // nbd15 43:480 0 0B 0 disk
5656+5757+ sysfsTarballFile: "sysfs.vda1.tar.gz",
5858+5959+ deviceMajor: 254, // points to vda1 partition
6060+ deviceMinor: 1,
6161+6262+ expectedDeviceName: "vda",
6363+ },
6464+ } {
6565+ test := test
6666+6767+ t.Run(t.Name(), func(t *testing.T) {
6868+ t.Parallel()
6969+7070+ // provide a custom sysfs location so that we can point the test
7171+ // at our sysfs snapshot
7272+ mockSysFSDir := filepath.Join(t.TempDir(), "sys")
7373+7474+ // unpack sysfs tarball
7575+ tarball := filepath.Join("testdata", test.sysfsTarballFile)
7676+ decompressSysFSTarball(t, tarball, mockSysFSDir)
7777+7878+ logger := logtest.Scoped(t)
7979+8080+ mockGetDeviceNumber := func(_ string) (major uint32, minor uint32, err error) {
8181+ return test.deviceMajor, test.deviceMinor, nil
8282+ }
8383+ fakeFilePath := "doesn't matter" // the file path itself doesn't matter since we hard-code the device number
8484+8585+ // execute the test with our injected mocks
8686+ actualDeviceName, err := discoverDeviceName(
8787+ logger,
8888+ discoverDeviceNameConfig{
8989+ sysfsMountPoint: mockSysFSDir,
9090+ getDeviceNumber: mockGetDeviceNumber,
9191+ },
9292+ fakeFilePath,
9393+ )
9494+9595+ if err != nil {
9696+ t.Fatalf("discovering device name for file path %q: %s", fakeFilePath, err)
9797+ }
9898+9999+ // verify that the discovered device name is the one that we expect
100100+101101+ if diff := cmp.Diff(test.expectedDeviceName, actualDeviceName); diff != "" {
102102+ t.Fatalf("recieved unexpected device name (-want +got):\n%s", diff)
103103+ }
104104+ })
105105+ }
106106+}
107107+108108+func decompressSysFSTarball(t *testing.T, tarball, outputFolder string) {
109109+ t.Helper()
110110+111111+ file, err := os.Open(tarball)
112112+ if err != nil {
113113+ t.Fatalf("opening tarball %q: %s", tarball, err)
114114+ }
115115+116116+ defer file.Close()
117117+118118+ gz, err := gzip.NewReader(file)
119119+ if err != nil {
120120+ t.Fatalf("initialzing gzip reader: %s", err)
121121+ }
122122+123123+ reader := tar.NewReader(gz)
124124+125125+ for {
126126+ header, err := reader.Next()
127127+ if err == io.EOF {
128128+ break
129129+ }
130130+131131+ if err != nil {
132132+ t.Fatalf("intializing tar reader: %s", err)
133133+ }
134134+135135+ outputFile := filepath.Join(outputFolder, header.Name)
136136+137137+ switch header.Typeflag {
138138+ case tar.TypeDir:
139139+ err := os.MkdirAll(outputFile, os.FileMode(header.Mode))
140140+ if err != nil {
141141+ t.Fatalf("creating directory %q: %s", outputFile, err)
142142+ }
143143+144144+ case tar.TypeSymlink:
145145+ err := os.Symlink(header.Linkname, outputFile)
146146+ if err != nil {
147147+ t.Fatalf("creating symlink (%q -> %q): %s", outputFile, header.Linkname, err)
148148+ }
149149+150150+ case tar.TypeReg:
151151+ f, err := os.OpenFile(outputFile, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
152152+ if err != nil {
153153+ t.Fatalf("creating file %q: %s", outputFile, err)
154154+ }
155155+156156+ _, err = io.Copy(f, reader)
157157+ if err != nil {
158158+ t.Fatalf("writing file %q: %s", outputFile, err)
159159+ }
160160+161161+ f.Close()
162162+163163+ default:
164164+ t.Fatalf("encounted unknown file header type (%d) for file %q", header.Typeflag, header.Name)
165165+ }
166166+ }
167167+}
+48
internal/mountinfo/testdata/snapshot.sh
···11+#!/usr/bin/env bash
22+33+# Create a tarball of this system's sysfs filesystem + place it in the home directory.
44+#
55+# (This special logic is necessary since /sys is a pseudo-filesystem that exposes kernel variables.
66+# The files in /sys and their sizes will frequently change in between read()'s, which can break naive tar invocations.)
77+#
88+# Usage: ./snapshot.sh sysfs.tar.gz
99+1010+dst="$PWD/$1"
1111+tmp=$(mktemp -d -t sysfs_snapshot_XXXXXXX)
1212+1313+cleanup() {
1414+ rm -rf "$tmp"
1515+}
1616+trap cleanup EXIT
1717+1818+set -euxo pipefail
1919+2020+find /sys/devices/*/block /sys/dev/block /sys/class/block -print0 | sort -z | while IFS= read -d $'\0' -r file; do
2121+ # create the new file name by stripping the leading
2222+ # /sys and mashing it against the temp folder
2323+ temp_file="${tmp}/${file#*/sys/}"
2424+2525+ # create equivalent symlink
2626+ if [ -L "$file" ]; then
2727+ cp -d "$file" "$temp_file"
2828+ continue
2929+ fi
3030+3131+ # create necessary directories
3232+ if [ -d "$file" ]; then
3333+ mkdir -p "$temp_file"
3434+ continue
3535+ fi
3636+3737+ # skip over any files that we lack permissions to read,
3838+ # we encounter I/O errors when trying to read, or
3939+ # have some other weirdness
4040+ if ! wc -l "$file" >/dev/null 2>&1; then
4141+ continue
4242+ fi
4343+4444+ cp "$file" "$temp_file"
4545+done
4646+4747+cd "$tmp"
4848+tar vczf "$dst" .