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  }