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  }