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  }