github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/cdc/processor/memquota/mem_quota.go (about)

     1  // Copyright 2022 PingCAP, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package memquota
    15  
    16  import (
    17  	"context"
    18  	"sort"
    19  	"sync"
    20  	"sync/atomic"
    21  	"time"
    22  
    23  	"github.com/pingcap/log"
    24  	"github.com/pingcap/tiflow/cdc/model"
    25  	"github.com/pingcap/tiflow/cdc/processor/tablepb"
    26  	"github.com/pingcap/tiflow/pkg/spanz"
    27  	"github.com/prometheus/client_golang/prometheus"
    28  	"go.uber.org/zap"
    29  )
    30  
    31  // MemConsumeRecord is used to trace memory usage.
    32  type MemConsumeRecord struct {
    33  	ResolvedTs model.ResolvedTs
    34  	Size       uint64
    35  }
    36  
    37  // MemQuota is used to trace memory usage.
    38  type MemQuota struct {
    39  	changefeedID model.ChangeFeedID
    40  	// totalBytes is the total memory quota for one changefeed.
    41  	totalBytes uint64
    42  
    43  	// usedBytes is the memory usage of one changefeed.
    44  	usedBytes atomic.Uint64
    45  
    46  	// isClosed is used to indicate whether the mem quota is closed.
    47  	isClosed atomic.Bool
    48  
    49  	// To close the background metrics goroutine.
    50  	wg      sync.WaitGroup
    51  	closeBg chan struct{}
    52  
    53  	// blockAcquireCond is used to notify the blocked acquire.
    54  	blockAcquireCond *sync.Cond
    55  
    56  	metricTotal prometheus.Gauge
    57  	metricUsed  prometheus.Gauge
    58  
    59  	// mu protects the following fields.
    60  	mu sync.Mutex
    61  	// tableMemory is the memory usage of each table.
    62  	tableMemory *spanz.HashMap[[]*MemConsumeRecord]
    63  }
    64  
    65  // NewMemQuota creates a MemQuota instance.
    66  func NewMemQuota(changefeedID model.ChangeFeedID, totalBytes uint64, comp string) *MemQuota {
    67  	m := &MemQuota{
    68  		changefeedID:     changefeedID,
    69  		totalBytes:       totalBytes,
    70  		blockAcquireCond: sync.NewCond(&sync.Mutex{}),
    71  		metricTotal: MemoryQuota.WithLabelValues(changefeedID.Namespace,
    72  			changefeedID.ID, "total", comp),
    73  		metricUsed: MemoryQuota.WithLabelValues(changefeedID.Namespace,
    74  			changefeedID.ID, "used", comp),
    75  		closeBg: make(chan struct{}, 1),
    76  
    77  		tableMemory: spanz.NewHashMap[[]*MemConsumeRecord](),
    78  	}
    79  	m.metricTotal.Set(float64(totalBytes))
    80  	m.metricUsed.Set(float64(0))
    81  
    82  	log.Info("New memory quota",
    83  		zap.String("namespace", changefeedID.Namespace),
    84  		zap.String("changefeed", changefeedID.ID),
    85  		zap.Uint64("total", totalBytes))
    86  
    87  	m.wg.Add(1)
    88  	go func() {
    89  		timer := time.NewTicker(3 * time.Second)
    90  		defer timer.Stop()
    91  		for {
    92  			select {
    93  			case <-timer.C:
    94  				m.metricUsed.Set(float64(m.usedBytes.Load()))
    95  			case <-m.closeBg:
    96  				m.metricUsed.Set(0.0)
    97  				m.wg.Done()
    98  				return
    99  			}
   100  		}
   101  	}()
   102  	return m
   103  }
   104  
   105  // TryAcquire returns true if the memory quota is available, otherwise returns false.
   106  func (m *MemQuota) TryAcquire(nBytes uint64) bool {
   107  	for {
   108  		usedBytes := m.usedBytes.Load()
   109  		if usedBytes+nBytes > m.totalBytes {
   110  			return false
   111  		}
   112  		if m.usedBytes.CompareAndSwap(usedBytes, usedBytes+nBytes) {
   113  			return true
   114  		}
   115  	}
   116  }
   117  
   118  // ForceAcquire is used to force acquire the memory quota.
   119  func (m *MemQuota) ForceAcquire(nBytes uint64) {
   120  	m.usedBytes.Add(nBytes)
   121  }
   122  
   123  // BlockAcquire is used to block the request when the memory quota is not available.
   124  func (m *MemQuota) BlockAcquire(nBytes uint64) error {
   125  	for {
   126  		if m.isClosed.Load() {
   127  			return context.Canceled
   128  		}
   129  		usedBytes := m.usedBytes.Load()
   130  		if usedBytes+nBytes > m.totalBytes {
   131  			m.blockAcquireCond.L.Lock()
   132  			m.blockAcquireCond.Wait()
   133  			m.blockAcquireCond.L.Unlock()
   134  			continue
   135  		}
   136  		if m.usedBytes.CompareAndSwap(usedBytes, usedBytes+nBytes) {
   137  			return nil
   138  		}
   139  	}
   140  }
   141  
   142  // Refund directly release the memory quota.
   143  func (m *MemQuota) Refund(nBytes uint64) {
   144  	if nBytes == 0 {
   145  		return
   146  	}
   147  	usedBytes := m.usedBytes.Load()
   148  	if usedBytes < nBytes {
   149  		log.Panic("MemQuota.refund fail",
   150  			zap.Uint64("used", usedBytes), zap.Uint64("refund", nBytes))
   151  	}
   152  	if m.usedBytes.Add(^(nBytes - 1)) < m.totalBytes {
   153  		m.blockAcquireCond.Broadcast()
   154  	}
   155  }
   156  
   157  // AddTable adds a table into the quota.
   158  func (m *MemQuota) AddTable(span tablepb.Span) {
   159  	m.mu.Lock()
   160  	defer m.mu.Unlock()
   161  	m.tableMemory.ReplaceOrInsert(span, make([]*MemConsumeRecord, 0, 2))
   162  }
   163  
   164  // Record records the memory usage of a table.
   165  func (m *MemQuota) Record(span tablepb.Span, resolved model.ResolvedTs, nBytes uint64) {
   166  	if nBytes == 0 {
   167  		return
   168  	}
   169  	m.mu.Lock()
   170  	defer m.mu.Unlock()
   171  	if _, ok := m.tableMemory.Get(span); !ok {
   172  		// Can't find the table record, the table must be removed.
   173  		usedBytes := m.usedBytes.Load()
   174  		if usedBytes < nBytes {
   175  			log.Panic("MemQuota.record fail",
   176  				zap.Uint64("used", usedBytes), zap.Uint64("record", nBytes))
   177  		}
   178  		// If we cannot find the table, then the previous acquired memory quota needed to be returned.
   179  		// Note that "usedBytes.Add(^(nBytes - 1))" means "usedBytes.Sub(nBytes)". But atomic don't
   180  		// have Sub method.
   181  		if m.usedBytes.Add(^(nBytes - 1)) < m.totalBytes {
   182  			m.blockAcquireCond.Broadcast()
   183  		}
   184  		return
   185  	}
   186  	m.tableMemory.ReplaceOrInsert(span, append(m.tableMemory.GetV(span), &MemConsumeRecord{
   187  		ResolvedTs: resolved,
   188  		Size:       nBytes,
   189  	}))
   190  }
   191  
   192  // Release try to use resolvedTs to release the memory quota.
   193  // Because we append records in order, we can use binary search to find the first record
   194  // that is greater than resolvedTs, and release the memory quota of the records before it.
   195  func (m *MemQuota) Release(span tablepb.Span, resolved model.ResolvedTs) {
   196  	m.mu.Lock()
   197  	defer m.mu.Unlock()
   198  	if _, ok := m.tableMemory.Get(span); !ok {
   199  		// This can happen when
   200  		// 1. the table has no data and never been recorded.
   201  		// 2. the table is in async removing.
   202  		return
   203  	}
   204  	records := m.tableMemory.GetV(span)
   205  	i := sort.Search(len(records), func(i int) bool {
   206  		return records[i].ResolvedTs.Greater(resolved)
   207  	})
   208  	var toRelease uint64 = 0
   209  	for j := 0; j < i; j++ {
   210  		toRelease += records[j].Size
   211  	}
   212  	m.tableMemory.ReplaceOrInsert(span, records[i:])
   213  	if toRelease == 0 {
   214  		return
   215  	}
   216  
   217  	usedBytes := m.usedBytes.Load()
   218  	if usedBytes < toRelease {
   219  		log.Panic("MemQuota.release fail",
   220  			zap.Uint64("used", usedBytes), zap.Uint64("release", toRelease))
   221  	}
   222  	if m.usedBytes.Add(^(toRelease - 1)) < m.totalBytes {
   223  		m.blockAcquireCond.Broadcast()
   224  	}
   225  }
   226  
   227  // RemoveTable clears all records of the table and remove the table.
   228  // Return the cleaned memory quota.
   229  func (m *MemQuota) RemoveTable(span tablepb.Span) uint64 {
   230  	m.mu.Lock()
   231  	cleaned := m.clear(span)
   232  	m.tableMemory.Delete(span)
   233  	m.mu.Unlock()
   234  	return cleaned
   235  }
   236  
   237  // ClearTable is like RemoveTable but only clear the memory usage records but doesn't
   238  // remove the table.
   239  func (m *MemQuota) ClearTable(span tablepb.Span) uint64 {
   240  	m.mu.Lock()
   241  	cleaned := m.clear(span)
   242  	m.tableMemory.ReplaceOrInsert(span, make([]*MemConsumeRecord, 0, 2))
   243  	m.mu.Unlock()
   244  	return cleaned
   245  }
   246  
   247  func (m *MemQuota) clear(span tablepb.Span) uint64 {
   248  	if _, ok := m.tableMemory.Get(span); !ok {
   249  		// This can happen when the table has no data and never been recorded.
   250  		log.Warn("Table consumed memory records not found",
   251  			zap.String("namespace", m.changefeedID.Namespace),
   252  			zap.String("changefeed", m.changefeedID.ID),
   253  			zap.Stringer("span", &span))
   254  		return 0
   255  	}
   256  
   257  	cleaned := uint64(0)
   258  	records := m.tableMemory.GetV(span)
   259  	for _, record := range records {
   260  		cleaned += record.Size
   261  	}
   262  
   263  	if m.usedBytes.Add(^(cleaned - 1)) < m.totalBytes {
   264  		m.blockAcquireCond.Broadcast()
   265  	}
   266  	return cleaned
   267  }
   268  
   269  // Close the mem quota and notify the blocked acquire.
   270  func (m *MemQuota) Close() {
   271  	if m.isClosed.CompareAndSwap(false, true) {
   272  		m.blockAcquireCond.Broadcast()
   273  		close(m.closeBg)
   274  		m.wg.Wait()
   275  	}
   276  }
   277  
   278  // GetUsedBytes returns the used memory quota.
   279  func (m *MemQuota) GetUsedBytes() uint64 {
   280  	return m.usedBytes.Load()
   281  }
   282  
   283  // hasAvailable returns true if the memory quota is available, otherwise returns false.
   284  func (m *MemQuota) hasAvailable(nBytes uint64) bool {
   285  	return m.usedBytes.Load()+nBytes <= m.totalBytes
   286  }