github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/parallel-manager.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 "os" 22 "runtime" 23 "strconv" 24 "sync" 25 "sync/atomic" 26 "time" 27 28 "github.com/minio/minio-go/v7" 29 "github.com/shirou/gopsutil/v3/mem" 30 ) 31 32 const ( 33 // Maximum number of parallel workers 34 maxParallelWorkers = 128 35 36 // Monitor tick to decide to add new workers 37 monitorPeriod = 4 * time.Second 38 ) 39 40 // Number of workers added per bandwidth monitoring. 41 var defaultWorkerFactor = runtime.GOMAXPROCS(0) 42 43 // A task is a copy/mirror action that needs to be executed 44 type task struct { 45 // The function to execute in this task 46 fn func() URLs 47 // If set to true, ensure no tasks are 48 // executed in parallel to this one. 49 barrier bool 50 // The total size of the information that we need to upload 51 uploadSize int64 52 } 53 54 // ParallelManager - helps manage parallel workers to run tasks 55 type ParallelManager struct { 56 // Calculate sent bytes. 57 // Keep this as first element of struct because it guarantees 64bit 58 // alignment on 32 bit machines. atomic.* functions crash if operand is not 59 // aligned at 64bit. See https://github.com/golang/go/issues/599 60 sentBytes int64 61 62 // Synchronize workers 63 wg *sync.WaitGroup 64 barrierSync sync.RWMutex 65 66 // Current threads number 67 workersNum uint32 68 69 // Channel to receive tasks to run 70 queueCh chan task 71 72 // Channel to send back results 73 resultCh chan URLs 74 75 stopMonitorCh chan struct{} 76 77 // The maximum memory to use 78 maxMem uint64 79 } 80 81 // addWorker creates a new worker to process tasks 82 func (p *ParallelManager) addWorker() { 83 if atomic.LoadUint32(&p.workersNum) >= maxParallelWorkers { 84 // Number of maximum workers is reached, no need to 85 // to create a new one. 86 return 87 } 88 89 // Update number of threads 90 atomic.AddUint32(&p.workersNum, 1) 91 92 // Start a new worker 93 p.wg.Add(1) 94 go func() { 95 for { 96 // Wait for jobs 97 t, ok := <-p.queueCh 98 if !ok { 99 // No more tasks, quit 100 p.wg.Done() 101 return 102 } 103 104 // Execute the task and send the result to channel. 105 p.resultCh <- t.fn() 106 107 if t.barrier { 108 p.barrierSync.Unlock() 109 } else { 110 p.barrierSync.RUnlock() 111 } 112 } 113 }() 114 } 115 116 func (p *ParallelManager) Read(b []byte) (n int, err error) { 117 atomic.AddInt64(&p.sentBytes, int64(len(b))) 118 return len(b), nil 119 } 120 121 // monitorProgress monitors realtime transfer speed of data 122 // and increases threads until it reaches a maximum number of 123 // threads or notice there is no apparent enhancement of 124 // transfer speed. 125 func (p *ParallelManager) monitorProgress() { 126 go func() { 127 ticker := time.NewTicker(monitorPeriod) 128 defer ticker.Stop() 129 130 var prevSentBytes, maxBandwidth int64 131 var retry int 132 133 for { 134 select { 135 case <-p.stopMonitorCh: 136 // Ordered to quit immediately 137 return 138 case <-ticker.C: 139 // Compute new bandwidth from counted sent bytes 140 sentBytes := atomic.LoadInt64(&p.sentBytes) 141 bandwidth := sentBytes - prevSentBytes 142 prevSentBytes = sentBytes 143 144 if bandwidth <= maxBandwidth { 145 retry++ 146 // We still want to add more workers 147 // until we are sure that it is not 148 // useful to add more of them. 149 if retry > 2 { 150 return 151 } 152 } else { 153 retry = 0 154 maxBandwidth = bandwidth 155 } 156 157 for i := 0; i < defaultWorkerFactor; i++ { 158 p.addWorker() 159 } 160 } 161 } 162 }() 163 } 164 165 // Queue task in parallel 166 func (p *ParallelManager) queueTask(fn func() URLs, uploadSize int64) { 167 p.doQueueTask(task{fn: fn, uploadSize: uploadSize}) 168 } 169 170 // Queue task but ensures that no tasks is running at parallel, 171 // which also means wait until all concurrent tasks finish before 172 // queueing this and execute it solely. 173 func (p *ParallelManager) queueTaskWithBarrier(fn func() URLs, uploadSize int64) { 174 p.doQueueTask(task{fn: fn, barrier: true, uploadSize: uploadSize}) 175 } 176 177 func (p *ParallelManager) enoughMemForUpload(uploadSize int64) bool { 178 if uploadSize < 0 { 179 panic("unexpected size") 180 } 181 182 if uploadSize == 0 || p.maxMem == 0 { 183 return true 184 } 185 186 estimateNeededMemoryForUpload := func(size int64) uint64 { 187 partsCount, partSize, _, e := minio.OptimalPartInfo(size, 0) 188 if e != nil { 189 panic(e) 190 } 191 if partsCount >= 4 { 192 return 4 * uint64(partSize) 193 } 194 return uint64(size) 195 } 196 197 smem := runtime.MemStats{} 198 if uploadSize > 50<<20 { 199 // GC if upload is bigger than 50MB. 200 runtime.GC() 201 } 202 runtime.ReadMemStats(&smem) 203 204 return estimateNeededMemoryForUpload(uploadSize)+smem.Alloc < p.maxMem 205 } 206 207 func (p *ParallelManager) doQueueTask(t task) { 208 // Check if we have enough memory to perform next task, 209 // if not, wait to finish all currents tasks to continue 210 if !p.enoughMemForUpload(t.uploadSize) { 211 t.barrier = true 212 } 213 if t.barrier { 214 p.barrierSync.Lock() 215 } else { 216 p.barrierSync.RLock() 217 } 218 p.queueCh <- t 219 } 220 221 // Wait for all workers to finish tasks before shutting down Parallel 222 func (p *ParallelManager) stopAndWait() { 223 close(p.queueCh) 224 p.wg.Wait() 225 close(p.stopMonitorCh) 226 } 227 228 const cgroupLimitFile = "/sys/fs/cgroup/memory/memory.limit_in_bytes" 229 230 func cgroupLimit(limitFile string) (limit uint64) { 231 buf, e := os.ReadFile(limitFile) 232 if e != nil { 233 return 9223372036854771712 234 } 235 limit, e = strconv.ParseUint(string(buf), 10, 64) 236 if e != nil { 237 return 9223372036854771712 238 } 239 return limit 240 } 241 242 func availableMemory() (available uint64) { 243 available = 4 << 30 // Default to 4 GiB when we can't find the limits. 244 245 if runtime.GOOS == "linux" { 246 available = cgroupLimit(cgroupLimitFile) 247 248 // No limit set, It's the highest positive signed 64-bit 249 // integer (2^63-1), rounded down to multiples of 4096 (2^12), 250 // the most common page size on x86 systems - for cgroup_limits. 251 if available != 9223372036854771712 { 252 // This means cgroup memory limit is configured. 253 return 254 } // no-limit set proceed to set the limits based on virtual memory. 255 256 } // for all other platforms limits are based on virtual memory. 257 258 memStats, _ := mem.VirtualMemory() 259 if memStats.Available > 0 { 260 available = memStats.Available 261 } 262 263 // Always use 50% of available memory. 264 available = available / 2 265 return 266 } 267 268 // newParallelManager starts new workers waiting for executing tasks 269 func newParallelManager(resultCh chan URLs) *ParallelManager { 270 p := &ParallelManager{ 271 wg: &sync.WaitGroup{}, 272 workersNum: 0, 273 stopMonitorCh: make(chan struct{}), 274 queueCh: make(chan task), 275 resultCh: resultCh, 276 maxMem: availableMemory(), 277 } 278 279 // Start with runtime.NumCPU(). 280 for i := 0; i < runtime.NumCPU(); i++ { 281 p.addWorker() 282 } 283 284 // Start monitoring tasks progress 285 p.monitorProgress() 286 287 return p 288 }