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 }