github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/speedtest.go (about)

     1  // Copyright (c) 2015-2021 MinIO, Inc.
     2  //
     3  // This file is part of MinIO Object Storage stack
     4  //
     5  // This program is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Affero General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // This program is distributed in the hope that it will be useful
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU Affero General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Affero General Public License
    16  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package cmd
    19  
    20  import (
    21  	"context"
    22  	"fmt"
    23  	"net/url"
    24  	"runtime"
    25  	"sort"
    26  	"time"
    27  
    28  	"github.com/minio/dperf/pkg/dperf"
    29  	"github.com/minio/madmin-go/v3"
    30  	xioutil "github.com/minio/minio/internal/ioutil"
    31  )
    32  
    33  const speedTest = "speedtest"
    34  
    35  type speedTestOpts struct {
    36  	objectSize       int
    37  	concurrencyStart int
    38  	concurrency      int
    39  	duration         time.Duration
    40  	autotune         bool
    41  	storageClass     string
    42  	bucketName       string
    43  	enableSha256     bool
    44  }
    45  
    46  // Get the max throughput and iops numbers.
    47  func objectSpeedTest(ctx context.Context, opts speedTestOpts) chan madmin.SpeedTestResult {
    48  	ch := make(chan madmin.SpeedTestResult, 1)
    49  	go func() {
    50  		defer xioutil.SafeClose(ch)
    51  
    52  		concurrency := opts.concurrencyStart
    53  
    54  		if opts.autotune {
    55  			// if we have less drives than concurrency then choose
    56  			// only the concurrency to be number of drives to start
    57  			// with - since default '32' might be big and may not
    58  			// complete in total time of 10s.
    59  			if globalEndpoints.NEndpoints() < concurrency {
    60  				concurrency = globalEndpoints.NEndpoints()
    61  			}
    62  
    63  			// Check if we have local disks per pool less than
    64  			// the concurrency make sure we choose only the "start"
    65  			// concurrency to be equal to the lowest number of
    66  			// local disks per server.
    67  			for _, localDiskCount := range globalEndpoints.NLocalDisksPathsPerPool() {
    68  				if localDiskCount < concurrency {
    69  					concurrency = localDiskCount
    70  				}
    71  			}
    72  
    73  			// Any concurrency less than '4' just stick to '4' concurrent
    74  			// operations for now to begin with.
    75  			if concurrency < 4 {
    76  				concurrency = 4
    77  			}
    78  
    79  			// if GOMAXPROCS is set to a lower value then choose to use
    80  			// concurrency == GOMAXPROCS instead.
    81  			if runtime.GOMAXPROCS(0) < concurrency {
    82  				concurrency = runtime.GOMAXPROCS(0)
    83  			}
    84  		}
    85  
    86  		throughputHighestGet := uint64(0)
    87  		throughputHighestPut := uint64(0)
    88  		var throughputHighestResults []SpeedTestResult
    89  
    90  		sendResult := func() {
    91  			var result madmin.SpeedTestResult
    92  
    93  			durationSecs := opts.duration.Seconds()
    94  
    95  			result.GETStats.ThroughputPerSec = throughputHighestGet / uint64(durationSecs)
    96  			result.GETStats.ObjectsPerSec = throughputHighestGet / uint64(opts.objectSize) / uint64(durationSecs)
    97  			result.PUTStats.ThroughputPerSec = throughputHighestPut / uint64(durationSecs)
    98  			result.PUTStats.ObjectsPerSec = throughputHighestPut / uint64(opts.objectSize) / uint64(durationSecs)
    99  			var totalUploadTimes madmin.TimeDurations
   100  			var totalDownloadTimes madmin.TimeDurations
   101  			var totalDownloadTTFB madmin.TimeDurations
   102  			for i := 0; i < len(throughputHighestResults); i++ {
   103  				errStr := ""
   104  				if throughputHighestResults[i].Error != "" {
   105  					errStr = throughputHighestResults[i].Error
   106  				}
   107  
   108  				// if the default concurrency yields zero results, throw an error.
   109  				if throughputHighestResults[i].Downloads == 0 && opts.concurrencyStart == concurrency {
   110  					errStr = fmt.Sprintf("no results for downloads upon first attempt, concurrency %d and duration %s", opts.concurrencyStart, opts.duration)
   111  				}
   112  
   113  				// if the default concurrency yields zero results, throw an error.
   114  				if throughputHighestResults[i].Uploads == 0 && opts.concurrencyStart == concurrency {
   115  					errStr = fmt.Sprintf("no results for uploads upon first attempt, concurrency %d and duration %s", opts.concurrencyStart, opts.duration)
   116  				}
   117  
   118  				result.PUTStats.Servers = append(result.PUTStats.Servers, madmin.SpeedTestStatServer{
   119  					Endpoint:         throughputHighestResults[i].Endpoint,
   120  					ThroughputPerSec: throughputHighestResults[i].Uploads / uint64(durationSecs),
   121  					ObjectsPerSec:    throughputHighestResults[i].Uploads / uint64(opts.objectSize) / uint64(durationSecs),
   122  					Err:              errStr,
   123  				})
   124  
   125  				result.GETStats.Servers = append(result.GETStats.Servers, madmin.SpeedTestStatServer{
   126  					Endpoint:         throughputHighestResults[i].Endpoint,
   127  					ThroughputPerSec: throughputHighestResults[i].Downloads / uint64(durationSecs),
   128  					ObjectsPerSec:    throughputHighestResults[i].Downloads / uint64(opts.objectSize) / uint64(durationSecs),
   129  					Err:              errStr,
   130  				})
   131  
   132  				totalUploadTimes = append(totalUploadTimes, throughputHighestResults[i].UploadTimes...)
   133  				totalDownloadTimes = append(totalDownloadTimes, throughputHighestResults[i].DownloadTimes...)
   134  				totalDownloadTTFB = append(totalDownloadTTFB, throughputHighestResults[i].DownloadTTFB...)
   135  			}
   136  
   137  			result.PUTStats.Response = totalUploadTimes.Measure()
   138  			result.GETStats.Response = totalDownloadTimes.Measure()
   139  			result.GETStats.TTFB = totalDownloadTTFB.Measure()
   140  
   141  			result.Size = opts.objectSize
   142  			result.Disks = globalEndpoints.NEndpoints()
   143  			result.Servers = len(globalNotificationSys.peerClients) + 1
   144  			result.Version = Version
   145  			result.Concurrent = concurrency
   146  
   147  			select {
   148  			case ch <- result:
   149  			case <-ctx.Done():
   150  				return
   151  			}
   152  		}
   153  
   154  		for {
   155  			select {
   156  			case <-ctx.Done():
   157  				// If the client got disconnected stop the speedtest.
   158  				return
   159  			default:
   160  			}
   161  
   162  			sopts := speedTestOpts{
   163  				objectSize:   opts.objectSize,
   164  				concurrency:  concurrency,
   165  				duration:     opts.duration,
   166  				storageClass: opts.storageClass,
   167  				bucketName:   opts.bucketName,
   168  				enableSha256: opts.enableSha256,
   169  			}
   170  
   171  			results := globalNotificationSys.SpeedTest(ctx, sopts)
   172  			sort.Slice(results, func(i, j int) bool {
   173  				return results[i].Endpoint < results[j].Endpoint
   174  			})
   175  
   176  			totalPut := uint64(0)
   177  			totalGet := uint64(0)
   178  			for _, result := range results {
   179  				totalPut += result.Uploads
   180  				totalGet += result.Downloads
   181  			}
   182  
   183  			if totalGet < throughputHighestGet {
   184  				// Following check is for situations
   185  				// when Writes() scale higher than Reads()
   186  				// - practically speaking this never happens
   187  				// and should never happen - however it has
   188  				// been seen recently due to hardware issues
   189  				// causes Reads() to go slower than Writes().
   190  				//
   191  				// Send such results anyways as this shall
   192  				// expose a problem underneath.
   193  				if totalPut > throughputHighestPut {
   194  					throughputHighestResults = results
   195  					throughputHighestPut = totalPut
   196  					// let the client see lower value as well
   197  					throughputHighestGet = totalGet
   198  				}
   199  				sendResult()
   200  				break
   201  			}
   202  
   203  			// We break if we did not see 2.5% growth rate in total GET
   204  			// requests, we have reached our peak at this point.
   205  			doBreak := float64(totalGet-throughputHighestGet)/float64(totalGet) < 0.025
   206  
   207  			throughputHighestGet = totalGet
   208  			throughputHighestResults = results
   209  			throughputHighestPut = totalPut
   210  
   211  			if doBreak {
   212  				sendResult()
   213  				break
   214  			}
   215  
   216  			for _, result := range results {
   217  				if result.Error != "" {
   218  					// Break out on errors.
   219  					sendResult()
   220  					return
   221  				}
   222  			}
   223  
   224  			sendResult()
   225  			if !opts.autotune {
   226  				break
   227  			}
   228  
   229  			// Try with a higher concurrency to see if we get better throughput
   230  			concurrency += (concurrency + 1) / 2
   231  		}
   232  	}()
   233  	return ch
   234  }
   235  
   236  func driveSpeedTest(ctx context.Context, opts madmin.DriveSpeedTestOpts) madmin.DriveSpeedTestResult {
   237  	perf := &dperf.DrivePerf{
   238  		Serial:    opts.Serial,
   239  		BlockSize: opts.BlockSize,
   240  		FileSize:  opts.FileSize,
   241  	}
   242  
   243  	localPaths := globalEndpoints.LocalDisksPaths()
   244  	var ignoredPaths []string
   245  	paths := func() (tmpPaths []string) {
   246  		for _, lp := range localPaths {
   247  			if _, err := Lstat(pathJoin(lp, minioMetaBucket, formatConfigFile)); err == nil {
   248  				tmpPaths = append(tmpPaths, pathJoin(lp, minioMetaTmpBucket))
   249  			} else {
   250  				// Use dperf on only formatted drives.
   251  				ignoredPaths = append(ignoredPaths, lp)
   252  			}
   253  		}
   254  		return tmpPaths
   255  	}()
   256  
   257  	scheme := "http"
   258  	if globalIsTLS {
   259  		scheme = "https"
   260  	}
   261  
   262  	u := &url.URL{
   263  		Scheme: scheme,
   264  		Host:   globalLocalNodeName,
   265  	}
   266  
   267  	perfs, err := perf.Run(ctx, paths...)
   268  	return madmin.DriveSpeedTestResult{
   269  		Endpoint: u.String(),
   270  		Version:  Version,
   271  		DrivePerf: func() (results []madmin.DrivePerf) {
   272  			for idx, r := range perfs {
   273  				result := madmin.DrivePerf{
   274  					Path:            localPaths[idx],
   275  					ReadThroughput:  r.ReadThroughput,
   276  					WriteThroughput: r.WriteThroughput,
   277  					Error: func() string {
   278  						if r.Error != nil {
   279  							return r.Error.Error()
   280  						}
   281  						return ""
   282  					}(),
   283  				}
   284  				results = append(results, result)
   285  			}
   286  			for _, inp := range ignoredPaths {
   287  				results = append(results, madmin.DrivePerf{
   288  					Path:  inp,
   289  					Error: errFaultyDisk.Error(),
   290  				})
   291  			}
   292  			return results
   293  		}(),
   294  		Error: func() string {
   295  			if err != nil {
   296  				return err.Error()
   297  			}
   298  			return ""
   299  		}(),
   300  	}
   301  }