github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/cli/systembench/disk_bench.go (about)

     1  // Copyright 2018 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  package systembench
    12  
    13  import (
    14  	"context"
    15  	"fmt"
    16  	"io/ioutil"
    17  	"math/rand"
    18  	"os"
    19  	"os/signal"
    20  	"sync/atomic"
    21  	"time"
    22  
    23  	"github.com/cockroachdb/cockroach/pkg/util/log"
    24  	"github.com/cockroachdb/cockroach/pkg/util/sysutil"
    25  	"github.com/cockroachdb/cockroach/pkg/util/timeutil"
    26  	"github.com/cockroachdb/errors"
    27  	"golang.org/x/sync/errgroup"
    28  )
    29  
    30  const (
    31  	maxFileSize = 1 << 30 // A gigabyte
    32  )
    33  
    34  // DiskBenchmarkType represents an I/O benchmark.
    35  type DiskBenchmarkType int
    36  
    37  const (
    38  	// SeqWriteTest identifies a sequential write I/O
    39  	// benchmark.
    40  	SeqWriteTest DiskBenchmarkType = iota
    41  )
    42  
    43  // DiskOptions holds parameters for the test.
    44  type DiskOptions struct {
    45  	Dir          string
    46  	Concurrency  int
    47  	Duration     time.Duration
    48  	WriteSize    int64
    49  	SyncInterval int64
    50  
    51  	Type DiskBenchmarkType
    52  }
    53  
    54  // workerSeqWrite holds a temp file and random byte array to write to
    55  // the temp file.
    56  type workerSeqWrite struct {
    57  	latency *namedHistogram
    58  	data    []byte
    59  
    60  	tempFileDir  string
    61  	syncInterval int64
    62  }
    63  
    64  // newWorkerSeqWrite creates a worker that writes writeSize sized
    65  // sequential blocks with fsync every syncInterval bytes to a file.
    66  func newWorkerSeqWrite(
    67  	ctx context.Context, diskOptions *DiskOptions, registry *histogramRegistry,
    68  ) worker {
    69  	data := make([]byte, diskOptions.WriteSize)
    70  	_, err := rand.Read(data)
    71  	if err != nil {
    72  		log.Fatalf(ctx, "Failed to fill byte array with random bytes %s", err)
    73  	}
    74  
    75  	w := &workerSeqWrite{data: data,
    76  		tempFileDir:  diskOptions.Dir,
    77  		syncInterval: diskOptions.SyncInterval,
    78  	}
    79  	w.latency = registry.Register("ops")
    80  	return w
    81  }
    82  
    83  // workerSeqWrite implements the worker interface.
    84  func (w *workerSeqWrite) run(ctx context.Context) error {
    85  	writtenPreSync := int64(0)
    86  	writtenInFile := int64(0)
    87  
    88  	tempFile, err := newTempFile(w.tempFileDir)
    89  	if err != nil {
    90  		return err
    91  	}
    92  
    93  	writeSize := int64(len(w.data))
    94  	defer func() {
    95  		_ = os.Remove(tempFile.Name())
    96  	}()
    97  
    98  	for {
    99  		if ctx.Err() != nil {
   100  			return ctx.Err()
   101  		}
   102  		start := timeutil.Now()
   103  		if _, err := tempFile.Write(w.data); err != nil {
   104  			tempFile.Close()
   105  			return err
   106  		}
   107  
   108  		writtenPreSync += writeSize
   109  		writtenInFile += writeSize
   110  
   111  		if writtenPreSync >= w.syncInterval {
   112  			if err := tempFile.Sync(); err != nil {
   113  				return err
   114  			}
   115  			writtenPreSync = int64(0)
   116  		}
   117  
   118  		if writtenInFile >= maxFileSize {
   119  			if err := tempFile.Truncate(0); err != nil {
   120  				return err
   121  			}
   122  
   123  			if _, err := tempFile.Seek(0, 0); err != nil {
   124  				return err
   125  			}
   126  		}
   127  
   128  		bytes := uint64(writeSize)
   129  		atomic.AddUint64(&numOps, 1)
   130  		atomic.AddUint64(&numBytes, bytes)
   131  		w.latency.Record(timeutil.Since(start))
   132  	}
   133  }
   134  
   135  // workerSeqWrite implements the worker interface.
   136  func (w *workerSeqWrite) getLatencyHistogram() *namedHistogram {
   137  	return w.latency
   138  }
   139  
   140  // newTempFile creates a temporary file, closes it and
   141  // reopens in append mode.
   142  func newTempFile(dir string) (*os.File, error) {
   143  	tempFile, err := ioutil.TempFile(dir, "")
   144  	if err != nil {
   145  		return nil, err
   146  	}
   147  
   148  	tempFileName := tempFile.Name()
   149  	if err := tempFile.Close(); err != nil {
   150  		return nil, err
   151  	}
   152  
   153  	return os.OpenFile(tempFileName,
   154  		os.O_RDWR|os.O_APPEND, 0640)
   155  }
   156  
   157  // Run runs I/O benchmarks specified by diskOpts.
   158  func Run(diskOpts DiskOptions) error {
   159  	ctx := context.Background()
   160  	reg := newHistogramRegistry()
   161  
   162  	// Check if the directory exists.
   163  	_, err := os.Stat(diskOpts.Dir)
   164  	if err != nil {
   165  		return errors.Errorf("error: supplied path '%s' must exist", diskOpts.Dir)
   166  	}
   167  
   168  	log.Infof(ctx, "writing to %s\n", diskOpts.Dir)
   169  
   170  	workers := make([]worker, diskOpts.Concurrency)
   171  	var workerCreator func(ctx context.Context, diskOptions *DiskOptions, registry *histogramRegistry) worker
   172  
   173  	switch diskOpts.Type {
   174  	case SeqWriteTest:
   175  		workerCreator = newWorkerSeqWrite
   176  	default:
   177  		return errors.Errorf("Please specify a valid subtest.")
   178  	}
   179  
   180  	g, ctx := errgroup.WithContext(ctx)
   181  
   182  	var cancel func()
   183  	ctx, cancel = context.WithCancel(ctx)
   184  	defer cancel()
   185  
   186  	for i := range workers {
   187  		workers[i] = workerCreator(ctx, &diskOpts, reg)
   188  	}
   189  
   190  	for i := range workers {
   191  		g.Go(func() error {
   192  			return workers[i].run(ctx)
   193  		})
   194  	}
   195  
   196  	ticker := time.NewTicker(time.Second)
   197  	defer ticker.Stop()
   198  
   199  	errs := make(chan error, 1)
   200  	done := make(chan os.Signal, 3)
   201  	signal.Notify(done, os.Interrupt)
   202  
   203  	go func() {
   204  		if err := g.Wait(); err != nil {
   205  			errs <- err
   206  		} else {
   207  			done <- sysutil.Signal(0)
   208  		}
   209  	}()
   210  
   211  	if diskOpts.Duration > 0 {
   212  		go func() {
   213  			time.Sleep(diskOpts.Duration)
   214  			done <- sysutil.Signal(0)
   215  		}()
   216  	}
   217  
   218  	start := timeutil.Now()
   219  	lastNow := start
   220  	var lastOps uint64
   221  	var lastBytes uint64
   222  
   223  	for i := 0; ; i++ {
   224  		select {
   225  		case <-ticker.C:
   226  			now := timeutil.Now()
   227  			elapsed := now.Sub(lastNow)
   228  			ops := atomic.LoadUint64(&numOps)
   229  			bytes := atomic.LoadUint64(&numBytes)
   230  
   231  			if i%20 == 0 {
   232  				fmt.Println("_elapsed____ops/sec___mb/sec__p50(ms)__p95(ms)__p99(ms)_pMax(ms)")
   233  			}
   234  			reg.Tick(func(tick histogramTick) {
   235  				h := tick.Hist
   236  				fmt.Printf("%8s %10.1f %8.1f %8.1f %8.1f %8.1f %8.1f\n",
   237  					time.Duration(timeutil.Since(start).Seconds()+0.5)*time.Second,
   238  					float64(ops-lastOps)/elapsed.Seconds(),
   239  					float64(bytes-lastBytes)/(1024.0*1024.0)/elapsed.Seconds(),
   240  					time.Duration(h.ValueAtQuantile(50)).Seconds()*1000,
   241  					time.Duration(h.ValueAtQuantile(95)).Seconds()*1000,
   242  					time.Duration(h.ValueAtQuantile(99)).Seconds()*1000,
   243  					time.Duration(h.ValueAtQuantile(100)).Seconds()*1000,
   244  				)
   245  			})
   246  			lastNow = now
   247  			lastOps = ops
   248  			lastBytes = bytes
   249  
   250  		case <-done:
   251  			cancel()
   252  			startElapsed := timeutil.Since(start)
   253  			const totalHeader = "\n_elapsed____ops(total)__mb(total)__avg(ms)__p50(ms)__p95(ms)__p99(ms)_pMax(ms)"
   254  			fmt.Println(totalHeader + `__total`)
   255  			reg.Tick(func(tick histogramTick) {
   256  				h := tick.Cumulative
   257  				fmt.Printf("%8s %13d %10.1f %8.1f %8.1f %8.1f %8.1f %8.1f\n",
   258  					time.Duration(startElapsed.Seconds())*time.Second,
   259  					atomic.LoadUint64(&numOps),
   260  					float64(atomic.LoadUint64(&numBytes)/(1024.0*1024.0)),
   261  					time.Duration(h.Mean()).Seconds()*1000,
   262  					time.Duration(h.ValueAtQuantile(50)).Seconds()*1000,
   263  					time.Duration(h.ValueAtQuantile(95)).Seconds()*1000,
   264  					time.Duration(h.ValueAtQuantile(99)).Seconds()*1000,
   265  					time.Duration(h.ValueAtQuantile(100)).Seconds()*1000)
   266  			})
   267  			return nil
   268  		case err := <-errs:
   269  			return err
   270  		}
   271  	}
   272  }