You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

308 lines
9.2 KiB

  1. // Copyright (c) 2015-2024 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. package cmd
  18. import (
  19. "context"
  20. "fmt"
  21. "net/url"
  22. "runtime"
  23. "sort"
  24. "time"
  25. "github.com/minio/dperf/pkg/dperf"
  26. "github.com/minio/madmin-go/v3"
  27. "github.com/minio/minio/internal/auth"
  28. xioutil "github.com/minio/minio/internal/ioutil"
  29. )
  30. const speedTest = "speedtest"
  31. type speedTestOpts struct {
  32. objectSize int
  33. concurrencyStart int
  34. concurrency int
  35. duration time.Duration
  36. autotune bool
  37. storageClass string
  38. bucketName string
  39. enableSha256 bool
  40. enableMultipart bool
  41. creds auth.Credentials
  42. }
  43. // Get the max throughput and iops numbers.
  44. func objectSpeedTest(ctx context.Context, opts speedTestOpts) chan madmin.SpeedTestResult {
  45. ch := make(chan madmin.SpeedTestResult, 1)
  46. go func() {
  47. defer xioutil.SafeClose(ch)
  48. concurrency := opts.concurrencyStart
  49. if opts.autotune {
  50. // if we have less drives than concurrency then choose
  51. // only the concurrency to be number of drives to start
  52. // with - since default '32' might be big and may not
  53. // complete in total time of 10s.
  54. if globalEndpoints.NEndpoints() < concurrency {
  55. concurrency = globalEndpoints.NEndpoints()
  56. }
  57. // Check if we have local disks per pool less than
  58. // the concurrency make sure we choose only the "start"
  59. // concurrency to be equal to the lowest number of
  60. // local disks per server.
  61. for _, localDiskCount := range globalEndpoints.NLocalDisksPathsPerPool() {
  62. if localDiskCount < concurrency {
  63. concurrency = localDiskCount
  64. }
  65. }
  66. // Any concurrency less than '4' just stick to '4' concurrent
  67. // operations for now to begin with.
  68. if concurrency < 4 {
  69. concurrency = 4
  70. }
  71. // if GOMAXPROCS is set to a lower value then choose to use
  72. // concurrency == GOMAXPROCS instead.
  73. if runtime.GOMAXPROCS(0) < concurrency {
  74. concurrency = runtime.GOMAXPROCS(0)
  75. }
  76. }
  77. throughputHighestGet := uint64(0)
  78. throughputHighestPut := uint64(0)
  79. var throughputHighestResults []SpeedTestResult
  80. sendResult := func() {
  81. var result madmin.SpeedTestResult
  82. durationSecs := opts.duration.Seconds()
  83. result.GETStats.ThroughputPerSec = throughputHighestGet / uint64(durationSecs)
  84. result.GETStats.ObjectsPerSec = throughputHighestGet / uint64(opts.objectSize) / uint64(durationSecs)
  85. result.PUTStats.ThroughputPerSec = throughputHighestPut / uint64(durationSecs)
  86. result.PUTStats.ObjectsPerSec = throughputHighestPut / uint64(opts.objectSize) / uint64(durationSecs)
  87. var totalUploadTimes madmin.TimeDurations
  88. var totalDownloadTimes madmin.TimeDurations
  89. var totalDownloadTTFB madmin.TimeDurations
  90. for i := range len(throughputHighestResults) {
  91. errStr := ""
  92. if throughputHighestResults[i].Error != "" {
  93. errStr = throughputHighestResults[i].Error
  94. }
  95. // if the default concurrency yields zero results, throw an error.
  96. if throughputHighestResults[i].Downloads == 0 && opts.concurrencyStart == concurrency {
  97. errStr = fmt.Sprintf("no results for downloads upon first attempt, concurrency %d and duration %s",
  98. opts.concurrencyStart, opts.duration)
  99. }
  100. // if the default concurrency yields zero results, throw an error.
  101. if throughputHighestResults[i].Uploads == 0 && opts.concurrencyStart == concurrency {
  102. errStr = fmt.Sprintf("no results for uploads upon first attempt, concurrency %d and duration %s",
  103. opts.concurrencyStart, opts.duration)
  104. }
  105. result.PUTStats.Servers = append(result.PUTStats.Servers, madmin.SpeedTestStatServer{
  106. Endpoint: throughputHighestResults[i].Endpoint,
  107. ThroughputPerSec: throughputHighestResults[i].Uploads / uint64(durationSecs),
  108. ObjectsPerSec: throughputHighestResults[i].Uploads / uint64(opts.objectSize) / uint64(durationSecs),
  109. Err: errStr,
  110. })
  111. result.GETStats.Servers = append(result.GETStats.Servers, madmin.SpeedTestStatServer{
  112. Endpoint: throughputHighestResults[i].Endpoint,
  113. ThroughputPerSec: throughputHighestResults[i].Downloads / uint64(durationSecs),
  114. ObjectsPerSec: throughputHighestResults[i].Downloads / uint64(opts.objectSize) / uint64(durationSecs),
  115. Err: errStr,
  116. })
  117. totalUploadTimes = append(totalUploadTimes, throughputHighestResults[i].UploadTimes...)
  118. totalDownloadTimes = append(totalDownloadTimes, throughputHighestResults[i].DownloadTimes...)
  119. totalDownloadTTFB = append(totalDownloadTTFB, throughputHighestResults[i].DownloadTTFB...)
  120. }
  121. result.PUTStats.Response = totalUploadTimes.Measure()
  122. result.GETStats.Response = totalDownloadTimes.Measure()
  123. result.GETStats.TTFB = totalDownloadTTFB.Measure()
  124. result.Size = opts.objectSize
  125. result.Disks = globalEndpoints.NEndpoints()
  126. result.Servers = len(globalNotificationSys.peerClients) + 1
  127. result.Version = Version
  128. result.Concurrent = concurrency
  129. select {
  130. case ch <- result:
  131. case <-ctx.Done():
  132. return
  133. }
  134. }
  135. for {
  136. select {
  137. case <-ctx.Done():
  138. // If the client got disconnected stop the speedtest.
  139. return
  140. default:
  141. }
  142. sopts := speedTestOpts{
  143. objectSize: opts.objectSize,
  144. concurrency: concurrency,
  145. duration: opts.duration,
  146. storageClass: opts.storageClass,
  147. bucketName: opts.bucketName,
  148. enableSha256: opts.enableSha256,
  149. enableMultipart: opts.enableMultipart,
  150. creds: opts.creds,
  151. }
  152. results := globalNotificationSys.SpeedTest(ctx, sopts)
  153. sort.Slice(results, func(i, j int) bool {
  154. return results[i].Endpoint < results[j].Endpoint
  155. })
  156. totalPut := uint64(0)
  157. totalGet := uint64(0)
  158. for _, result := range results {
  159. totalPut += result.Uploads
  160. totalGet += result.Downloads
  161. }
  162. if totalGet < throughputHighestGet {
  163. // Following check is for situations
  164. // when Writes() scale higher than Reads()
  165. // - practically speaking this never happens
  166. // and should never happen - however it has
  167. // been seen recently due to hardware issues
  168. // causes Reads() to go slower than Writes().
  169. //
  170. // Send such results anyways as this shall
  171. // expose a problem underneath.
  172. if totalPut > throughputHighestPut {
  173. throughputHighestResults = results
  174. throughputHighestPut = totalPut
  175. // let the client see lower value as well
  176. throughputHighestGet = totalGet
  177. }
  178. sendResult()
  179. break
  180. }
  181. // We break if we did not see 2.5% growth rate in total GET
  182. // requests, we have reached our peak at this point.
  183. doBreak := float64(totalGet-throughputHighestGet)/float64(totalGet) < 0.025
  184. throughputHighestGet = totalGet
  185. throughputHighestResults = results
  186. throughputHighestPut = totalPut
  187. if doBreak {
  188. sendResult()
  189. break
  190. }
  191. for _, result := range results {
  192. if result.Error != "" {
  193. // Break out on errors.
  194. sendResult()
  195. return
  196. }
  197. }
  198. sendResult()
  199. if !opts.autotune {
  200. break
  201. }
  202. // Try with a higher concurrency to see if we get better throughput
  203. concurrency += (concurrency + 1) / 2
  204. }
  205. }()
  206. return ch
  207. }
  208. func driveSpeedTest(ctx context.Context, opts madmin.DriveSpeedTestOpts) madmin.DriveSpeedTestResult {
  209. perf := &dperf.DrivePerf{
  210. Serial: opts.Serial,
  211. BlockSize: opts.BlockSize,
  212. FileSize: opts.FileSize,
  213. }
  214. localPaths := globalEndpoints.LocalDisksPaths()
  215. var ignoredPaths []string
  216. paths := func() (tmpPaths []string) {
  217. for _, lp := range localPaths {
  218. if _, err := Lstat(pathJoin(lp, minioMetaBucket, formatConfigFile)); err == nil {
  219. tmpPaths = append(tmpPaths, pathJoin(lp, minioMetaTmpBucket))
  220. } else {
  221. // Use dperf on only formatted drives.
  222. ignoredPaths = append(ignoredPaths, lp)
  223. }
  224. }
  225. return tmpPaths
  226. }()
  227. scheme := "http"
  228. if globalIsTLS {
  229. scheme = "https"
  230. }
  231. u := &url.URL{
  232. Scheme: scheme,
  233. Host: globalLocalNodeName,
  234. }
  235. perfs, err := perf.Run(ctx, paths...)
  236. return madmin.DriveSpeedTestResult{
  237. Endpoint: u.String(),
  238. Version: Version,
  239. DrivePerf: func() (results []madmin.DrivePerf) {
  240. for idx, r := range perfs {
  241. result := madmin.DrivePerf{
  242. Path: localPaths[idx],
  243. ReadThroughput: r.ReadThroughput,
  244. WriteThroughput: r.WriteThroughput,
  245. Error: func() string {
  246. if r.Error != nil {
  247. return r.Error.Error()
  248. }
  249. return ""
  250. }(),
  251. }
  252. results = append(results, result)
  253. }
  254. for _, inp := range ignoredPaths {
  255. results = append(results, madmin.DrivePerf{
  256. Path: inp,
  257. Error: errFaultyDisk.Error(),
  258. })
  259. }
  260. return results
  261. }(),
  262. Error: func() string {
  263. if err != nil {
  264. return err.Error()
  265. }
  266. return ""
  267. }(),
  268. }
  269. }