mirror of
https://github.com/cozystack/cozystack.git
synced 2026-10-27 23:27:58 +09:00
Every admission walks the owner chain (Pod, StatefulSet, HelmRelease, application CR) with one uncached GET per level, and sibling Pods repeat the same lookups. On a cluster with many tenants that put the lineage webhook at 300-500ms p99, the largest share of apiserver admission latency. Owners are now reused for 60 seconds, successful lookups only. A failed lookup, whether RESTMapping, NotFound or a timeout, leaves the walk without an owner, so the object is admitted with no lineage labels and stays unlabeled until its next UPDATE runs the walk again. Secrets, Services and PVCs can go a long time between updates, so a cached failure would leave every object admitted within the TTL unlabeled for that long, and the aggregated cozystack-api makes such failures routine: discovery of apps.cozystack.io fails while it rolls. Non-Pod objects that already carry every lineage label, tenantresource included, skip the walk, since the objectSelector still matches an UPDATE whose old object lacks the label. Pods go through it regardless, as each Pod admission also sets the scheduling class. Assisted-by: LLM Signed-off-by: Mattia Eleuteri <mattia@hidora.io>
111 lines
2.9 KiB
Go
111 lines
2.9 KiB
Go
package lineage
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
)
|
|
|
|
// ObjectCache holds owner objects fetched during ownership walks, so sibling
|
|
// admissions under the same owner chain share one GET per TTL window.
|
|
//
|
|
// Only successful lookups are stored. A failed lookup leaves the walk without
|
|
// an owner, so the webhook admits the object with no lineage labels, and the
|
|
// object stays unlabeled until its next UPDATE runs the walk again. Secrets,
|
|
// Services and PVCs can go a long time between updates, so a cached failure
|
|
// would outlive the TTL on every object admitted meanwhile.
|
|
//
|
|
// A nil ObjectCache is valid: Get always misses and Set does nothing.
|
|
type ObjectCache struct {
|
|
ttl time.Duration
|
|
mu sync.RWMutex
|
|
items map[cacheKey]cacheEntry
|
|
}
|
|
|
|
type cacheKey struct {
|
|
apiVersion string
|
|
kind string
|
|
namespace string
|
|
name string
|
|
}
|
|
|
|
type cacheEntry struct {
|
|
obj *unstructured.Unstructured
|
|
expiresAt time.Time
|
|
}
|
|
|
|
// NewObjectCache returns nil, a disabled cache, when ttl is not positive.
|
|
func NewObjectCache(ttl time.Duration) *ObjectCache {
|
|
if ttl <= 0 {
|
|
return nil
|
|
}
|
|
return &ObjectCache{
|
|
ttl: ttl,
|
|
items: make(map[cacheKey]cacheEntry),
|
|
}
|
|
}
|
|
|
|
// Get returns the stored pointer, shared with concurrent admissions: callers
|
|
// must DeepCopy before mutating it.
|
|
func (c *ObjectCache) Get(apiVersion, kind, namespace, name string) (*unstructured.Unstructured, bool) {
|
|
if c == nil {
|
|
return nil, false
|
|
}
|
|
k := cacheKey{apiVersion, kind, namespace, name}
|
|
c.mu.RLock()
|
|
entry, ok := c.items[k]
|
|
c.mu.RUnlock()
|
|
if !ok || time.Now().After(entry.expiresAt) {
|
|
return nil, false
|
|
}
|
|
return entry.obj, true
|
|
}
|
|
|
|
func (c *ObjectCache) Set(apiVersion, kind, namespace, name string, obj *unstructured.Unstructured) {
|
|
if c == nil {
|
|
return
|
|
}
|
|
k := cacheKey{apiVersion, kind, namespace, name}
|
|
entry := cacheEntry{
|
|
obj: obj,
|
|
expiresAt: time.Now().Add(c.ttl),
|
|
}
|
|
c.mu.Lock()
|
|
c.items[k] = entry
|
|
if len(c.items) > evictionThreshold {
|
|
c.evictExpiredSampleLocked(time.Now(), evictionSampleSize)
|
|
}
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
// evictExpiredSampleLocked removes the expired entries among the first n that
|
|
// map iteration yields. Go randomises that order, so repeated calls cover the
|
|
// whole map without a full scan under the write lock. The caller holds c.mu.
|
|
func (c *ObjectCache) evictExpiredSampleLocked(now time.Time, n int) {
|
|
if n <= 0 {
|
|
return
|
|
}
|
|
i := 0
|
|
for key, e := range c.items {
|
|
if now.After(e.expiresAt) {
|
|
delete(c.items, key)
|
|
}
|
|
i++
|
|
if i >= n {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
const (
|
|
// Nothing is evicted below evictionThreshold entries, and Get does not
|
|
// drop expired ones, so up to this many owner objects (HelmReleases and
|
|
// application CRs, values included) stay in memory for the life of the
|
|
// process, expired or not.
|
|
evictionThreshold = 4096
|
|
// evictionSampleSize caps the entries one Set inspects above the
|
|
// threshold, which bounds the write-lock hold time.
|
|
evictionSampleSize = 64
|
|
)
|