github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/support-perf.go (about) 1 // Copyright (c) 2015-2022 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 "archive/zip" 22 gojson "encoding/json" 23 "fmt" 24 "os" 25 "path/filepath" 26 "time" 27 28 humanize "github.com/dustin/go-humanize" 29 "github.com/minio/cli" 30 json "github.com/minio/colorjson" 31 "github.com/minio/madmin-go/v3" 32 "github.com/minio/mc/pkg/probe" 33 "github.com/minio/pkg/v2/console" 34 ) 35 36 var supportPerfFlags = append([]cli.Flag{ 37 cli.StringFlag{ 38 Name: "size", 39 Usage: "size of the object used for uploads/downloads", 40 Value: "64MiB", 41 }, 42 cli.BoolFlag{ 43 Name: "verbose, v", 44 Usage: "display per-server stats", 45 }, 46 cli.StringFlag{ 47 Name: "duration", 48 Usage: "maximum duration each perf tests are run", 49 Value: "10s", 50 Hidden: true, 51 }, 52 cli.IntFlag{ 53 Name: "concurrent", 54 Usage: "number of concurrent requests per server", 55 Value: 32, 56 Hidden: true, 57 }, 58 cli.StringFlag{ 59 Name: "bucket", 60 Usage: "provide a custom bucket name to use (NOTE: bucket must be created prior)", 61 Hidden: true, // Hidden for now. 62 }, 63 cli.BoolFlag{ 64 Name: "noclear", 65 Usage: "do not clear bucket after running object perf test", 66 Hidden: true, // Hidden for now. 67 }, 68 // Drive test specific flags. 69 cli.StringFlag{ 70 Name: "filesize", 71 Usage: "total amount of data read/written to each drive", 72 Value: "1GiB", 73 Hidden: true, 74 }, 75 cli.StringFlag{ 76 Name: "blocksize", 77 Usage: "read/write block size", 78 Value: "4MiB", 79 Hidden: true, 80 }, 81 cli.BoolFlag{ 82 Name: "serial", 83 Usage: "run tests on drive(s) one-by-one", 84 Hidden: true, 85 }, 86 }, subnetCommonFlags...) 87 88 var supportPerfCmd = cli.Command{ 89 Name: "perf", 90 Usage: "upload object, network and drive performance analysis", 91 Action: mainSupportPerf, 92 OnUsageError: onUsageError, 93 Before: setGlobalsFromContext, 94 Flags: supportPerfFlags, 95 HideHelpCommand: true, 96 CustomHelpTemplate: `NAME: 97 {{.HelpName}} - {{.Usage}} 98 99 USAGE: 100 {{.HelpName}} [COMMAND] [FLAGS] TARGET 101 102 FLAGS: 103 {{range .VisibleFlags}}{{.}} 104 {{end}} 105 EXAMPLES: 106 1. Upload object storage, network, and drive performance analysis for cluster with alias 'myminio' to SUBNET 107 {{.Prompt}} {{.HelpName}} myminio 108 109 2. Run object storage, network, and drive performance tests on cluster with alias 'myminio', save and upload to SUBNET manually 110 {{.Prompt}} {{.HelpName}} myminio --airgap 111 `, 112 } 113 114 // PerfTestOutput - stores the final output of performance test(s) 115 type PerfTestOutput struct { 116 ObjectResults *ObjTestResults `json:"object,omitempty"` 117 NetResults *NetTestResults `json:"network,omitempty"` 118 SiteReplicationResults *SiteReplicationTestResults `json:"siteReplication,omitempty"` 119 DriveResults *DriveTestResults `json:"drive,omitempty"` 120 ClientResults *ClientResult `json:"client,omitempty"` 121 Error string `json:"error,omitempty"` 122 } 123 124 // DriveTestResult - result of the drive performance test on a given endpoint 125 type DriveTestResult struct { 126 Endpoint string `json:"endpoint"` 127 Perf []madmin.DrivePerf `json:"perf,omitempty"` 128 Error string `json:"error,omitempty"` 129 } 130 131 // DriveTestResults - results of drive performance test across all endpoints 132 type DriveTestResults struct { 133 Results []DriveTestResult `json:"servers"` 134 } 135 136 // ObjTestResults - result of the object performance test 137 type ObjTestResults struct { 138 ObjectSize int `json:"objectSize"` 139 Threads int `json:"threads"` 140 PUTResults ObjPUTPerfResults `json:"PUT"` 141 GETResults ObjGETPerfResults `json:"GET"` 142 } 143 144 // ObjStats - Object performance stats 145 type ObjStats struct { 146 Throughput uint64 `json:"throughput"` 147 ObjectsPerSec uint64 `json:"objectsPerSec"` 148 } 149 150 // ObjStatServer - Server level object performance stats 151 type ObjStatServer struct { 152 Endpoint string `json:"endpoint"` 153 Perf ObjStats `json:"perf"` 154 Error string `json:"error,omitempty"` 155 } 156 157 // ObjPUTPerfResults - Object PUT performance results 158 type ObjPUTPerfResults struct { 159 Perf ObjPUTStats `json:"perf"` 160 Servers []ObjStatServer `json:"servers"` 161 } 162 163 // ObjPUTStats - PUT stats of all the servers 164 type ObjPUTStats struct { 165 Throughput uint64 `json:"throughput"` 166 ObjectsPerSec uint64 `json:"objectsPerSec"` 167 Response madmin.Timings `json:"responseTime"` 168 } 169 170 // ObjGETPerfResults - Object GET performance results 171 type ObjGETPerfResults struct { 172 Perf ObjGETStats `json:"perf"` 173 Servers []ObjStatServer `json:"servers"` 174 } 175 176 // ObjGETStats - GET stats of all the servers 177 type ObjGETStats struct { 178 ObjPUTStats 179 TTFB madmin.Timings `json:"ttfb,omitempty"` 180 } 181 182 // NetStats - Network performance stats 183 type NetStats struct { 184 TX uint64 `json:"tx"` 185 RX uint64 `json:"rx"` 186 } 187 188 // NetTestResult - result of the network performance test for given endpoint 189 type NetTestResult struct { 190 Endpoint string `json:"endpoint"` 191 Perf NetStats `json:"perf"` 192 Error string `json:"error,omitempty"` 193 } 194 195 // NetTestResults - result of the network performance test across all endpoints 196 type NetTestResults struct { 197 Results []NetTestResult `json:"servers"` 198 } 199 200 // ClientResult - result of the network from client to server 201 type ClientResult struct { 202 BytesSent uint64 `json:"bytesSent"` 203 TimeSpent int64 `json:"timeSpent"` 204 Endpoint string `json:"endpoint"` 205 Error string `json:"error"` 206 } 207 208 // SiteNetStats - status for siteNet 209 type SiteNetStats struct { 210 TX uint64 `json:"tx"` // transfer rate in bytes 211 TXTotalDuration time.Duration `json:"txTotalDuration"` 212 RX uint64 `json:"rx"` // received rate in bytes 213 RXTotalDuration time.Duration `json:"rxTotalDuration"` 214 TotalConn uint64 `json:"totalConn"` 215 } 216 217 // SiteReplicationTestNodeResult - result of the network performance test for site-replication 218 type SiteReplicationTestNodeResult struct { 219 Endpoint string `json:"endpoint"` 220 Perf SiteNetStats `json:"perf"` 221 Error string `json:"error,omitempty"` 222 } 223 224 // SiteReplicationTestResults - result of the network performance test across all site-replication 225 type SiteReplicationTestResults struct { 226 Results []SiteReplicationTestNodeResult `json:"servers"` 227 } 228 229 func objectTestVerboseResult(result *madmin.SpeedTestResult) (msg string) { 230 msg += "PUT:\n" 231 for _, node := range result.PUTStats.Servers { 232 msg += fmt.Sprintf(" * %s: %s/s %s objs/s", node.Endpoint, humanize.IBytes(node.ThroughputPerSec), humanize.Comma(int64(node.ObjectsPerSec))) 233 if node.Err != "" { 234 msg += " Err: " + node.Err 235 } 236 msg += "\n" 237 } 238 239 msg += "GET:\n" 240 for _, node := range result.GETStats.Servers { 241 msg += fmt.Sprintf(" * %s: %s/s %s objs/s", node.Endpoint, humanize.IBytes(node.ThroughputPerSec), humanize.Comma(int64(node.ObjectsPerSec))) 242 if node.Err != "" { 243 msg += " Err: " + node.Err 244 } 245 msg += "\n" 246 } 247 248 return msg 249 } 250 251 func objectTestShortResult(result *madmin.SpeedTestResult) (msg string) { 252 msg += fmt.Sprintf("MinIO %s, %d servers, %d drives, %s objects, %d threads", 253 result.Version, result.Servers, result.Disks, 254 humanize.IBytes(uint64(result.Size)), result.Concurrent) 255 256 return msg 257 } 258 259 // String - dummy function to confirm to the 'message' interface. Not used. 260 func (p PerfTestOutput) String() string { 261 return "" 262 } 263 264 // JSON - jsonified output of the perf tests 265 func (p PerfTestOutput) JSON() string { 266 JSONBytes, e := json.MarshalIndent(p, "", " ") 267 fatalIf(probe.NewError(e), "Unable to marshal into JSON") 268 return string(JSONBytes) 269 } 270 271 var globalPerfTestVerbose bool 272 273 func mainSupportPerf(ctx *cli.Context) error { 274 args := ctx.Args() 275 276 // the alias parameter from cli 277 aliasedURL := "" 278 perfType := "" 279 switch len(args) { 280 case 1: 281 // cannot use alias by the name 'drive' or 'net' 282 if args[0] == "drive" || args[0] == "net" || args[0] == "object" || args[0] == "site-replication" { 283 showCommandHelpAndExit(ctx, 1) 284 } 285 aliasedURL = args[0] 286 287 case 2: 288 perfType = args[0] 289 aliasedURL = args[1] 290 default: 291 showCommandHelpAndExit(ctx, 1) // last argument is exit code 292 } 293 294 // Main execution 295 execSupportPerf(ctx, aliasedURL, perfType) 296 297 return nil 298 } 299 300 func convertDriveTestResult(dr madmin.DriveSpeedTestResult) DriveTestResult { 301 return DriveTestResult{ 302 Endpoint: dr.Endpoint, 303 Perf: dr.DrivePerf, 304 Error: dr.Error, 305 } 306 } 307 308 func convertDriveTestResults(driveResults []madmin.DriveSpeedTestResult) *DriveTestResults { 309 if driveResults == nil { 310 return nil 311 } 312 results := []DriveTestResult{} 313 for _, dr := range driveResults { 314 results = append(results, convertDriveTestResult(dr)) 315 } 316 r := DriveTestResults{ 317 Results: results, 318 } 319 return &r 320 } 321 322 func convertClientResult(result *madmin.ClientPerfResult) *ClientResult { 323 if result == nil || result.TimeSpent <= 0 { 324 return nil 325 } 326 return &ClientResult{ 327 BytesSent: result.BytesSend, 328 TimeSpent: result.TimeSpent, 329 Endpoint: result.Endpoint, 330 Error: result.Error, 331 } 332 } 333 334 func convertSiteReplicationTestResults(netResults *madmin.SiteNetPerfResult) *SiteReplicationTestResults { 335 if netResults == nil { 336 return nil 337 } 338 results := []SiteReplicationTestNodeResult{} 339 for _, nr := range netResults.NodeResults { 340 results = append(results, SiteReplicationTestNodeResult{ 341 Endpoint: nr.Endpoint, 342 Error: nr.Error, 343 Perf: SiteNetStats{ 344 TX: nr.TX, 345 TXTotalDuration: nr.TXTotalDuration, 346 RX: nr.RX, 347 RXTotalDuration: nr.RXTotalDuration, 348 TotalConn: nr.TotalConn, 349 }, 350 }) 351 } 352 r := SiteReplicationTestResults{ 353 Results: results, 354 } 355 return &r 356 } 357 358 func convertNetTestResults(netResults *madmin.NetperfResult) *NetTestResults { 359 if netResults == nil { 360 return nil 361 } 362 results := []NetTestResult{} 363 for _, nr := range netResults.NodeResults { 364 results = append(results, NetTestResult{ 365 Endpoint: nr.Endpoint, 366 Error: nr.Error, 367 Perf: NetStats{ 368 TX: nr.TX, 369 RX: nr.RX, 370 }, 371 }) 372 } 373 r := NetTestResults{ 374 Results: results, 375 } 376 return &r 377 } 378 379 func convertObjStatServers(ss []madmin.SpeedTestStatServer) []ObjStatServer { 380 out := []ObjStatServer{} 381 for _, s := range ss { 382 out = append(out, ObjStatServer{ 383 Endpoint: s.Endpoint, 384 Perf: ObjStats{ 385 Throughput: s.ThroughputPerSec, 386 ObjectsPerSec: s.ObjectsPerSec, 387 }, 388 Error: s.Err, 389 }) 390 } 391 return out 392 } 393 394 func convertPUTStats(stats madmin.SpeedTestStats) ObjPUTStats { 395 return ObjPUTStats{ 396 Throughput: stats.ThroughputPerSec, 397 ObjectsPerSec: stats.ObjectsPerSec, 398 Response: stats.Response, 399 } 400 } 401 402 func convertPUTResults(stats madmin.SpeedTestStats) ObjPUTPerfResults { 403 return ObjPUTPerfResults{ 404 Perf: convertPUTStats(stats), 405 Servers: convertObjStatServers(stats.Servers), 406 } 407 } 408 409 func convertGETResults(stats madmin.SpeedTestStats) ObjGETPerfResults { 410 return ObjGETPerfResults{ 411 Perf: ObjGETStats{ 412 ObjPUTStats: convertPUTStats(stats), 413 TTFB: stats.TTFB, 414 }, 415 Servers: convertObjStatServers(stats.Servers), 416 } 417 } 418 419 func convertObjTestResults(objResult *madmin.SpeedTestResult) *ObjTestResults { 420 if objResult == nil { 421 return nil 422 } 423 result := ObjTestResults{ 424 ObjectSize: objResult.Size, 425 Threads: objResult.Concurrent, 426 } 427 result.PUTResults = convertPUTResults(objResult.PUTStats) 428 result.GETResults = convertGETResults(objResult.GETStats) 429 return &result 430 } 431 432 func updatePerfOutput(r PerfTestResult, out *PerfTestOutput) { 433 switch r.Type { 434 case DrivePerfTest: 435 out.DriveResults = convertDriveTestResults(r.DriveResult) 436 case ObjectPerfTest: 437 out.ObjectResults = convertObjTestResults(r.ObjectResult) 438 case NetPerfTest: 439 out.NetResults = convertNetTestResults(r.NetResult) 440 case SiteReplicationPerfTest: 441 out.SiteReplicationResults = convertSiteReplicationTestResults(r.SiteReplicationResult) 442 case ClientPerfTest: 443 out.ClientResults = convertClientResult(r.ClientResult) 444 default: 445 fatalIf(errDummy().Trace(), fmt.Sprintf("Invalid test type %d", r.Type)) 446 } 447 } 448 449 func convertPerfResult(r PerfTestResult) PerfTestOutput { 450 out := PerfTestOutput{} 451 updatePerfOutput(r, &out) 452 return out 453 } 454 455 func convertPerfResults(results []PerfTestResult) PerfTestOutput { 456 out := PerfTestOutput{} 457 for _, r := range results { 458 updatePerfOutput(r, &out) 459 } 460 return out 461 } 462 463 func execSupportPerf(ctx *cli.Context, aliasedURL, perfType string) { 464 alias, apiKey := initSubnetConnectivity(ctx, aliasedURL, true) 465 if len(apiKey) == 0 { 466 // api key not passed as flag. Check that the cluster is registered. 467 apiKey = validateClusterRegistered(alias, true) 468 } 469 470 results := runPerfTests(ctx, aliasedURL, perfType) 471 if globalJSON { 472 // No file to be saved or uploaded to SUBNET in case of `--json` 473 return 474 } 475 476 // If results still not available, don't write anything 477 if len(results) == 0 { 478 console.Fatalln("No performance reports were captured, please report this issue") 479 } else { 480 resultFileNamePfx := fmt.Sprintf("%s-perf_%s", filepath.Clean(alias), UTCNow().Format("20060102150405")) 481 resultFileName := resultFileNamePfx + ".json" 482 483 regInfo := GetClusterRegInfo(getAdminInfo(aliasedURL), alias) 484 tmpFileName, e := zipPerfResult(convertPerfResults(results), resultFileName, regInfo) 485 fatalIf(probe.NewError(e), "Unable to generate zip file from performance results") 486 487 if globalAirgapped { 488 console.Infoln() 489 savePerfResultFile(tmpFileName, resultFileNamePfx) 490 return 491 } 492 493 uploadURL := SubnetUploadURL("perf") 494 reqURL, headers := prepareSubnetUploadURL(uploadURL, alias, apiKey) 495 496 _, e = (&SubnetFileUploader{ 497 alias: alias, 498 FilePath: tmpFileName, 499 ReqURL: reqURL, 500 Headers: headers, 501 DeleteAfterUpload: true, 502 }).UploadFileToSubnet() 503 if e != nil { 504 errorIf(probe.NewError(e), "Unable to upload performance results to SUBNET portal") 505 savePerfResultFile(tmpFileName, resultFileNamePfx) 506 return 507 } 508 509 console.Infoln("Uploaded performance report to SUBNET successfully") 510 } 511 } 512 513 func savePerfResultFile(tmpFileName, resultFileNamePfx string) { 514 zipFileName := resultFileNamePfx + ".zip" 515 e := moveFile(tmpFileName, zipFileName) 516 fatalIf(probe.NewError(e), fmt.Sprintf("Unable to move %s -> %s", tmpFileName, zipFileName)) 517 console.Infof("MinIO performance report saved at %s, please upload to SUBNET portal manually\n", zipFileName) 518 } 519 520 func runPerfTests(ctx *cli.Context, aliasedURL, perfType string) []PerfTestResult { 521 resultCh := make(chan PerfTestResult) 522 results := []PerfTestResult{} 523 defer close(resultCh) 524 525 tests := []string{perfType} 526 if len(perfType) == 0 { 527 // by default run all tests 528 tests = []string{"net", "drive", "object", "client"} 529 } 530 531 for _, t := range tests { 532 switch t { 533 case "drive": 534 mainAdminSpeedTestDrive(ctx, aliasedURL, resultCh) 535 case "object": 536 mainAdminSpeedTestObject(ctx, aliasedURL, resultCh) 537 case "net": 538 mainAdminSpeedTestNetperf(ctx, aliasedURL, resultCh) 539 case "site-replication": 540 mainAdminSpeedTestSiteReplication(ctx, aliasedURL, resultCh) 541 case "client": 542 mainAdminSpeedTestClientPerf(ctx, aliasedURL, resultCh) 543 default: 544 showCommandHelpAndExit(ctx, 1) // last argument is exit code 545 } 546 547 if !globalJSON { 548 results = append(results, <-resultCh) 549 } 550 } 551 552 return results 553 } 554 555 func writeJSONObjToZip(zipWriter *zip.Writer, obj interface{}, filename string) error { 556 writer, e := zipWriter.Create(filename) 557 if e != nil { 558 return e 559 } 560 561 return gojson.NewEncoder(writer).Encode(obj) 562 } 563 564 // compress MinIO performance output 565 func zipPerfResult(perfOutput PerfTestOutput, resultFilename string, regInfo ClusterRegistrationInfo) (string, error) { 566 // Create perf results zip file 567 tmpArchive, e := os.CreateTemp("", "mc-perf-*.zip") 568 569 if e != nil { 570 return "", e 571 } 572 defer tmpArchive.Close() 573 574 zipWriter := zip.NewWriter(tmpArchive) 575 defer zipWriter.Close() 576 577 e = writeJSONObjToZip(zipWriter, perfOutput, resultFilename) 578 if e != nil { 579 return "", e 580 } 581 582 e = writeJSONObjToZip(zipWriter, regInfo, "cluster.info") 583 if e != nil { 584 return "", e 585 } 586 587 return tmpArchive.Name(), nil 588 }