1
0
mirror of https://github.com/minio/mc.git synced 2025-04-19 21:02:15 +03:00
mc/cmd/support-perf.go
2024-05-23 00:59:47 -07:00

589 lines
16 KiB
Go

// Copyright (c) 2015-2022 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 <http://www.gnu.org/licenses/>.
package cmd
import (
"archive/zip"
gojson "encoding/json"
"fmt"
"os"
"path/filepath"
"time"
humanize "github.com/dustin/go-humanize"
"github.com/minio/cli"
json "github.com/minio/colorjson"
"github.com/minio/madmin-go/v3"
"github.com/minio/mc/pkg/probe"
"github.com/minio/pkg/v3/console"
)
var supportPerfFlags = append([]cli.Flag{
cli.StringFlag{
Name: "size",
Usage: "size of the object used for uploads/downloads",
Value: "64MiB",
},
cli.BoolFlag{
Name: "verbose, v",
Usage: "display per-server stats",
},
cli.StringFlag{
Name: "duration",
Usage: "maximum duration each perf tests are run",
Value: "10s",
Hidden: true,
},
cli.IntFlag{
Name: "concurrent",
Usage: "number of concurrent requests per server",
Value: 32,
Hidden: true,
},
cli.StringFlag{
Name: "bucket",
Usage: "provide a custom bucket name to use (NOTE: bucket must be created prior)",
Hidden: true, // Hidden for now.
},
cli.BoolFlag{
Name: "noclear",
Usage: "do not clear bucket after running object perf test",
Hidden: true, // Hidden for now.
},
// Drive test specific flags.
cli.StringFlag{
Name: "filesize",
Usage: "total amount of data read/written to each drive",
Value: "1GiB",
Hidden: true,
},
cli.StringFlag{
Name: "blocksize",
Usage: "read/write block size",
Value: "4MiB",
Hidden: true,
},
cli.BoolFlag{
Name: "serial",
Usage: "run tests on drive(s) one-by-one",
Hidden: true,
},
}, subnetCommonFlags...)
var supportPerfCmd = cli.Command{
Name: "perf",
Usage: "upload object, network and drive performance analysis",
Action: mainSupportPerf,
OnUsageError: onUsageError,
Before: setGlobalsFromContext,
Flags: supportPerfFlags,
HideHelpCommand: true,
CustomHelpTemplate: `NAME:
{{.HelpName}} - {{.Usage}}
USAGE:
{{.HelpName}} [COMMAND] [FLAGS] TARGET
FLAGS:
{{range .VisibleFlags}}{{.}}
{{end}}
EXAMPLES:
1. Upload object storage, network, and drive performance analysis for cluster with alias 'myminio' to SUBNET
{{.Prompt}} {{.HelpName}} myminio
2. Run object storage, network, and drive performance tests on cluster with alias 'myminio', save and upload to SUBNET manually
{{.Prompt}} {{.HelpName}} myminio --airgap
`,
}
// PerfTestOutput - stores the final output of performance test(s)
type PerfTestOutput struct {
ObjectResults *ObjTestResults `json:"object,omitempty"`
NetResults *NetTestResults `json:"network,omitempty"`
SiteReplicationResults *SiteReplicationTestResults `json:"siteReplication,omitempty"`
DriveResults *DriveTestResults `json:"drive,omitempty"`
ClientResults *ClientResult `json:"client,omitempty"`
Error string `json:"error,omitempty"`
}
// DriveTestResult - result of the drive performance test on a given endpoint
type DriveTestResult struct {
Endpoint string `json:"endpoint"`
Perf []madmin.DrivePerf `json:"perf,omitempty"`
Error string `json:"error,omitempty"`
}
// DriveTestResults - results of drive performance test across all endpoints
type DriveTestResults struct {
Results []DriveTestResult `json:"servers"`
}
// ObjTestResults - result of the object performance test
type ObjTestResults struct {
ObjectSize int `json:"objectSize"`
Threads int `json:"threads"`
PUTResults ObjPUTPerfResults `json:"PUT"`
GETResults ObjGETPerfResults `json:"GET"`
}
// ObjStats - Object performance stats
type ObjStats struct {
Throughput uint64 `json:"throughput"`
ObjectsPerSec uint64 `json:"objectsPerSec"`
}
// ObjStatServer - Server level object performance stats
type ObjStatServer struct {
Endpoint string `json:"endpoint"`
Perf ObjStats `json:"perf"`
Error string `json:"error,omitempty"`
}
// ObjPUTPerfResults - Object PUT performance results
type ObjPUTPerfResults struct {
Perf ObjPUTStats `json:"perf"`
Servers []ObjStatServer `json:"servers"`
}
// ObjPUTStats - PUT stats of all the servers
type ObjPUTStats struct {
Throughput uint64 `json:"throughput"`
ObjectsPerSec uint64 `json:"objectsPerSec"`
Response madmin.Timings `json:"responseTime"`
}
// ObjGETPerfResults - Object GET performance results
type ObjGETPerfResults struct {
Perf ObjGETStats `json:"perf"`
Servers []ObjStatServer `json:"servers"`
}
// ObjGETStats - GET stats of all the servers
type ObjGETStats struct {
ObjPUTStats
TTFB madmin.Timings `json:"ttfb,omitempty"`
}
// NetStats - Network performance stats
type NetStats struct {
TX uint64 `json:"tx"`
RX uint64 `json:"rx"`
}
// NetTestResult - result of the network performance test for given endpoint
type NetTestResult struct {
Endpoint string `json:"endpoint"`
Perf NetStats `json:"perf"`
Error string `json:"error,omitempty"`
}
// NetTestResults - result of the network performance test across all endpoints
type NetTestResults struct {
Results []NetTestResult `json:"servers"`
}
// ClientResult - result of the network from client to server
type ClientResult struct {
BytesSent uint64 `json:"bytesSent"`
TimeSpent int64 `json:"timeSpent"`
Endpoint string `json:"endpoint"`
Error string `json:"error"`
}
// SiteNetStats - status for siteNet
type SiteNetStats struct {
TX uint64 `json:"tx"` // transfer rate in bytes
TXTotalDuration time.Duration `json:"txTotalDuration"`
RX uint64 `json:"rx"` // received rate in bytes
RXTotalDuration time.Duration `json:"rxTotalDuration"`
TotalConn uint64 `json:"totalConn"`
}
// SiteReplicationTestNodeResult - result of the network performance test for site-replication
type SiteReplicationTestNodeResult struct {
Endpoint string `json:"endpoint"`
Perf SiteNetStats `json:"perf"`
Error string `json:"error,omitempty"`
}
// SiteReplicationTestResults - result of the network performance test across all site-replication
type SiteReplicationTestResults struct {
Results []SiteReplicationTestNodeResult `json:"servers"`
}
func objectTestVerboseResult(result *madmin.SpeedTestResult) (msg string) {
msg += "PUT:\n"
for _, node := range result.PUTStats.Servers {
msg += fmt.Sprintf(" * %s: %s/s %s objs/s", node.Endpoint, humanize.IBytes(node.ThroughputPerSec), humanize.Comma(int64(node.ObjectsPerSec)))
if node.Err != "" {
msg += " Err: " + node.Err
}
msg += "\n"
}
msg += "GET:\n"
for _, node := range result.GETStats.Servers {
msg += fmt.Sprintf(" * %s: %s/s %s objs/s", node.Endpoint, humanize.IBytes(node.ThroughputPerSec), humanize.Comma(int64(node.ObjectsPerSec)))
if node.Err != "" {
msg += " Err: " + node.Err
}
msg += "\n"
}
return msg
}
func objectTestShortResult(result *madmin.SpeedTestResult) (msg string) {
msg += fmt.Sprintf("MinIO %s, %d servers, %d drives, %s objects, %d threads",
result.Version, result.Servers, result.Disks,
humanize.IBytes(uint64(result.Size)), result.Concurrent)
return msg
}
// String - dummy function to confirm to the 'message' interface. Not used.
func (p PerfTestOutput) String() string {
return ""
}
// JSON - jsonified output of the perf tests
func (p PerfTestOutput) JSON() string {
JSONBytes, e := json.MarshalIndent(p, "", " ")
fatalIf(probe.NewError(e), "Unable to marshal into JSON")
return string(JSONBytes)
}
var globalPerfTestVerbose bool
func mainSupportPerf(ctx *cli.Context) error {
args := ctx.Args()
// the alias parameter from cli
aliasedURL := ""
perfType := ""
switch len(args) {
case 1:
// cannot use alias by the name 'drive' or 'net'
if args[0] == "drive" || args[0] == "net" || args[0] == "object" || args[0] == "site-replication" {
showCommandHelpAndExit(ctx, 1)
}
aliasedURL = args[0]
case 2:
perfType = args[0]
aliasedURL = args[1]
default:
showCommandHelpAndExit(ctx, 1) // last argument is exit code
}
// Main execution
execSupportPerf(ctx, aliasedURL, perfType)
return nil
}
func convertDriveTestResult(dr madmin.DriveSpeedTestResult) DriveTestResult {
return DriveTestResult{
Endpoint: dr.Endpoint,
Perf: dr.DrivePerf,
Error: dr.Error,
}
}
func convertDriveTestResults(driveResults []madmin.DriveSpeedTestResult) *DriveTestResults {
if driveResults == nil {
return nil
}
results := []DriveTestResult{}
for _, dr := range driveResults {
results = append(results, convertDriveTestResult(dr))
}
r := DriveTestResults{
Results: results,
}
return &r
}
func convertClientResult(result *madmin.ClientPerfResult) *ClientResult {
if result == nil || result.TimeSpent <= 0 {
return nil
}
return &ClientResult{
BytesSent: result.BytesSend,
TimeSpent: result.TimeSpent,
Endpoint: result.Endpoint,
Error: result.Error,
}
}
func convertSiteReplicationTestResults(netResults *madmin.SiteNetPerfResult) *SiteReplicationTestResults {
if netResults == nil {
return nil
}
results := []SiteReplicationTestNodeResult{}
for _, nr := range netResults.NodeResults {
results = append(results, SiteReplicationTestNodeResult{
Endpoint: nr.Endpoint,
Error: nr.Error,
Perf: SiteNetStats{
TX: nr.TX,
TXTotalDuration: nr.TXTotalDuration,
RX: nr.RX,
RXTotalDuration: nr.RXTotalDuration,
TotalConn: nr.TotalConn,
},
})
}
r := SiteReplicationTestResults{
Results: results,
}
return &r
}
func convertNetTestResults(netResults *madmin.NetperfResult) *NetTestResults {
if netResults == nil {
return nil
}
results := []NetTestResult{}
for _, nr := range netResults.NodeResults {
results = append(results, NetTestResult{
Endpoint: nr.Endpoint,
Error: nr.Error,
Perf: NetStats{
TX: nr.TX,
RX: nr.RX,
},
})
}
r := NetTestResults{
Results: results,
}
return &r
}
func convertObjStatServers(ss []madmin.SpeedTestStatServer) []ObjStatServer {
out := []ObjStatServer{}
for _, s := range ss {
out = append(out, ObjStatServer{
Endpoint: s.Endpoint,
Perf: ObjStats{
Throughput: s.ThroughputPerSec,
ObjectsPerSec: s.ObjectsPerSec,
},
Error: s.Err,
})
}
return out
}
func convertPUTStats(stats madmin.SpeedTestStats) ObjPUTStats {
return ObjPUTStats{
Throughput: stats.ThroughputPerSec,
ObjectsPerSec: stats.ObjectsPerSec,
Response: stats.Response,
}
}
func convertPUTResults(stats madmin.SpeedTestStats) ObjPUTPerfResults {
return ObjPUTPerfResults{
Perf: convertPUTStats(stats),
Servers: convertObjStatServers(stats.Servers),
}
}
func convertGETResults(stats madmin.SpeedTestStats) ObjGETPerfResults {
return ObjGETPerfResults{
Perf: ObjGETStats{
ObjPUTStats: convertPUTStats(stats),
TTFB: stats.TTFB,
},
Servers: convertObjStatServers(stats.Servers),
}
}
func convertObjTestResults(objResult *madmin.SpeedTestResult) *ObjTestResults {
if objResult == nil {
return nil
}
result := ObjTestResults{
ObjectSize: objResult.Size,
Threads: objResult.Concurrent,
}
result.PUTResults = convertPUTResults(objResult.PUTStats)
result.GETResults = convertGETResults(objResult.GETStats)
return &result
}
func updatePerfOutput(r PerfTestResult, out *PerfTestOutput) {
switch r.Type {
case DrivePerfTest:
out.DriveResults = convertDriveTestResults(r.DriveResult)
case ObjectPerfTest:
out.ObjectResults = convertObjTestResults(r.ObjectResult)
case NetPerfTest:
out.NetResults = convertNetTestResults(r.NetResult)
case SiteReplicationPerfTest:
out.SiteReplicationResults = convertSiteReplicationTestResults(r.SiteReplicationResult)
case ClientPerfTest:
out.ClientResults = convertClientResult(r.ClientResult)
default:
fatalIf(errDummy().Trace(), fmt.Sprintf("Invalid test type %d", r.Type))
}
}
func convertPerfResult(r PerfTestResult) PerfTestOutput {
out := PerfTestOutput{}
updatePerfOutput(r, &out)
return out
}
func convertPerfResults(results []PerfTestResult) PerfTestOutput {
out := PerfTestOutput{}
for _, r := range results {
updatePerfOutput(r, &out)
}
return out
}
func execSupportPerf(ctx *cli.Context, aliasedURL, perfType string) {
alias, apiKey := initSubnetConnectivity(ctx, aliasedURL, true)
if len(apiKey) == 0 {
// api key not passed as flag. Check that the cluster is registered.
apiKey = validateClusterRegistered(alias, true)
}
results := runPerfTests(ctx, aliasedURL, perfType)
if globalJSON {
// No file to be saved or uploaded to SUBNET in case of `--json`
return
}
// If results still not available, don't write anything
if len(results) == 0 {
console.Fatalln("No performance reports were captured, please report this issue")
} else {
resultFileNamePfx := fmt.Sprintf("%s-perf_%s", filepath.Clean(alias), UTCNow().Format("20060102150405"))
resultFileName := resultFileNamePfx + ".json"
regInfo := GetClusterRegInfo(getAdminInfo(aliasedURL), alias)
tmpFileName, e := zipPerfResult(convertPerfResults(results), resultFileName, regInfo)
fatalIf(probe.NewError(e), "Unable to generate zip file from performance results")
if globalAirgapped {
console.Infoln()
savePerfResultFile(tmpFileName, resultFileNamePfx)
return
}
uploadURL := SubnetUploadURL("perf")
reqURL, headers := prepareSubnetUploadURL(uploadURL, alias, apiKey)
_, e = (&SubnetFileUploader{
alias: alias,
FilePath: tmpFileName,
ReqURL: reqURL,
Headers: headers,
DeleteAfterUpload: true,
}).UploadFileToSubnet()
if e != nil {
errorIf(probe.NewError(e), "Unable to upload performance results to SUBNET portal")
savePerfResultFile(tmpFileName, resultFileNamePfx)
return
}
console.Infoln("Uploaded performance report to SUBNET successfully")
}
}
func savePerfResultFile(tmpFileName, resultFileNamePfx string) {
zipFileName := resultFileNamePfx + ".zip"
e := moveFile(tmpFileName, zipFileName)
fatalIf(probe.NewError(e), fmt.Sprintf("Unable to move %s -> %s", tmpFileName, zipFileName))
console.Infof("MinIO performance report saved at %s, please upload to SUBNET portal manually\n", zipFileName)
}
func runPerfTests(ctx *cli.Context, aliasedURL, perfType string) []PerfTestResult {
resultCh := make(chan PerfTestResult)
results := []PerfTestResult{}
defer close(resultCh)
tests := []string{perfType}
if len(perfType) == 0 {
// by default run all tests
tests = []string{"net", "drive", "object", "client"}
}
for _, t := range tests {
switch t {
case "drive":
mainAdminSpeedTestDrive(ctx, aliasedURL, resultCh)
case "object":
mainAdminSpeedTestObject(ctx, aliasedURL, resultCh)
case "net":
mainAdminSpeedTestNetperf(ctx, aliasedURL, resultCh)
case "site-replication":
mainAdminSpeedTestSiteReplication(ctx, aliasedURL, resultCh)
case "client":
mainAdminSpeedTestClientPerf(ctx, aliasedURL, resultCh)
default:
showCommandHelpAndExit(ctx, 1) // last argument is exit code
}
if !globalJSON {
results = append(results, <-resultCh)
}
}
return results
}
func writeJSONObjToZip(zipWriter *zip.Writer, obj interface{}, filename string) error {
writer, e := zipWriter.Create(filename)
if e != nil {
return e
}
return gojson.NewEncoder(writer).Encode(obj)
}
// compress MinIO performance output
func zipPerfResult(perfOutput PerfTestOutput, resultFilename string, regInfo ClusterRegistrationInfo) (string, error) {
// Create perf results zip file
tmpArchive, e := os.CreateTemp("", "mc-perf-*.zip")
if e != nil {
return "", e
}
defer tmpArchive.Close()
zipWriter := zip.NewWriter(tmpArchive)
defer zipWriter.Close()
e = writeJSONObjToZip(zipWriter, perfOutput, resultFilename)
if e != nil {
return "", e
}
e = writeJSONObjToZip(zipWriter, regInfo, "cluster.info")
if e != nil {
return "", e
}
return tmpArchive.Name(), nil
}