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 }