Skip to content

Commit 353322f

Browse files
author
Igor German
committed
fs: get inodes and disk usage via pure go
1 parent cd92744 commit 353322f

File tree

4 files changed

+82
-91
lines changed

4 files changed

+82
-91
lines changed

container/common/fsHandler.go

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ type realFsHandler struct {
5151
}
5252

5353
const (
54-
timeout = 2 * time.Minute
5554
maxBackoffFactor = 20
5655
)
5756

@@ -74,36 +73,34 @@ func NewFsHandler(period time.Duration, rootfs, extraDir string, fsInfo fs.FsInf
7473

7574
func (fh *realFsHandler) update() error {
7675
var (
77-
baseUsage, extraDirUsage, inodeUsage uint64
78-
rootDiskErr, rootInodeErr, extraDiskErr error
76+
rootUsage, extraUsage fs.UsageInfo
77+
rootErr, extraErr error
7978
)
8079
// TODO(vishh): Add support for external mounts.
8180
if fh.rootfs != "" {
82-
baseUsage, rootDiskErr = fh.fsInfo.GetDirDiskUsage(fh.rootfs, timeout)
83-
inodeUsage, rootInodeErr = fh.fsInfo.GetDirInodeUsage(fh.rootfs, timeout)
81+
rootUsage, rootErr = fh.fsInfo.GetDirUsage(fh.rootfs)
8482
}
8583

8684
if fh.extraDir != "" {
87-
extraDirUsage, extraDiskErr = fh.fsInfo.GetDirDiskUsage(fh.extraDir, timeout)
85+
extraUsage, extraErr = fh.fsInfo.GetDirUsage(fh.extraDir)
8886
}
8987

9088
// Wait to handle errors until after all operartions are run.
9189
// An error in one will not cause an early return, skipping others
9290
fh.Lock()
9391
defer fh.Unlock()
9492
fh.lastUpdate = time.Now()
95-
if rootInodeErr == nil && fh.rootfs != "" {
96-
fh.usage.InodeUsage = inodeUsage
93+
if fh.rootfs != "" && rootErr == nil {
94+
fh.usage.InodeUsage = rootUsage.Inodes
95+
fh.usage.TotalUsageBytes = rootUsage.Bytes + extraUsage.Bytes
9796
}
98-
if rootDiskErr == nil && fh.rootfs != "" {
99-
fh.usage.TotalUsageBytes = baseUsage + extraDirUsage
100-
}
101-
if extraDiskErr == nil && fh.extraDir != "" {
102-
fh.usage.BaseUsageBytes = baseUsage
97+
if fh.extraDir != "" && extraErr == nil {
98+
fh.usage.BaseUsageBytes = rootUsage.Bytes
10399
}
100+
104101
// Combine errors into a single error to return
105-
if rootDiskErr != nil || rootInodeErr != nil || extraDiskErr != nil {
106-
return fmt.Errorf("rootDiskErr: %v, rootInodeErr: %v, extraDiskErr: %v", rootDiskErr, rootInodeErr, extraDiskErr)
102+
if rootErr != nil || extraErr != nil {
103+
return fmt.Errorf("rootDiskErr: %v, extraDiskErr: %v", rootErr, extraErr)
107104
}
108105
return nil
109106
}
@@ -132,7 +129,7 @@ func (fh *realFsHandler) trackUsage() {
132129
// if the long duration is persistent either because of slow
133130
// disk or lots of containers.
134131
longOp = longOp + time.Second
135-
klog.V(2).Infof("du and find on following dirs took %v: %v; will not log again for this container unless duration exceeds %v", duration, []string{fh.rootfs, fh.extraDir}, longOp)
132+
klog.V(2).Infof("fs: disk usage and inodes count on following dirs took %v: %v; will not log again for this container unless duration exceeds %v", duration, []string{fh.rootfs, fh.extraDir}, longOp)
136133
}
137134
}
138135
}

fs/fs.go

Lines changed: 58 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ package fs
1919

2020
import (
2121
"bufio"
22-
"bytes"
2322
"fmt"
2423
"io/ioutil"
2524
"os"
@@ -30,7 +29,6 @@ import (
3029
"strconv"
3130
"strings"
3231
"syscall"
33-
"time"
3432

3533
"github.com/docker/docker/pkg/mount"
3634
"github.com/google/cadvisor/devicemapper"
@@ -47,8 +45,12 @@ const (
4745
LabelCrioImages = "crio-images"
4846
)
4947

50-
// The maximum number of `du` and `find` tasks that can be running at once.
51-
const maxConcurrentOps = 20
48+
const (
49+
// The block size in bytes.
50+
statBlockSize uint64 = 512
51+
// The maximum number of `disk usage` tasks that can be running at once.
52+
maxConcurrentOps = 20
53+
)
5254

5355
// A pool for restricting the number of consecutive `du` and `find` tasks running.
5456
var pool = make(chan struct{}, maxConcurrentOps)
@@ -559,78 +561,70 @@ func (self *RealFsInfo) GetDirFsDevice(dir string) (*DeviceInfo, error) {
559561
return nil, fmt.Errorf("could not find device with major: %d, minor: %d in cached partitions map", major, minor)
560562
}
561563

562-
func (self *RealFsInfo) GetDirDiskUsage(dir string, timeout time.Duration) (uint64, error) {
564+
func (self *RealFsInfo) GetDirUsage(dir string) (UsageInfo, error) {
563565
claimToken()
564566
defer releaseToken()
565-
return GetDirDiskUsage(dir, timeout)
566-
}
567567

568-
func GetDirDiskUsage(dir string, timeout time.Duration) (uint64, error) {
568+
var usage UsageInfo
569+
569570
if dir == "" {
570-
return 0, fmt.Errorf("invalid directory")
571-
}
572-
cmd := exec.Command("ionice", "-c3", "nice", "-n", "19", "du", "-s", dir)
573-
stdoutp, err := cmd.StdoutPipe()
574-
if err != nil {
575-
return 0, fmt.Errorf("failed to setup stdout for cmd %v - %v", cmd.Args, err)
576-
}
577-
stderrp, err := cmd.StderrPipe()
578-
if err != nil {
579-
return 0, fmt.Errorf("failed to setup stderr for cmd %v - %v", cmd.Args, err)
571+
return usage, fmt.Errorf("invalid directory")
580572
}
581573

582-
if err := cmd.Start(); err != nil {
583-
return 0, fmt.Errorf("failed to exec du - %v", err)
584-
}
585-
timer := time.AfterFunc(timeout, func() {
586-
klog.Warningf("Killing cmd %v due to timeout(%s)", cmd.Args, timeout.String())
587-
cmd.Process.Kill()
588-
})
589-
stdoutb, souterr := ioutil.ReadAll(stdoutp)
590-
if souterr != nil {
591-
klog.Errorf("Failed to read from stdout for cmd %v - %v", cmd.Args, souterr)
592-
}
593-
stderrb, _ := ioutil.ReadAll(stderrp)
594-
err = cmd.Wait()
595-
timer.Stop()
574+
rootInfo, err := os.Stat(dir)
596575
if err != nil {
597-
return 0, fmt.Errorf("du command failed on %s with output stdout: %s, stderr: %s - %v", dir, string(stdoutb), string(stderrb), err)
576+
return usage, fmt.Errorf("could not stat %q to get inode usage: %v", dir, err)
598577
}
599-
stdout := string(stdoutb)
600-
usageInKb, err := strconv.ParseUint(strings.Fields(stdout)[0], 10, 64)
601-
if err != nil {
602-
return 0, fmt.Errorf("cannot parse 'du' output %s - %s", stdout, err)
578+
579+
rootStat, ok := rootInfo.Sys().(*syscall.Stat_t)
580+
if !ok {
581+
return usage, fmt.Errorf("unsuported fileinfo for getting inode usage of %q", dir)
603582
}
604-
return usageInKb * 1024, nil
605-
}
606583

607-
func (self *RealFsInfo) GetDirInodeUsage(dir string, timeout time.Duration) (uint64, error) {
608-
claimToken()
609-
defer releaseToken()
610-
return GetDirInodeUsage(dir, timeout)
611-
}
584+
rootDevId := rootStat.Dev
612585

613-
func GetDirInodeUsage(dir string, timeout time.Duration) (uint64, error) {
614-
if dir == "" {
615-
return 0, fmt.Errorf("invalid directory")
616-
}
617-
var counter byteCounter
618-
var stderr bytes.Buffer
619-
findCmd := exec.Command("ionice", "-c3", "nice", "-n", "19", "find", dir, "-xdev", "-printf", ".")
620-
findCmd.Stdout, findCmd.Stderr = &counter, &stderr
621-
if err := findCmd.Start(); err != nil {
622-
return 0, fmt.Errorf("failed to exec cmd %v - %v; stderr: %v", findCmd.Args, err, stderr.String())
623-
}
624-
timer := time.AfterFunc(timeout, func() {
625-
klog.Warningf("Killing cmd %v due to timeout(%s)", findCmd.Args, timeout.String())
626-
findCmd.Process.Kill()
586+
// dedupedInode stores inodes that could be duplicates (nlink > 1)
587+
dedupedInodes := make(map[uint64]struct{})
588+
589+
err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
590+
if os.IsNotExist(err) {
591+
// expected if files appear/vanish
592+
return nil
593+
}
594+
if err != nil {
595+
return fmt.Errorf("unable to count inodes for part of dir %s: %s", dir, err)
596+
}
597+
598+
// according to the docs, Sys can be nil
599+
if info.Sys() == nil {
600+
return fmt.Errorf("fileinfo Sys is nil")
601+
}
602+
603+
s, ok := info.Sys().(*syscall.Stat_t)
604+
if !ok {
605+
return fmt.Errorf("unsupported fileinfo; could not convert to stat_t")
606+
}
607+
608+
if s.Dev != rootDevId {
609+
// don't descend into directories on other devices
610+
return filepath.SkipDir
611+
}
612+
if s.Nlink > 1 {
613+
if _, ok := dedupedInodes[s.Ino]; !ok {
614+
// Dedupe things that could be hardlinks
615+
dedupedInodes[s.Ino] = struct{}{}
616+
617+
usage.Bytes += uint64(s.Blocks) * statBlockSize
618+
usage.Inodes++
619+
}
620+
} else {
621+
usage.Bytes += uint64(s.Blocks) * statBlockSize
622+
usage.Inodes++
623+
}
624+
return nil
627625
})
628-
err := findCmd.Wait()
629-
timer.Stop()
630-
if err != nil {
631-
return 0, fmt.Errorf("cmd %v failed. stderr: %s; err: %v", findCmd.Args, stderr.String(), err)
632-
}
633-
return counter.bytesWritten, nil
626+
627+
return usage, nil
634628
}
635629

636630
func getVfsStats(path string) (total uint64, free uint64, avail uint64, inodes uint64, inodesFree uint64, err error) {

fs/fs_test.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import (
2020
"os"
2121
"reflect"
2222
"testing"
23-
"time"
2423

2524
"github.com/docker/docker/pkg/mount"
2625
"github.com/stretchr/testify/assert"
@@ -101,9 +100,9 @@ func TestDirDiskUsage(t *testing.T) {
101100
fi, err := f.Stat()
102101
as.NoError(err)
103102
expectedSize := uint64(fi.Size())
104-
size, err := fsInfo.GetDirDiskUsage(dir, time.Minute)
103+
usage, err := fsInfo.GetDirUsage(dir)
105104
as.NoError(err)
106-
as.True(expectedSize <= size, "expected dir size to be at-least %d; got size: %d", expectedSize, size)
105+
as.True(expectedSize <= usage.Bytes, "expected dir size to be at-least %d; got size: %d", expectedSize, usage.Bytes)
107106
}
108107

109108
func TestDirInodeUsage(t *testing.T) {
@@ -118,10 +117,10 @@ func TestDirInodeUsage(t *testing.T) {
118117
_, err := ioutil.TempFile(dir, "")
119118
require.NoError(t, err)
120119
}
121-
inodes, err := fsInfo.GetDirInodeUsage(dir, time.Minute)
120+
usage, err := fsInfo.GetDirUsage(dir)
122121
as.NoError(err)
123122
// We sould get numFiles+1 inodes, since we get 1 inode for each file, plus 1 for the directory
124-
as.True(uint64(numFiles+1) == inodes, "expected inodes in dir to be %d; got inodes: %d", numFiles+1, inodes)
123+
as.True(uint64(numFiles+1) == usage.Inodes, "expected inodes in dir to be %d; got inodes: %d", numFiles+1, usage.Inodes)
125124
}
126125

127126
var dmStatusTests = []struct {

fs/types.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ package fs
1616

1717
import (
1818
"errors"
19-
"time"
2019
)
2120

2221
type DeviceInfo struct {
@@ -62,6 +61,11 @@ type DiskStats struct {
6261
WeightedIoTime uint64
6362
}
6463

64+
type UsageInfo struct {
65+
Bytes uint64
66+
Inodes uint64
67+
}
68+
6569
// ErrNoSuchDevice is the error indicating the requested device does not exist.
6670
var ErrNoSuchDevice = errors.New("cadvisor: no such device")
6771

@@ -72,11 +76,8 @@ type FsInfo interface {
7276
// Returns capacity and free space, in bytes, of the set of mounts passed.
7377
GetFsInfoForPath(mountSet map[string]struct{}) ([]Fs, error)
7478

75-
// Returns number of bytes occupied by 'dir'.
76-
GetDirDiskUsage(dir string, timeout time.Duration) (uint64, error)
77-
78-
// Returns number of inodes used by 'dir'.
79-
GetDirInodeUsage(dir string, timeout time.Duration) (uint64, error)
79+
// GetDirUsage returns a usage information for 'dir'.
80+
GetDirUsage(dir string) (UsageInfo, error)
8081

8182
// GetDeviceInfoByFsUUID returns the information of the device with the
8283
// specified filesystem uuid. If no such device exists, this function will

0 commit comments

Comments
 (0)