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