Skip to content

Commit 79af676

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

File tree

4 files changed

+69
-110
lines changed

4 files changed

+69
-110
lines changed

container/common/fsHandler.go

Lines changed: 12 additions & 15 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
}

fs/fs.go

Lines changed: 46 additions & 84 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,26 +45,6 @@ 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
52-
53-
// A pool for restricting the number of consecutive `du` and `find` tasks running.
54-
var pool = make(chan struct{}, maxConcurrentOps)
55-
56-
func init() {
57-
for i := 0; i < maxConcurrentOps; i++ {
58-
releaseToken()
59-
}
60-
}
61-
62-
func claimToken() {
63-
<-pool
64-
}
65-
66-
func releaseToken() {
67-
pool <- struct{}{}
68-
}
69-
7048
type partition struct {
7149
mountpoint string
7250
major uint
@@ -559,78 +537,62 @@ func (self *RealFsInfo) GetDirFsDevice(dir string) (*DeviceInfo, error) {
559537
return nil, fmt.Errorf("could not find device with major: %d, minor: %d in cached partitions map", major, minor)
560538
}
561539

562-
func (self *RealFsInfo) GetDirDiskUsage(dir string, timeout time.Duration) (uint64, error) {
563-
claimToken()
564-
defer releaseToken()
565-
return GetDirDiskUsage(dir, timeout)
566-
}
540+
func (self *RealFsInfo) GetDirUsage(dir string) (UsageInfo, error) {
541+
var usage UsageInfo
567542

568-
func GetDirDiskUsage(dir string, timeout time.Duration) (uint64, error) {
569543
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)
544+
return usage, fmt.Errorf("invalid directory")
580545
}
581546

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()
547+
rootInfo, err := os.Stat(dir)
596548
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)
549+
return usage, fmt.Errorf("could not stat %q to get inode usage: %v", dir, err)
598550
}
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)
551+
552+
rootStat, ok := rootInfo.Sys().(*syscall.Stat_t)
553+
if !ok {
554+
return usage, fmt.Errorf("unsuported fileinfo for getting inode usage of %q", dir)
603555
}
604-
return usageInKb * 1024, nil
605-
}
606556

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

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()
559+
// dedupedInode stores inodes that could be duplicates (nlink > 1)
560+
dedupedInodes := make(map[uint64]struct{})
561+
562+
err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
563+
if os.IsNotExist(err) {
564+
// expected if files appear/vanish
565+
return nil
566+
}
567+
if err != nil {
568+
return fmt.Errorf("unable to count inodes for part of dir %s: %s", dir, err)
569+
}
570+
571+
s, ok := info.Sys().(*syscall.Stat_t)
572+
if !ok {
573+
return fmt.Errorf("unsupported fileinfo; could not convert to stat_t")
574+
}
575+
576+
if s.Dev != rootDevId {
577+
// don't descend into directories on other devices
578+
return filepath.SkipDir
579+
}
580+
if s.Nlink > 1 {
581+
if _, ok := dedupedInodes[s.Ino]; !ok {
582+
// Dedupe things that could be hardlinks
583+
dedupedInodes[s.Ino] = struct{}{}
584+
585+
usage.Bytes += uint64(s.Blocks) * uint64(512)
586+
usage.Inodes++
587+
}
588+
} else {
589+
usage.Bytes += uint64(s.Blocks) * uint64(512)
590+
usage.Inodes++
591+
}
592+
return nil
627593
})
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
594+
595+
return usage, nil
634596
}
635597

636598
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)