// Copyright (c) 2015-2024 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "context" "fmt" "net/url" "runtime" "sort" "time" "github.com/minio/dperf/pkg/dperf" "github.com/minio/madmin-go/v3" "github.com/minio/minio/internal/auth" xioutil "github.com/minio/minio/internal/ioutil" ) const speedTest = "speedtest" type speedTestOpts struct { objectSize int concurrencyStart int concurrency int duration time.Duration autotune bool storageClass string bucketName string enableSha256 bool enableMultipart bool creds auth.Credentials } // Get the max throughput and iops numbers. func objectSpeedTest(ctx context.Context, opts speedTestOpts) chan madmin.SpeedTestResult { ch := make(chan madmin.SpeedTestResult, 1) go func() { defer xioutil.SafeClose(ch) concurrency := opts.concurrencyStart if opts.autotune { // if we have less drives than concurrency then choose // only the concurrency to be number of drives to start // with - since default '32' might be big and may not // complete in total time of 10s. if globalEndpoints.NEndpoints() < concurrency { concurrency = globalEndpoints.NEndpoints() } // Check if we have local disks per pool less than // the concurrency make sure we choose only the "start" // concurrency to be equal to the lowest number of // local disks per server. for _, localDiskCount := range globalEndpoints.NLocalDisksPathsPerPool() { if localDiskCount < concurrency { concurrency = localDiskCount } } // Any concurrency less than '4' just stick to '4' concurrent // operations for now to begin with. if concurrency < 4 { concurrency = 4 } // if GOMAXPROCS is set to a lower value then choose to use // concurrency == GOMAXPROCS instead. if runtime.GOMAXPROCS(0) < concurrency { concurrency = runtime.GOMAXPROCS(0) } } throughputHighestGet := uint64(0) throughputHighestPut := uint64(0) var throughputHighestResults []SpeedTestResult sendResult := func() { var result madmin.SpeedTestResult durationSecs := opts.duration.Seconds() result.GETStats.ThroughputPerSec = throughputHighestGet / uint64(durationSecs) result.GETStats.ObjectsPerSec = throughputHighestGet / uint64(opts.objectSize) / uint64(durationSecs) result.PUTStats.ThroughputPerSec = throughputHighestPut / uint64(durationSecs) result.PUTStats.ObjectsPerSec = throughputHighestPut / uint64(opts.objectSize) / uint64(durationSecs) var totalUploadTimes madmin.TimeDurations var totalDownloadTimes madmin.TimeDurations var totalDownloadTTFB madmin.TimeDurations for i := 0; i < len(throughputHighestResults); i++ { errStr := "" if throughputHighestResults[i].Error != "" { errStr = throughputHighestResults[i].Error } // if the default concurrency yields zero results, throw an error. if throughputHighestResults[i].Downloads == 0 && opts.concurrencyStart == concurrency { errStr = fmt.Sprintf("no results for downloads upon first attempt, concurrency %d and duration %s", opts.concurrencyStart, opts.duration) } // if the default concurrency yields zero results, throw an error. if throughputHighestResults[i].Uploads == 0 && opts.concurrencyStart == concurrency { errStr = fmt.Sprintf("no results for uploads upon first attempt, concurrency %d and duration %s", opts.concurrencyStart, opts.duration) } result.PUTStats.Servers = append(result.PUTStats.Servers, madmin.SpeedTestStatServer{ Endpoint: throughputHighestResults[i].Endpoint, ThroughputPerSec: throughputHighestResults[i].Uploads / uint64(durationSecs), ObjectsPerSec: throughputHighestResults[i].Uploads / uint64(opts.objectSize) / uint64(durationSecs), Err: errStr, }) result.GETStats.Servers = append(result.GETStats.Servers, madmin.SpeedTestStatServer{ Endpoint: throughputHighestResults[i].Endpoint, ThroughputPerSec: throughputHighestResults[i].Downloads / uint64(durationSecs), ObjectsPerSec: throughputHighestResults[i].Downloads / uint64(opts.objectSize) / uint64(durationSecs), Err: errStr, }) totalUploadTimes = append(totalUploadTimes, throughputHighestResults[i].UploadTimes...) totalDownloadTimes = append(totalDownloadTimes, throughputHighestResults[i].DownloadTimes...) totalDownloadTTFB = append(totalDownloadTTFB, throughputHighestResults[i].DownloadTTFB...) } result.PUTStats.Response = totalUploadTimes.Measure() result.GETStats.Response = totalDownloadTimes.Measure() result.GETStats.TTFB = totalDownloadTTFB.Measure() result.Size = opts.objectSize result.Disks = globalEndpoints.NEndpoints() result.Servers = len(globalNotificationSys.peerClients) + 1 result.Version = Version result.Concurrent = concurrency select { case ch <- result: case <-ctx.Done(): return } } for { select { case <-ctx.Done(): // If the client got disconnected stop the speedtest. return default: } sopts := speedTestOpts{ objectSize: opts.objectSize, concurrency: concurrency, duration: opts.duration, storageClass: opts.storageClass, bucketName: opts.bucketName, enableSha256: opts.enableSha256, enableMultipart: opts.enableMultipart, creds: opts.creds, } results := globalNotificationSys.SpeedTest(ctx, sopts) sort.Slice(results, func(i, j int) bool { return results[i].Endpoint < results[j].Endpoint }) totalPut := uint64(0) totalGet := uint64(0) for _, result := range results { totalPut += result.Uploads totalGet += result.Downloads } if totalGet < throughputHighestGet { // Following check is for situations // when Writes() scale higher than Reads() // - practically speaking this never happens // and should never happen - however it has // been seen recently due to hardware issues // causes Reads() to go slower than Writes(). // // Send such results anyways as this shall // expose a problem underneath. if totalPut > throughputHighestPut { throughputHighestResults = results throughputHighestPut = totalPut // let the client see lower value as well throughputHighestGet = totalGet } sendResult() break } // We break if we did not see 2.5% growth rate in total GET // requests, we have reached our peak at this point. doBreak := float64(totalGet-throughputHighestGet)/float64(totalGet) < 0.025 throughputHighestGet = totalGet throughputHighestResults = results throughputHighestPut = totalPut if doBreak { sendResult() break } for _, result := range results { if result.Error != "" { // Break out on errors. sendResult() return } } sendResult() if !opts.autotune { break } // Try with a higher concurrency to see if we get better throughput concurrency += (concurrency + 1) / 2 } }() return ch } func driveSpeedTest(ctx context.Context, opts madmin.DriveSpeedTestOpts) madmin.DriveSpeedTestResult { perf := &dperf.DrivePerf{ Serial: opts.Serial, BlockSize: opts.BlockSize, FileSize: opts.FileSize, } localPaths := globalEndpoints.LocalDisksPaths() var ignoredPaths []string paths := func() (tmpPaths []string) { for _, lp := range localPaths { if _, err := Lstat(pathJoin(lp, minioMetaBucket, formatConfigFile)); err == nil { tmpPaths = append(tmpPaths, pathJoin(lp, minioMetaTmpBucket)) } else { // Use dperf on only formatted drives. ignoredPaths = append(ignoredPaths, lp) } } return tmpPaths }() scheme := "http" if globalIsTLS { scheme = "https" } u := &url.URL{ Scheme: scheme, Host: globalLocalNodeName, } perfs, err := perf.Run(ctx, paths...) return madmin.DriveSpeedTestResult{ Endpoint: u.String(), Version: Version, DrivePerf: func() (results []madmin.DrivePerf) { for idx, r := range perfs { result := madmin.DrivePerf{ Path: localPaths[idx], ReadThroughput: r.ReadThroughput, WriteThroughput: r.WriteThroughput, Error: func() string { if r.Error != nil { return r.Error.Error() } return "" }(), } results = append(results, result) } for _, inp := range ignoredPaths { results = append(results, madmin.DrivePerf{ Path: inp, Error: errFaultyDisk.Error(), }) } return results }(), Error: func() string { if err != nil { return err.Error() } return "" }(), } }