google.golang.org/grpc@v1.74.2/xds/internal/clients/lrsclient/load_store.go (about) 1 /* 2 * 3 * Copyright 2025 gRPC authors. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 * 17 */ 18 19 package lrsclient 20 21 import ( 22 "context" 23 "sync" 24 "sync/atomic" 25 "time" 26 27 "google.golang.org/grpc/xds/internal/clients" 28 lrsclientinternal "google.golang.org/grpc/xds/internal/clients/lrsclient/internal" 29 ) 30 31 // A LoadStore aggregates loads for multiple clusters and services that are 32 // intended to be reported via LRS. 33 // 34 // LoadStore stores loads reported to a single LRS server. Use multiple stores 35 // for multiple servers. 36 // 37 // It is safe for concurrent use. 38 type LoadStore struct { 39 // stop is the function to call to Stop the LoadStore reporting. 40 stop func(ctx context.Context) 41 42 // mu only protects the map (2 layers). The read/write to 43 // *PerClusterReporter doesn't need to hold the mu. 44 mu sync.Mutex 45 // clusters is a map with cluster name as the key. The second layer is a 46 // map with service name as the key. Each value (PerClusterReporter) 47 // contains data for a (cluster, service) pair. 48 // 49 // Note that new entries are added to this map, but never removed. This is 50 // potentially a memory leak. But the memory is allocated for each new 51 // (cluster,service) pair, and the memory allocated is just pointers and 52 // maps. So this shouldn't get too bad. 53 clusters map[string]map[string]*PerClusterReporter 54 } 55 56 func init() { 57 lrsclientinternal.TimeNow = time.Now 58 } 59 60 // newLoadStore creates a LoadStore. 61 func newLoadStore() *LoadStore { 62 return &LoadStore{ 63 clusters: make(map[string]map[string]*PerClusterReporter), 64 } 65 } 66 67 // Stop signals the LoadStore to stop reporting. 68 // 69 // Before closing the underlying LRS stream, this method may block until a 70 // final load report send attempt completes or the provided context `ctx` 71 // expires. 72 // 73 // The provided context must have a deadline or timeout set to prevent Stop 74 // from blocking indefinitely if the final send attempt fails to complete. 75 // 76 // Calling Stop on an already stopped LoadStore is a no-op. 77 func (ls *LoadStore) Stop(ctx context.Context) { 78 ls.stop(ctx) 79 } 80 81 // ReporterForCluster returns the PerClusterReporter for the given cluster and 82 // service. 83 func (ls *LoadStore) ReporterForCluster(clusterName, serviceName string) *PerClusterReporter { 84 ls.mu.Lock() 85 defer ls.mu.Unlock() 86 c, ok := ls.clusters[clusterName] 87 if !ok { 88 c = make(map[string]*PerClusterReporter) 89 ls.clusters[clusterName] = c 90 } 91 92 if p, ok := c[serviceName]; ok { 93 return p 94 } 95 p := &PerClusterReporter{ 96 cluster: clusterName, 97 service: serviceName, 98 lastLoadReportAt: lrsclientinternal.TimeNow(), 99 } 100 c[serviceName] = p 101 return p 102 } 103 104 // stats returns the load data for the given cluster names. Data is returned in 105 // a slice with no specific order. 106 // 107 // If no clusterName is given (an empty slice), all data for all known clusters 108 // is returned. 109 // 110 // If a cluster's loadData is empty (no load to report), it's not appended to 111 // the returned slice. 112 func (ls *LoadStore) stats(clusterNames []string) []*loadData { 113 ls.mu.Lock() 114 defer ls.mu.Unlock() 115 116 var ret []*loadData 117 if len(clusterNames) == 0 { 118 for _, c := range ls.clusters { 119 ret = appendClusterStats(ret, c) 120 } 121 return ret 122 } 123 for _, n := range clusterNames { 124 if c, ok := ls.clusters[n]; ok { 125 ret = appendClusterStats(ret, c) 126 } 127 } 128 129 return ret 130 } 131 132 // PerClusterReporter records load data pertaining to a single cluster. It 133 // provides methods to record call starts, finishes, server-reported loads, 134 // and dropped calls. 135 // 136 // It is safe for concurrent use. 137 // 138 // TODO(purnesh42h): Use regular maps with mutexes instead of sync.Map here. 139 // The latter is optimized for two common use cases: (1) when the entry for a 140 // given key is only ever written once but read many times, as in caches that 141 // only grow, or (2) when multiple goroutines read, write, and overwrite 142 // entries for disjoint sets of keys. In these two cases, use of a Map may 143 // significantly reduce lock contention compared to a Go map paired with a 144 // separate Mutex or RWMutex. 145 // Neither of these conditions are met here, and we should transition to a 146 // regular map with a mutex for better type safety. 147 type PerClusterReporter struct { 148 cluster, service string 149 drops sync.Map // map[string]*uint64 150 localityRPCCount sync.Map // map[clients.Locality]*rpcCountData 151 152 mu sync.Mutex 153 lastLoadReportAt time.Time 154 } 155 156 // CallStarted records a call started in the LoadStore. 157 func (p *PerClusterReporter) CallStarted(locality clients.Locality) { 158 s, ok := p.localityRPCCount.Load(locality) 159 if !ok { 160 tp := newRPCCountData() 161 s, _ = p.localityRPCCount.LoadOrStore(locality, tp) 162 } 163 s.(*rpcCountData).incrInProgress() 164 s.(*rpcCountData).incrIssued() 165 } 166 167 // CallFinished records a call finished in the LoadStore. 168 func (p *PerClusterReporter) CallFinished(locality clients.Locality, err error) { 169 f, ok := p.localityRPCCount.Load(locality) 170 if !ok { 171 // The map is never cleared, only values in the map are reset. So the 172 // case where entry for call-finish is not found should never happen. 173 return 174 } 175 f.(*rpcCountData).decrInProgress() 176 if err == nil { 177 f.(*rpcCountData).incrSucceeded() 178 } else { 179 f.(*rpcCountData).incrErrored() 180 } 181 } 182 183 // CallServerLoad records the server load in the LoadStore. 184 func (p *PerClusterReporter) CallServerLoad(locality clients.Locality, name string, val float64) { 185 s, ok := p.localityRPCCount.Load(locality) 186 if !ok { 187 // The map is never cleared, only values in the map are reset. So the 188 // case where entry for callServerLoad is not found should never happen. 189 return 190 } 191 s.(*rpcCountData).addServerLoad(name, val) 192 } 193 194 // CallDropped records a call dropped in the LoadStore. 195 func (p *PerClusterReporter) CallDropped(category string) { 196 d, ok := p.drops.Load(category) 197 if !ok { 198 tp := new(uint64) 199 d, _ = p.drops.LoadOrStore(category, tp) 200 } 201 atomic.AddUint64(d.(*uint64), 1) 202 } 203 204 // stats returns and resets all loads reported to the store, except inProgress 205 // rpc counts. 206 // 207 // It returns nil if the store doesn't contain any (new) data. 208 func (p *PerClusterReporter) stats() *loadData { 209 sd := newLoadData(p.cluster, p.service) 210 p.drops.Range(func(key, val any) bool { 211 d := atomic.SwapUint64(val.(*uint64), 0) 212 if d == 0 { 213 return true 214 } 215 sd.totalDrops += d 216 keyStr := key.(string) 217 if keyStr != "" { 218 // Skip drops without category. They are counted in total_drops, but 219 // not in per category. One example is drops by circuit breaking. 220 sd.drops[keyStr] = d 221 } 222 return true 223 }) 224 p.localityRPCCount.Range(func(key, val any) bool { 225 countData := val.(*rpcCountData) 226 succeeded := countData.loadAndClearSucceeded() 227 inProgress := countData.loadInProgress() 228 errored := countData.loadAndClearErrored() 229 issued := countData.loadAndClearIssued() 230 if succeeded == 0 && inProgress == 0 && errored == 0 && issued == 0 { 231 return true 232 } 233 234 ld := localityData{ 235 requestStats: requestData{ 236 succeeded: succeeded, 237 errored: errored, 238 inProgress: inProgress, 239 issued: issued, 240 }, 241 loadStats: make(map[string]serverLoadData), 242 } 243 countData.serverLoads.Range(func(key, val any) bool { 244 sum, count := val.(*rpcLoadData).loadAndClear() 245 if count == 0 { 246 return true 247 } 248 ld.loadStats[key.(string)] = serverLoadData{ 249 count: count, 250 sum: sum, 251 } 252 return true 253 }) 254 sd.localityStats[key.(clients.Locality)] = ld 255 return true 256 }) 257 258 p.mu.Lock() 259 sd.reportInterval = lrsclientinternal.TimeNow().Sub(p.lastLoadReportAt) 260 p.lastLoadReportAt = lrsclientinternal.TimeNow() 261 p.mu.Unlock() 262 263 if sd.totalDrops == 0 && len(sd.drops) == 0 && len(sd.localityStats) == 0 { 264 return nil 265 } 266 return sd 267 } 268 269 // loadData contains all load data reported to the LoadStore since the most recent 270 // call to stats(). 271 type loadData struct { 272 // cluster is the name of the cluster this data is for. 273 cluster string 274 // service is the name of the EDS service this data is for. 275 service string 276 // totalDrops is the total number of dropped requests. 277 totalDrops uint64 278 // drops is the number of dropped requests per category. 279 drops map[string]uint64 280 // localityStats contains load reports per locality. 281 localityStats map[clients.Locality]localityData 282 // reportInternal is the duration since last time load was reported (stats() 283 // was called). 284 reportInterval time.Duration 285 } 286 287 // localityData contains load data for a single locality. 288 type localityData struct { 289 // requestStats contains counts of requests made to the locality. 290 requestStats requestData 291 // loadStats contains server load data for requests made to the locality, 292 // indexed by the load type. 293 loadStats map[string]serverLoadData 294 } 295 296 // requestData contains request counts. 297 type requestData struct { 298 // succeeded is the number of succeeded requests. 299 succeeded uint64 300 // errored is the number of requests which ran into errors. 301 errored uint64 302 // inProgress is the number of requests in flight. 303 inProgress uint64 304 // issued is the total number requests that were sent. 305 issued uint64 306 } 307 308 // serverLoadData contains server load data. 309 type serverLoadData struct { 310 // count is the number of load reports. 311 count uint64 312 // sum is the total value of all load reports. 313 sum float64 314 } 315 316 // appendClusterStats gets the Data for all the given clusters, append to ret, 317 // and return the new slice. 318 // 319 // Data is only appended to ret if it's not empty. 320 func appendClusterStats(ret []*loadData, clusters map[string]*PerClusterReporter) []*loadData { 321 for _, d := range clusters { 322 data := d.stats() 323 if data == nil { 324 // Skip this data if it doesn't contain any information. 325 continue 326 } 327 ret = append(ret, data) 328 } 329 return ret 330 } 331 332 func newLoadData(cluster, service string) *loadData { 333 return &loadData{ 334 cluster: cluster, 335 service: service, 336 drops: make(map[string]uint64), 337 localityStats: make(map[clients.Locality]localityData), 338 } 339 } 340 341 type rpcCountData struct { 342 // Only atomic accesses are allowed for the fields. 343 succeeded *uint64 344 errored *uint64 345 inProgress *uint64 346 issued *uint64 347 348 // Map from load desc to load data (sum+count). Loading data from map is 349 // atomic, but updating data takes a lock, which could cause contention when 350 // multiple RPCs try to report loads for the same desc. 351 // 352 // To fix the contention, shard this map. 353 serverLoads sync.Map // map[string]*rpcLoadData 354 } 355 356 func newRPCCountData() *rpcCountData { 357 return &rpcCountData{ 358 succeeded: new(uint64), 359 errored: new(uint64), 360 inProgress: new(uint64), 361 issued: new(uint64), 362 } 363 } 364 365 func (rcd *rpcCountData) incrSucceeded() { 366 atomic.AddUint64(rcd.succeeded, 1) 367 } 368 369 func (rcd *rpcCountData) loadAndClearSucceeded() uint64 { 370 return atomic.SwapUint64(rcd.succeeded, 0) 371 } 372 373 func (rcd *rpcCountData) incrErrored() { 374 atomic.AddUint64(rcd.errored, 1) 375 } 376 377 func (rcd *rpcCountData) loadAndClearErrored() uint64 { 378 return atomic.SwapUint64(rcd.errored, 0) 379 } 380 381 func (rcd *rpcCountData) incrInProgress() { 382 atomic.AddUint64(rcd.inProgress, 1) 383 } 384 385 func (rcd *rpcCountData) decrInProgress() { 386 atomic.AddUint64(rcd.inProgress, ^uint64(0)) // atomic.Add(x, -1) 387 } 388 389 func (rcd *rpcCountData) loadInProgress() uint64 { 390 return atomic.LoadUint64(rcd.inProgress) // InProgress count is not clear when reading. 391 } 392 393 func (rcd *rpcCountData) incrIssued() { 394 atomic.AddUint64(rcd.issued, 1) 395 } 396 397 func (rcd *rpcCountData) loadAndClearIssued() uint64 { 398 return atomic.SwapUint64(rcd.issued, 0) 399 } 400 401 func (rcd *rpcCountData) addServerLoad(name string, d float64) { 402 loads, ok := rcd.serverLoads.Load(name) 403 if !ok { 404 tl := newRPCLoadData() 405 loads, _ = rcd.serverLoads.LoadOrStore(name, tl) 406 } 407 loads.(*rpcLoadData).add(d) 408 } 409 410 // rpcLoadData is data for server loads (from trailers or oob). Fields in this 411 // struct must be updated consistently. 412 // 413 // The current solution is to hold a lock, which could cause contention. To fix, 414 // shard serverLoads map in rpcCountData. 415 type rpcLoadData struct { 416 mu sync.Mutex 417 sum float64 418 count uint64 419 } 420 421 func newRPCLoadData() *rpcLoadData { 422 return &rpcLoadData{} 423 } 424 425 func (rld *rpcLoadData) add(v float64) { 426 rld.mu.Lock() 427 rld.sum += v 428 rld.count++ 429 rld.mu.Unlock() 430 } 431 432 func (rld *rpcLoadData) loadAndClear() (s float64, c uint64) { 433 rld.mu.Lock() 434 s, rld.sum = rld.sum, 0 435 c, rld.count = rld.count, 0 436 rld.mu.Unlock() 437 return s, c 438 }