Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (push) Failing after 1s
Testing / vet-proto (push) Failing after 1s
Testing / tests (arm, latest) (push) Waiting to run
Testing / extras (latest) (push) Failing after 1s
Testing / static checks (latest-1) (push) Failing after 1s
Testing / tests (-race, latest) (push) Failing after 1s
Testing / tests (i386, latest) (push) Failing after 1s
Testing / tests (latest) (push) Failing after 1s
Testing / tests (latest-1) (push) Failing after 1s
Lock Threads / lock (push) Failing after 1s
govulncheck / govulncheck (.) (push) Failing after 1s
govulncheck / govulncheck (./cmd/protoc-gen-go-grpc) (push) Failing after 1s
govulncheck / govulncheck (./gcp/observability) (push) Failing after 1s
govulncheck / govulncheck (./security/advancedtls) (push) Failing after 1s
govulncheck / govulncheck (./stats/opencensus) (push) Failing after 1s
Stale bot / stale (push) Failing after 1s
439 lines
12 KiB
Go
439 lines
12 KiB
Go
/*
|
|
*
|
|
* Copyright 2025 gRPC authors.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*
|
|
*/
|
|
|
|
package lrsclient
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"google.golang.org/grpc/internal/xds/clients"
|
|
lrsclientinternal "google.golang.org/grpc/internal/xds/clients/lrsclient/internal"
|
|
)
|
|
|
|
// A LoadStore aggregates loads for multiple clusters and services that are
|
|
// intended to be reported via LRS.
|
|
//
|
|
// LoadStore stores loads reported to a single LRS server. Use multiple stores
|
|
// for multiple servers.
|
|
//
|
|
// It is safe for concurrent use.
|
|
type LoadStore struct {
|
|
// stop is the function to call to Stop the LoadStore reporting.
|
|
stop func(ctx context.Context)
|
|
|
|
// mu only protects the map (2 layers). The read/write to
|
|
// *PerClusterReporter doesn't need to hold the mu.
|
|
mu sync.Mutex
|
|
// clusters is a map with cluster name as the key. The second layer is a
|
|
// map with service name as the key. Each value (PerClusterReporter)
|
|
// contains data for a (cluster, service) pair.
|
|
//
|
|
// Note that new entries are added to this map, but never removed. This is
|
|
// potentially a memory leak. But the memory is allocated for each new
|
|
// (cluster,service) pair, and the memory allocated is just pointers and
|
|
// maps. So this shouldn't get too bad.
|
|
clusters map[string]map[string]*PerClusterReporter
|
|
}
|
|
|
|
func init() {
|
|
lrsclientinternal.TimeNow = time.Now
|
|
}
|
|
|
|
// newLoadStore creates a LoadStore.
|
|
func newLoadStore() *LoadStore {
|
|
return &LoadStore{
|
|
clusters: make(map[string]map[string]*PerClusterReporter),
|
|
}
|
|
}
|
|
|
|
// Stop signals the LoadStore to stop reporting.
|
|
//
|
|
// Before closing the underlying LRS stream, this method may block until a
|
|
// final load report send attempt completes or the provided context `ctx`
|
|
// expires.
|
|
//
|
|
// The provided context must have a deadline or timeout set to prevent Stop
|
|
// from blocking indefinitely if the final send attempt fails to complete.
|
|
//
|
|
// Calling Stop on an already stopped LoadStore is a no-op.
|
|
func (ls *LoadStore) Stop(ctx context.Context) {
|
|
ls.stop(ctx)
|
|
}
|
|
|
|
// ReporterForCluster returns the PerClusterReporter for the given cluster and
|
|
// service.
|
|
func (ls *LoadStore) ReporterForCluster(clusterName, serviceName string) *PerClusterReporter {
|
|
ls.mu.Lock()
|
|
defer ls.mu.Unlock()
|
|
c, ok := ls.clusters[clusterName]
|
|
if !ok {
|
|
c = make(map[string]*PerClusterReporter)
|
|
ls.clusters[clusterName] = c
|
|
}
|
|
|
|
if p, ok := c[serviceName]; ok {
|
|
return p
|
|
}
|
|
p := &PerClusterReporter{
|
|
cluster: clusterName,
|
|
service: serviceName,
|
|
lastLoadReportAt: lrsclientinternal.TimeNow(),
|
|
}
|
|
c[serviceName] = p
|
|
return p
|
|
}
|
|
|
|
// stats returns the load data for the given cluster names. Data is returned in
|
|
// a slice with no specific order.
|
|
//
|
|
// If no clusterName is given (an empty slice), all data for all known clusters
|
|
// is returned.
|
|
//
|
|
// If a cluster's loadData is empty (no load to report), it's not appended to
|
|
// the returned slice.
|
|
func (ls *LoadStore) stats(clusterNames []string) []*loadData {
|
|
ls.mu.Lock()
|
|
defer ls.mu.Unlock()
|
|
|
|
var ret []*loadData
|
|
if len(clusterNames) == 0 {
|
|
for _, c := range ls.clusters {
|
|
ret = appendClusterStats(ret, c)
|
|
}
|
|
return ret
|
|
}
|
|
for _, n := range clusterNames {
|
|
if c, ok := ls.clusters[n]; ok {
|
|
ret = appendClusterStats(ret, c)
|
|
}
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
// PerClusterReporter records load data pertaining to a single cluster. It
|
|
// provides methods to record call starts, finishes, server-reported loads,
|
|
// and dropped calls.
|
|
//
|
|
// It is safe for concurrent use.
|
|
//
|
|
// TODO(purnesh42h): Use regular maps with mutexes instead of sync.Map here.
|
|
// The latter is optimized for two common use cases: (1) when the entry for a
|
|
// given key is only ever written once but read many times, as in caches that
|
|
// only grow, or (2) when multiple goroutines read, write, and overwrite
|
|
// entries for disjoint sets of keys. In these two cases, use of a Map may
|
|
// significantly reduce lock contention compared to a Go map paired with a
|
|
// separate Mutex or RWMutex.
|
|
// Neither of these conditions are met here, and we should transition to a
|
|
// regular map with a mutex for better type safety.
|
|
type PerClusterReporter struct {
|
|
cluster, service string
|
|
drops sync.Map // map[string]*uint64
|
|
localityRPCCount sync.Map // map[clients.Locality]*rpcCountData
|
|
|
|
mu sync.Mutex
|
|
lastLoadReportAt time.Time
|
|
}
|
|
|
|
// CallStarted records a call started in the LoadStore.
|
|
func (p *PerClusterReporter) CallStarted(locality clients.Locality) {
|
|
s, ok := p.localityRPCCount.Load(locality)
|
|
if !ok {
|
|
tp := newRPCCountData()
|
|
s, _ = p.localityRPCCount.LoadOrStore(locality, tp)
|
|
}
|
|
s.(*rpcCountData).incrInProgress()
|
|
s.(*rpcCountData).incrIssued()
|
|
}
|
|
|
|
// CallFinished records a call finished in the LoadStore.
|
|
func (p *PerClusterReporter) CallFinished(locality clients.Locality, err error) {
|
|
f, ok := p.localityRPCCount.Load(locality)
|
|
if !ok {
|
|
// The map is never cleared, only values in the map are reset. So the
|
|
// case where entry for call-finish is not found should never happen.
|
|
return
|
|
}
|
|
f.(*rpcCountData).decrInProgress()
|
|
if err == nil {
|
|
f.(*rpcCountData).incrSucceeded()
|
|
} else {
|
|
f.(*rpcCountData).incrErrored()
|
|
}
|
|
}
|
|
|
|
// CallServerLoad records the server load in the LoadStore.
|
|
func (p *PerClusterReporter) CallServerLoad(locality clients.Locality, name string, val float64) {
|
|
s, ok := p.localityRPCCount.Load(locality)
|
|
if !ok {
|
|
// The map is never cleared, only values in the map are reset. So the
|
|
// case where entry for callServerLoad is not found should never happen.
|
|
return
|
|
}
|
|
s.(*rpcCountData).addServerLoad(name, val)
|
|
}
|
|
|
|
// CallDropped records a call dropped in the LoadStore.
|
|
func (p *PerClusterReporter) CallDropped(category string) {
|
|
d, ok := p.drops.Load(category)
|
|
if !ok {
|
|
tp := new(uint64)
|
|
d, _ = p.drops.LoadOrStore(category, tp)
|
|
}
|
|
atomic.AddUint64(d.(*uint64), 1)
|
|
}
|
|
|
|
// stats returns and resets all loads reported to the store, except inProgress
|
|
// rpc counts.
|
|
//
|
|
// It returns nil if the store doesn't contain any (new) data.
|
|
func (p *PerClusterReporter) stats() *loadData {
|
|
sd := newLoadData(p.cluster, p.service)
|
|
p.drops.Range(func(key, val any) bool {
|
|
d := atomic.SwapUint64(val.(*uint64), 0)
|
|
if d == 0 {
|
|
return true
|
|
}
|
|
sd.totalDrops += d
|
|
keyStr := key.(string)
|
|
if keyStr != "" {
|
|
// Skip drops without category. They are counted in total_drops, but
|
|
// not in per category. One example is drops by circuit breaking.
|
|
sd.drops[keyStr] = d
|
|
}
|
|
return true
|
|
})
|
|
p.localityRPCCount.Range(func(key, val any) bool {
|
|
countData := val.(*rpcCountData)
|
|
succeeded := countData.loadAndClearSucceeded()
|
|
inProgress := countData.loadInProgress()
|
|
errored := countData.loadAndClearErrored()
|
|
issued := countData.loadAndClearIssued()
|
|
if succeeded == 0 && inProgress == 0 && errored == 0 && issued == 0 {
|
|
return true
|
|
}
|
|
|
|
ld := localityData{
|
|
requestStats: requestData{
|
|
succeeded: succeeded,
|
|
errored: errored,
|
|
inProgress: inProgress,
|
|
issued: issued,
|
|
},
|
|
loadStats: make(map[string]serverLoadData),
|
|
}
|
|
countData.serverLoads.Range(func(key, val any) bool {
|
|
sum, count := val.(*rpcLoadData).loadAndClear()
|
|
if count == 0 {
|
|
return true
|
|
}
|
|
ld.loadStats[key.(string)] = serverLoadData{
|
|
count: count,
|
|
sum: sum,
|
|
}
|
|
return true
|
|
})
|
|
sd.localityStats[key.(clients.Locality)] = ld
|
|
return true
|
|
})
|
|
|
|
p.mu.Lock()
|
|
sd.reportInterval = lrsclientinternal.TimeNow().Sub(p.lastLoadReportAt)
|
|
p.lastLoadReportAt = lrsclientinternal.TimeNow()
|
|
p.mu.Unlock()
|
|
|
|
if sd.totalDrops == 0 && len(sd.drops) == 0 && len(sd.localityStats) == 0 {
|
|
return nil
|
|
}
|
|
return sd
|
|
}
|
|
|
|
// loadData contains all load data reported to the LoadStore since the most recent
|
|
// call to stats().
|
|
type loadData struct {
|
|
// cluster is the name of the cluster this data is for.
|
|
cluster string
|
|
// service is the name of the EDS service this data is for.
|
|
service string
|
|
// totalDrops is the total number of dropped requests.
|
|
totalDrops uint64
|
|
// drops is the number of dropped requests per category.
|
|
drops map[string]uint64
|
|
// localityStats contains load reports per locality.
|
|
localityStats map[clients.Locality]localityData
|
|
// reportInternal is the duration since last time load was reported (stats()
|
|
// was called).
|
|
reportInterval time.Duration
|
|
}
|
|
|
|
// localityData contains load data for a single locality.
|
|
type localityData struct {
|
|
// requestStats contains counts of requests made to the locality.
|
|
requestStats requestData
|
|
// loadStats contains server load data for requests made to the locality,
|
|
// indexed by the load type.
|
|
loadStats map[string]serverLoadData
|
|
}
|
|
|
|
// requestData contains request counts.
|
|
type requestData struct {
|
|
// succeeded is the number of succeeded requests.
|
|
succeeded uint64
|
|
// errored is the number of requests which ran into errors.
|
|
errored uint64
|
|
// inProgress is the number of requests in flight.
|
|
inProgress uint64
|
|
// issued is the total number requests that were sent.
|
|
issued uint64
|
|
}
|
|
|
|
// serverLoadData contains server load data.
|
|
type serverLoadData struct {
|
|
// count is the number of load reports.
|
|
count uint64
|
|
// sum is the total value of all load reports.
|
|
sum float64
|
|
}
|
|
|
|
// appendClusterStats gets the Data for all the given clusters, append to ret,
|
|
// and return the new slice.
|
|
//
|
|
// Data is only appended to ret if it's not empty.
|
|
func appendClusterStats(ret []*loadData, clusters map[string]*PerClusterReporter) []*loadData {
|
|
for _, d := range clusters {
|
|
data := d.stats()
|
|
if data == nil {
|
|
// Skip this data if it doesn't contain any information.
|
|
continue
|
|
}
|
|
ret = append(ret, data)
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func newLoadData(cluster, service string) *loadData {
|
|
return &loadData{
|
|
cluster: cluster,
|
|
service: service,
|
|
drops: make(map[string]uint64),
|
|
localityStats: make(map[clients.Locality]localityData),
|
|
}
|
|
}
|
|
|
|
type rpcCountData struct {
|
|
// Only atomic accesses are allowed for the fields.
|
|
succeeded *uint64
|
|
errored *uint64
|
|
inProgress *uint64
|
|
issued *uint64
|
|
|
|
// Map from load desc to load data (sum+count). Loading data from map is
|
|
// atomic, but updating data takes a lock, which could cause contention when
|
|
// multiple RPCs try to report loads for the same desc.
|
|
//
|
|
// To fix the contention, shard this map.
|
|
serverLoads sync.Map // map[string]*rpcLoadData
|
|
}
|
|
|
|
func newRPCCountData() *rpcCountData {
|
|
return &rpcCountData{
|
|
succeeded: new(uint64),
|
|
errored: new(uint64),
|
|
inProgress: new(uint64),
|
|
issued: new(uint64),
|
|
}
|
|
}
|
|
|
|
func (rcd *rpcCountData) incrSucceeded() {
|
|
atomic.AddUint64(rcd.succeeded, 1)
|
|
}
|
|
|
|
func (rcd *rpcCountData) loadAndClearSucceeded() uint64 {
|
|
return atomic.SwapUint64(rcd.succeeded, 0)
|
|
}
|
|
|
|
func (rcd *rpcCountData) incrErrored() {
|
|
atomic.AddUint64(rcd.errored, 1)
|
|
}
|
|
|
|
func (rcd *rpcCountData) loadAndClearErrored() uint64 {
|
|
return atomic.SwapUint64(rcd.errored, 0)
|
|
}
|
|
|
|
func (rcd *rpcCountData) incrInProgress() {
|
|
atomic.AddUint64(rcd.inProgress, 1)
|
|
}
|
|
|
|
func (rcd *rpcCountData) decrInProgress() {
|
|
atomic.AddUint64(rcd.inProgress, ^uint64(0)) // atomic.Add(x, -1)
|
|
}
|
|
|
|
func (rcd *rpcCountData) loadInProgress() uint64 {
|
|
return atomic.LoadUint64(rcd.inProgress) // InProgress count is not clear when reading.
|
|
}
|
|
|
|
func (rcd *rpcCountData) incrIssued() {
|
|
atomic.AddUint64(rcd.issued, 1)
|
|
}
|
|
|
|
func (rcd *rpcCountData) loadAndClearIssued() uint64 {
|
|
return atomic.SwapUint64(rcd.issued, 0)
|
|
}
|
|
|
|
func (rcd *rpcCountData) addServerLoad(name string, d float64) {
|
|
loads, ok := rcd.serverLoads.Load(name)
|
|
if !ok {
|
|
tl := newRPCLoadData()
|
|
loads, _ = rcd.serverLoads.LoadOrStore(name, tl)
|
|
}
|
|
loads.(*rpcLoadData).add(d)
|
|
}
|
|
|
|
// rpcLoadData is data for server loads (from trailers or oob). Fields in this
|
|
// struct must be updated consistently.
|
|
//
|
|
// The current solution is to hold a lock, which could cause contention. To fix,
|
|
// shard serverLoads map in rpcCountData.
|
|
type rpcLoadData struct {
|
|
mu sync.Mutex
|
|
sum float64
|
|
count uint64
|
|
}
|
|
|
|
func newRPCLoadData() *rpcLoadData {
|
|
return &rpcLoadData{}
|
|
}
|
|
|
|
func (rld *rpcLoadData) add(v float64) {
|
|
rld.mu.Lock()
|
|
rld.sum += v
|
|
rld.count++
|
|
rld.mu.Unlock()
|
|
}
|
|
|
|
func (rld *rpcLoadData) loadAndClear() (s float64, c uint64) {
|
|
rld.mu.Lock()
|
|
s, rld.sum = rld.sum, 0
|
|
c, rld.count = rld.count, 0
|
|
rld.mu.Unlock()
|
|
return s, c
|
|
}
|