Initial commit
This commit is contained in:
495
vendor/github.com/cockroachdb/pebble/vfs/disk_health.go
generated
vendored
Normal file
495
vendor/github.com/cockroachdb/pebble/vfs/disk_health.go
generated
vendored
Normal file
@@ -0,0 +1,495 @@
|
||||
// Copyright 2020 The LevelDB-Go and Pebble Authors. All rights reserved. Use
|
||||
// of this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
|
||||
package vfs
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// defaultTickInterval is the default interval between two ticks of each
|
||||
// diskHealthCheckingFile loop iteration.
|
||||
defaultTickInterval = 2 * time.Second
|
||||
// preallocatedSlotCount is the default number of slots available for
|
||||
// concurrent filesystem operations. The slot count may be exceeded, but
|
||||
// each additional slot will incur an additional allocation. We choose 16
|
||||
// here with the expectation that it is significantly more than required in
|
||||
// practice. See the comment above the diskHealthCheckingFS type definition.
|
||||
preallocatedSlotCount = 16
|
||||
)
|
||||
|
||||
// diskHealthCheckingFile is a File wrapper to detect slow disk operations, and
|
||||
// call onSlowDisk if a disk operation is seen to exceed diskSlowThreshold.
|
||||
//
|
||||
// This struct creates a goroutine (in startTicker()) that, at every tick
|
||||
// interval, sees if there's a disk operation taking longer than the specified
|
||||
// duration. This setup is preferable to creating a new timer at every disk
|
||||
// operation, as it reduces overhead per disk operation.
|
||||
type diskHealthCheckingFile struct {
|
||||
File
|
||||
|
||||
onSlowDisk func(time.Duration)
|
||||
diskSlowThreshold time.Duration
|
||||
tickInterval time.Duration
|
||||
|
||||
stopper chan struct{}
|
||||
lastWriteNanos int64
|
||||
}
|
||||
|
||||
// newDiskHealthCheckingFile instantiates a new diskHealthCheckingFile, with the
|
||||
// specified time threshold and event listener.
|
||||
func newDiskHealthCheckingFile(
|
||||
file File, diskSlowThreshold time.Duration, onSlowDisk func(time.Duration),
|
||||
) *diskHealthCheckingFile {
|
||||
return &diskHealthCheckingFile{
|
||||
File: file,
|
||||
onSlowDisk: onSlowDisk,
|
||||
diskSlowThreshold: diskSlowThreshold,
|
||||
tickInterval: defaultTickInterval,
|
||||
|
||||
stopper: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// startTicker starts a new goroutine with a ticker to monitor disk operations.
|
||||
// Can only be called if the ticker goroutine isn't running already.
|
||||
func (d *diskHealthCheckingFile) startTicker() {
|
||||
if d.diskSlowThreshold == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(d.tickInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-d.stopper:
|
||||
return
|
||||
|
||||
case <-ticker.C:
|
||||
lastWriteNanos := atomic.LoadInt64(&d.lastWriteNanos)
|
||||
if lastWriteNanos == 0 {
|
||||
continue
|
||||
}
|
||||
lastWrite := time.Unix(0, lastWriteNanos)
|
||||
now := time.Now()
|
||||
if lastWrite.Add(d.diskSlowThreshold).Before(now) {
|
||||
// diskSlowThreshold was exceeded. Call the passed-in
|
||||
// listener.
|
||||
d.onSlowDisk(now.Sub(lastWrite))
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// stopTicker stops the goroutine started in startTicker.
|
||||
func (d *diskHealthCheckingFile) stopTicker() {
|
||||
close(d.stopper)
|
||||
}
|
||||
|
||||
// Write implements the io.Writer interface.
|
||||
func (d *diskHealthCheckingFile) Write(p []byte) (n int, err error) {
|
||||
d.timeDiskOp(func() {
|
||||
n, err = d.File.Write(p)
|
||||
})
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Close implements the io.Closer interface.
|
||||
func (d *diskHealthCheckingFile) Close() error {
|
||||
d.stopTicker()
|
||||
return d.File.Close()
|
||||
}
|
||||
|
||||
// Sync implements the io.Syncer interface.
|
||||
func (d *diskHealthCheckingFile) Sync() (err error) {
|
||||
d.timeDiskOp(func() {
|
||||
err = d.File.Sync()
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// timeDiskOp runs the specified closure and makes its timing visible to the
|
||||
// monitoring goroutine, in case it exceeds one of the slow disk durations.
|
||||
func (d *diskHealthCheckingFile) timeDiskOp(op func()) {
|
||||
if d == nil {
|
||||
op()
|
||||
return
|
||||
}
|
||||
|
||||
atomic.StoreInt64(&d.lastWriteNanos, time.Now().UnixNano())
|
||||
defer func() {
|
||||
atomic.StoreInt64(&d.lastWriteNanos, 0)
|
||||
}()
|
||||
op()
|
||||
}
|
||||
|
||||
// diskHealthCheckingFS adds disk-health checking facilities to a VFS.
|
||||
// It times disk write operations in two ways:
|
||||
//
|
||||
// 1. Wrapping vfs.Files.
|
||||
//
|
||||
// The bulk of write I/O activity is file writing and syncing, invoked through
|
||||
// the `vfs.File` interface. This VFS wraps all files open for writing with a
|
||||
// special diskHealthCheckingFile implementation of the vfs.File interface. See
|
||||
// above for the implementation.
|
||||
//
|
||||
// 2. Monitoring filesystem metadata operations.
|
||||
//
|
||||
// Filesystem metadata operations (create, link, remove, rename, etc) are also
|
||||
// sources of disk writes. Unlike a vfs.File which requires Write and Sync calls
|
||||
// to be sequential, a vfs.FS may receive these filesystem metadata operations
|
||||
// in parallel. To accommodate this parallelism, the diskHealthCheckingFS's
|
||||
// write-oriented filesystem operations record their start times into a 'slot'
|
||||
// on the filesystem. A single long-running goroutine periodically scans the
|
||||
// slots looking for slow operations.
|
||||
//
|
||||
// The number of slots on a diskHealthCheckingFS grows to a working set of the
|
||||
// maximum concurrent filesystem operations. This is expected to be very few
|
||||
// for these reasons:
|
||||
// 1. Pebble has limited write concurrency. Flushes, compactions and WAL
|
||||
// rotations are the primary sources of filesystem metadata operations. With
|
||||
// the default max-compaction concurrency, these operations require at most 5
|
||||
// concurrent slots if all 5 perform a filesystem metadata operation
|
||||
// simultaneously.
|
||||
// 2. Pebble's limited concurrent I/O writers spend most of their time
|
||||
// performing file I/O, not performing the filesystem metadata operations that
|
||||
// require recording a slot on the diskHealthCheckingFS.
|
||||
// 3. In CockroachDB, each additional store/Pebble instance has its own vfs.FS
|
||||
// which provides a separate goroutine and set of slots.
|
||||
// 4. In CockroachDB, many of the additional sources of filesystem metadata
|
||||
// operations (like encryption-at-rest) are sequential with respect to Pebble's
|
||||
// threads.
|
||||
type diskHealthCheckingFS struct {
|
||||
tickInterval time.Duration
|
||||
diskSlowThreshold time.Duration
|
||||
onSlowDisk func(string, time.Duration)
|
||||
fs FS
|
||||
mu struct {
|
||||
sync.Mutex
|
||||
tickerRunning bool
|
||||
stopper chan struct{}
|
||||
inflight []*slot
|
||||
}
|
||||
// prealloc preallocates the memory for mu.inflight slots and the slice
|
||||
// itself. The contained fields are not accessed directly except by
|
||||
// WithDiskHealthChecks when initializing mu.inflight. The number of slots
|
||||
// in d.mu.inflight will grow to the maximum number of concurrent file
|
||||
// metadata operations (create, remove, link, etc). If the number of
|
||||
// concurrent operations never exceeds preallocatedSlotCount, we'll never
|
||||
// incur an additional allocation.
|
||||
prealloc struct {
|
||||
slots [preallocatedSlotCount]slot
|
||||
slotPtrSlice [preallocatedSlotCount]*slot
|
||||
}
|
||||
}
|
||||
|
||||
type slot struct {
|
||||
name string
|
||||
startNanos int64
|
||||
}
|
||||
|
||||
// diskHealthCheckingFS implements FS.
|
||||
var _ FS = (*diskHealthCheckingFS)(nil)
|
||||
|
||||
// WithDiskHealthChecks wraps an FS and ensures that all write-oriented
|
||||
// operations on the FS are wrapped with disk health detection checks. Disk
|
||||
// operations that are observed to take longer than diskSlowThreshold trigger an
|
||||
// onSlowDisk call.
|
||||
//
|
||||
// A threshold of zero disables disk-health checking.
|
||||
func WithDiskHealthChecks(
|
||||
innerFS FS, diskSlowThreshold time.Duration, onSlowDisk func(string, time.Duration),
|
||||
) (FS, io.Closer) {
|
||||
if diskSlowThreshold == 0 {
|
||||
return innerFS, noopCloser{}
|
||||
}
|
||||
|
||||
fs := &diskHealthCheckingFS{
|
||||
fs: innerFS,
|
||||
tickInterval: defaultTickInterval,
|
||||
diskSlowThreshold: diskSlowThreshold,
|
||||
onSlowDisk: onSlowDisk,
|
||||
}
|
||||
fs.mu.stopper = make(chan struct{})
|
||||
// The fs holds preallocated slots and a preallocated array of slot pointers
|
||||
// with equal length. Initialize the inflight slice to use a slice backed by
|
||||
// the preallocated array with each slot initialized to a preallocated slot.
|
||||
fs.mu.inflight = fs.prealloc.slotPtrSlice[:]
|
||||
for i := range fs.mu.inflight {
|
||||
fs.mu.inflight[i] = &fs.prealloc.slots[i]
|
||||
}
|
||||
return fs, fs
|
||||
}
|
||||
|
||||
func (d *diskHealthCheckingFS) timeFilesystemOp(name string, op func()) {
|
||||
if d == nil {
|
||||
op()
|
||||
return
|
||||
}
|
||||
|
||||
// Record this operation's start time on the FS, so that the long-running
|
||||
// goroutine can monitor the filesystem operation.
|
||||
//
|
||||
// The diskHealthCheckingFile implementation uses a single field that is
|
||||
// atomically updated, taking advantage of the fact that writes to a single
|
||||
// vfs.File handle are not performed in parallel. The vfs.FS however may
|
||||
// receive write filesystem operations in parallel. To accommodate this
|
||||
// parallelism, writing goroutines append their start time to a
|
||||
// mutex-protected vector. On ticks, the long-running goroutine scans the
|
||||
// vector searching for start times older than the slow-disk threshold. When
|
||||
// a writing goroutine completes its operation, it atomically overwrites its
|
||||
// slot to signal completion.
|
||||
var s *slot
|
||||
func() {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
// If there's no long-running goroutine to monitor this filesystem
|
||||
// operation, start one.
|
||||
if !d.mu.tickerRunning {
|
||||
d.startTickerLocked()
|
||||
}
|
||||
|
||||
startNanos := time.Now().UnixNano()
|
||||
for i := 0; i < len(d.mu.inflight); i++ {
|
||||
if atomic.LoadInt64(&d.mu.inflight[i].startNanos) == 0 {
|
||||
// This slot is not in use. Claim it.
|
||||
s = d.mu.inflight[i]
|
||||
s.name = name
|
||||
atomic.StoreInt64(&s.startNanos, startNanos)
|
||||
break
|
||||
}
|
||||
}
|
||||
// If we didn't find any unused slots, create a new slot and append it.
|
||||
// This slot will exist forever. The number of slots will grow to the
|
||||
// maximum number of concurrent filesystem operations over the lifetime
|
||||
// of the process. Only operations that grow the number of slots must
|
||||
// incur an allocation.
|
||||
if s == nil {
|
||||
s = &slot{
|
||||
name: name,
|
||||
startNanos: startNanos,
|
||||
}
|
||||
d.mu.inflight = append(d.mu.inflight, s)
|
||||
}
|
||||
}()
|
||||
|
||||
op()
|
||||
|
||||
// Signal completion by zeroing the start time.
|
||||
atomic.StoreInt64(&s.startNanos, 0)
|
||||
}
|
||||
|
||||
// startTickerLocked starts a new goroutine with a ticker to monitor disk
|
||||
// filesystem operations. Requires d.mu and !d.mu.tickerRunning.
|
||||
func (d *diskHealthCheckingFS) startTickerLocked() {
|
||||
d.mu.tickerRunning = true
|
||||
stopper := d.mu.stopper
|
||||
go func() {
|
||||
ticker := time.NewTicker(d.tickInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
// Scan the inflight slots for any slots recording a start
|
||||
// time older than the diskSlowThreshold.
|
||||
d.mu.Lock()
|
||||
now := time.Now()
|
||||
for i := range d.mu.inflight {
|
||||
nanos := atomic.LoadInt64(&d.mu.inflight[i].startNanos)
|
||||
if nanos != 0 && time.Unix(0, nanos).Add(d.diskSlowThreshold).Before(now) {
|
||||
// diskSlowThreshold was exceeded. Invoke the provided
|
||||
// callback.
|
||||
d.onSlowDisk(d.mu.inflight[i].name, now.Sub(time.Unix(0, nanos)))
|
||||
}
|
||||
}
|
||||
d.mu.Unlock()
|
||||
case <-stopper:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Close implements io.Closer. Close stops the long-running goroutine that
|
||||
// monitors for slow filesystem metadata operations. Close may be called
|
||||
// multiple times. If the filesystem is used after Close has been called, a new
|
||||
// long-running goroutine will be created.
|
||||
func (d *diskHealthCheckingFS) Close() error {
|
||||
d.mu.Lock()
|
||||
if !d.mu.tickerRunning {
|
||||
// Nothing to stop.
|
||||
d.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Grab the stopper so we can request the long-running goroutine to stop.
|
||||
// Replace the stopper in case this FS is reused. It's possible to Close and
|
||||
// reuse a disk-health checking FS. This is to accommodate the on-by-default
|
||||
// behavior in Pebble, and the possibility that users may continue to use
|
||||
// the Pebble default FS beyond the lifetime of a single DB.
|
||||
stopper := d.mu.stopper
|
||||
d.mu.stopper = make(chan struct{})
|
||||
d.mu.tickerRunning = false
|
||||
d.mu.Unlock()
|
||||
|
||||
// Ask the long-running goroutine to stop. This is a synchronous channel
|
||||
// send.
|
||||
stopper <- struct{}{}
|
||||
close(stopper)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create implements the FS interface.
|
||||
func (d *diskHealthCheckingFS) Create(name string) (File, error) {
|
||||
var f File
|
||||
var err error
|
||||
d.timeFilesystemOp(name, func() {
|
||||
f, err = d.fs.Create(name)
|
||||
})
|
||||
if err != nil {
|
||||
return f, err
|
||||
}
|
||||
if d.diskSlowThreshold == 0 {
|
||||
return f, nil
|
||||
}
|
||||
checkingFile := newDiskHealthCheckingFile(f, d.diskSlowThreshold, func(duration time.Duration) {
|
||||
d.onSlowDisk(name, duration)
|
||||
})
|
||||
checkingFile.startTicker()
|
||||
return WithFd(f, checkingFile), nil
|
||||
}
|
||||
|
||||
// GetDiskUsage implements the FS interface.
|
||||
func (d *diskHealthCheckingFS) GetDiskUsage(path string) (DiskUsage, error) {
|
||||
return d.fs.GetDiskUsage(path)
|
||||
}
|
||||
|
||||
// Link implements the FS interface.
|
||||
func (d *diskHealthCheckingFS) Link(oldname, newname string) error {
|
||||
var err error
|
||||
d.timeFilesystemOp(newname, func() {
|
||||
err = d.fs.Link(oldname, newname)
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// List implements the FS interface.
|
||||
func (d *diskHealthCheckingFS) List(dir string) ([]string, error) {
|
||||
return d.fs.List(dir)
|
||||
}
|
||||
|
||||
// Lock implements the FS interface.
|
||||
func (d *diskHealthCheckingFS) Lock(name string) (io.Closer, error) {
|
||||
return d.fs.Lock(name)
|
||||
}
|
||||
|
||||
// MkdirAll implements the FS interface.
|
||||
func (d *diskHealthCheckingFS) MkdirAll(dir string, perm os.FileMode) error {
|
||||
var err error
|
||||
d.timeFilesystemOp(dir, func() {
|
||||
err = d.fs.MkdirAll(dir, perm)
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// Open implements the FS interface.
|
||||
func (d *diskHealthCheckingFS) Open(name string, opts ...OpenOption) (File, error) {
|
||||
return d.fs.Open(name, opts...)
|
||||
}
|
||||
|
||||
// OpenDir implements the FS interface.
|
||||
func (d *diskHealthCheckingFS) OpenDir(name string) (File, error) {
|
||||
f, err := d.fs.OpenDir(name)
|
||||
if err != nil {
|
||||
return f, err
|
||||
}
|
||||
// Directories opened with OpenDir must be opened with health checking,
|
||||
// because they may be explicitly synced.
|
||||
checkingFile := newDiskHealthCheckingFile(f, d.diskSlowThreshold, func(duration time.Duration) {
|
||||
d.onSlowDisk(name, duration)
|
||||
})
|
||||
checkingFile.startTicker()
|
||||
return WithFd(f, checkingFile), nil
|
||||
}
|
||||
|
||||
// PathBase implements the FS interface.
|
||||
func (d *diskHealthCheckingFS) PathBase(path string) string {
|
||||
return d.fs.PathBase(path)
|
||||
}
|
||||
|
||||
// PathJoin implements the FS interface.
|
||||
func (d *diskHealthCheckingFS) PathJoin(elem ...string) string {
|
||||
return d.fs.PathJoin(elem...)
|
||||
}
|
||||
|
||||
// PathDir implements the FS interface.
|
||||
func (d *diskHealthCheckingFS) PathDir(path string) string {
|
||||
return d.fs.PathDir(path)
|
||||
}
|
||||
|
||||
// Remove implements the FS interface.
|
||||
func (d *diskHealthCheckingFS) Remove(name string) error {
|
||||
var err error
|
||||
d.timeFilesystemOp(name, func() {
|
||||
err = d.fs.Remove(name)
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// RemoveAll implements the FS interface.
|
||||
func (d *diskHealthCheckingFS) RemoveAll(name string) error {
|
||||
var err error
|
||||
d.timeFilesystemOp(name, func() {
|
||||
err = d.fs.RemoveAll(name)
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// Rename implements the FS interface.
|
||||
func (d *diskHealthCheckingFS) Rename(oldname, newname string) error {
|
||||
var err error
|
||||
d.timeFilesystemOp(newname, func() {
|
||||
err = d.fs.Rename(oldname, newname)
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// ReuseForWrite implements the FS interface.
|
||||
func (d *diskHealthCheckingFS) ReuseForWrite(oldname, newname string) (File, error) {
|
||||
var f File
|
||||
var err error
|
||||
d.timeFilesystemOp(newname, func() {
|
||||
f, err = d.fs.ReuseForWrite(oldname, newname)
|
||||
})
|
||||
if err != nil {
|
||||
return f, err
|
||||
}
|
||||
if d.diskSlowThreshold == 0 {
|
||||
return f, nil
|
||||
}
|
||||
checkingFile := newDiskHealthCheckingFile(f, d.diskSlowThreshold, func(duration time.Duration) {
|
||||
d.onSlowDisk(newname, duration)
|
||||
})
|
||||
checkingFile.startTicker()
|
||||
return WithFd(f, checkingFile), nil
|
||||
}
|
||||
|
||||
// Stat implements the FS interface.
|
||||
func (d *diskHealthCheckingFS) Stat(name string) (os.FileInfo, error) {
|
||||
return d.fs.Stat(name)
|
||||
}
|
||||
|
||||
type noopCloser struct{}
|
||||
|
||||
func (noopCloser) Close() error { return nil }
|
||||
Reference in New Issue
Block a user