mirror of
https://github.com/k3s-io/kubernetes.git
synced 2026-07-18 04:54:54 +00:00
Merge pull request #131641 from pohly/dra-kubelet-in-use-metric
DRA kubelet: add dra_resource_claims_in_use gauge vector
This commit is contained in:
@@ -62,6 +62,7 @@ import (
|
||||
kubecontainer "k8s.io/kubernetes/pkg/kubelet/container"
|
||||
"k8s.io/kubernetes/pkg/kubelet/events"
|
||||
"k8s.io/kubernetes/pkg/kubelet/lifecycle"
|
||||
"k8s.io/kubernetes/pkg/kubelet/metrics"
|
||||
"k8s.io/kubernetes/pkg/kubelet/pluginmanager/cache"
|
||||
"k8s.io/kubernetes/pkg/kubelet/stats/pidlimit"
|
||||
"k8s.io/kubernetes/pkg/kubelet/status"
|
||||
@@ -310,10 +311,11 @@ func NewContainerManager(mountUtil mount.Interface, cadvisorInterface cadvisor.I
|
||||
// Initialize DRA manager
|
||||
if utilfeature.DefaultFeatureGate.Enabled(kubefeatures.DynamicResourceAllocation) {
|
||||
klog.InfoS("Creating Dynamic Resource Allocation (DRA) manager")
|
||||
cm.draManager, err = dra.NewManager(kubeClient, nodeConfig.KubeletRootDir)
|
||||
cm.draManager, err = dra.NewManager(klog.TODO(), kubeClient, nodeConfig.KubeletRootDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
metrics.Register(cm.draManager.NewMetricsCollector())
|
||||
}
|
||||
cm.kubeClient = kubeClient
|
||||
|
||||
|
||||
@@ -20,13 +20,19 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
|
||||
resourceapi "k8s.io/api/resource/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/component-base/metrics"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/kubernetes/pkg/kubelet/cm/dra/state"
|
||||
kubecontainer "k8s.io/kubernetes/pkg/kubelet/container"
|
||||
kubeletmetrics "k8s.io/kubernetes/pkg/kubelet/metrics"
|
||||
)
|
||||
|
||||
// ClaimInfo holds information required
|
||||
@@ -39,6 +45,8 @@ type ClaimInfo struct {
|
||||
|
||||
// claimInfoCache is a cache of processed resource claims keyed by namespace/claimname.
|
||||
type claimInfoCache struct {
|
||||
logger klog.Logger
|
||||
|
||||
sync.RWMutex
|
||||
checkpointer state.Checkpointer
|
||||
claimInfo map[string]*ClaimInfo
|
||||
@@ -111,8 +119,25 @@ func (info *ClaimInfo) isPrepared() bool {
|
||||
return info.prepared
|
||||
}
|
||||
|
||||
// cdiDevicesAsList returns a list of CDIDevices from the provided claim info.
|
||||
// When the request name is non-empty, only devices relevant for that request
|
||||
// are returned.
|
||||
func (info *ClaimInfo) cdiDevicesAsList(requestName string) []kubecontainer.CDIDevice {
|
||||
var cdiDevices []kubecontainer.CDIDevice
|
||||
for _, driverData := range info.DriverState {
|
||||
for _, device := range driverData.Devices {
|
||||
if requestName == "" || len(device.RequestNames) == 0 || slices.Contains(device.RequestNames, requestName) {
|
||||
for _, cdiDeviceID := range device.CDIDeviceIDs {
|
||||
cdiDevices = append(cdiDevices, kubecontainer.CDIDevice{Name: cdiDeviceID})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return cdiDevices
|
||||
}
|
||||
|
||||
// newClaimInfoCache creates a new claim info cache object, pre-populated from a checkpoint (if present).
|
||||
func newClaimInfoCache(stateDir, checkpointName string) (*claimInfoCache, error) {
|
||||
func newClaimInfoCache(logger klog.Logger, stateDir, checkpointName string) (*claimInfoCache, error) {
|
||||
checkpointer, err := state.NewCheckpointer(stateDir, checkpointName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not initialize checkpoint manager, please drain node and remove DRA state file, err: %w", err)
|
||||
@@ -124,6 +149,7 @@ func newClaimInfoCache(stateDir, checkpointName string) (*claimInfoCache, error)
|
||||
}
|
||||
|
||||
cache := &claimInfoCache{
|
||||
logger: logger,
|
||||
checkpointer: checkpointer,
|
||||
claimInfo: make(map[string]*ClaimInfo),
|
||||
}
|
||||
@@ -142,9 +168,31 @@ func newClaimInfoCache(stateDir, checkpointName string) (*claimInfoCache, error)
|
||||
}
|
||||
|
||||
// withLock runs a function while holding the claimInfoCache lock.
|
||||
// It logs changes.
|
||||
func (cache *claimInfoCache) withLock(f func() error) error {
|
||||
cache.Lock()
|
||||
defer cache.Unlock()
|
||||
|
||||
if loggerV := cache.logger.V(5); loggerV.Enabled() {
|
||||
claimsInUseBefore := cache.claimsInUse()
|
||||
defer func() {
|
||||
claimsInUseAfter := cache.claimsInUse()
|
||||
delta := claimsInUseDelta(claimsInUseBefore, claimsInUseAfter)
|
||||
|
||||
changed := false
|
||||
for _, inUse := range delta {
|
||||
if inUse.Delta != 0 {
|
||||
changed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if changed {
|
||||
cache.logger.V(5).Info("ResourceClaim usage changed", "claimsInUse", delta)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return f()
|
||||
}
|
||||
|
||||
@@ -203,19 +251,112 @@ func (cache *claimInfoCache) syncToCheckpoint() error {
|
||||
return cache.checkpointer.Store(checkpoint)
|
||||
}
|
||||
|
||||
// cdiDevicesAsList returns a list of CDIDevices from the provided claim info.
|
||||
// When the request name is non-empty, only devices relevant for that request
|
||||
// are returned.
|
||||
func (info *ClaimInfo) cdiDevicesAsList(requestName string) []kubecontainer.CDIDevice {
|
||||
var cdiDevices []kubecontainer.CDIDevice
|
||||
for _, driverData := range info.DriverState {
|
||||
for _, device := range driverData.Devices {
|
||||
if requestName == "" || len(device.RequestNames) == 0 || slices.Contains(device.RequestNames, requestName) {
|
||||
for _, cdiDeviceID := range device.CDIDeviceIDs {
|
||||
cdiDevices = append(cdiDevices, kubecontainer.CDIDevice{Name: cdiDeviceID})
|
||||
}
|
||||
}
|
||||
// claimsInUse computes the the current counter vector for DRAResourceClaimsInUse.
|
||||
// It returns a map of driver name to number of claims which have been prepared using
|
||||
// the driver. The [kubeletmetrics.DRAResourceClaimsInUseAnyDriver] key stands for
|
||||
// all prepared claims.
|
||||
//
|
||||
// Must be called while the rlock is held.
|
||||
func (cache *claimInfoCache) claimsInUse() map[string]int {
|
||||
counts := make(map[string]int)
|
||||
total := 0
|
||||
for _, claimInfo := range cache.claimInfo {
|
||||
if !claimInfo.isPrepared() {
|
||||
continue
|
||||
}
|
||||
total++
|
||||
|
||||
for driverName := range claimInfo.DriverState {
|
||||
counts[driverName]++
|
||||
}
|
||||
}
|
||||
return cdiDevices
|
||||
counts[kubeletmetrics.DRAResourceClaimsInUseAnyDriver] = total
|
||||
return counts
|
||||
}
|
||||
|
||||
// claimsInUseDelta compares two maps returned by claimsInUse.
|
||||
// The type can be used as value in structured logging.
|
||||
func claimsInUseDelta(before, after map[string]int) ClaimsInUseDelta {
|
||||
var delta ClaimsInUseDelta
|
||||
for driverName, count := range before {
|
||||
if _, stillSet := after[driverName]; !stillSet {
|
||||
delta = append(delta, ClaimsInUse{DriverName: driverName, Count: 0, Delta: -count})
|
||||
}
|
||||
}
|
||||
for driverName, count := range after {
|
||||
delta = append(delta, ClaimsInUse{DriverName: driverName, Count: count, Delta: count - before[driverName]})
|
||||
}
|
||||
return delta
|
||||
}
|
||||
|
||||
// ClaimsInUseDelta provides String (for text logging) and MarshalLog (for structured logging).
|
||||
type ClaimsInUseDelta []ClaimsInUse
|
||||
|
||||
var _ fmt.Stringer = ClaimsInUseDelta{}
|
||||
var _ logr.Marshaler = ClaimsInUseDelta{}
|
||||
|
||||
func (d ClaimsInUseDelta) String() string {
|
||||
d = d.sort()
|
||||
var buffer strings.Builder
|
||||
for i, inUse := range d {
|
||||
if i > 0 {
|
||||
buffer.WriteByte('\n')
|
||||
}
|
||||
buffer.WriteString(fmt.Sprintf("%s: %d (%+d)", inUse.DriverName, inUse.Count, inUse.Delta))
|
||||
}
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
func (d ClaimsInUseDelta) MarshalLog() any {
|
||||
d = d.sort()
|
||||
return []ClaimsInUse(d)
|
||||
}
|
||||
|
||||
// sort returns a sorted copy of the slice.
|
||||
func (d ClaimsInUseDelta) sort() ClaimsInUseDelta {
|
||||
d = slices.Clone(d)
|
||||
slices.SortFunc(d, func(a, b ClaimsInUse) int {
|
||||
return strings.Compare(a.DriverName, b.DriverName)
|
||||
})
|
||||
return d
|
||||
}
|
||||
|
||||
type ClaimsInUse struct {
|
||||
DriverName string
|
||||
Count int
|
||||
Delta int
|
||||
}
|
||||
|
||||
// claimInfoCollector provides metrics for a claimInfoCache.
|
||||
type claimInfoCollector struct {
|
||||
metrics.BaseStableCollector
|
||||
cache *claimInfoCache
|
||||
}
|
||||
|
||||
var _ metrics.StableCollector = &claimInfoCollector{}
|
||||
|
||||
// DescribeWithStability implements the metrics.StableCollector interface.
|
||||
func (collector *claimInfoCollector) DescribeWithStability(ch chan<- *metrics.Desc) {
|
||||
ch <- kubeletmetrics.DRAResourceClaimsInUseDesc
|
||||
}
|
||||
|
||||
// CollectWithStability implements the metrics.StableCollector interface.
|
||||
func (collector *claimInfoCollector) CollectWithStability(ch chan<- metrics.Metric) {
|
||||
var claimsInUse map[string]int
|
||||
_ = collector.cache.withRLock(func() error {
|
||||
claimsInUse = collector.cache.claimsInUse()
|
||||
return nil
|
||||
})
|
||||
|
||||
// Only currently known drivers are listed. If a driver had active
|
||||
// claims in the past, no longer does and then gets uninstalled, it no
|
||||
// longer shows up. This avoids the memory leak problem in a normal
|
||||
// GaugeVec which could grow over time unless obsolete drivers are
|
||||
// actively deleted.
|
||||
//
|
||||
// The empty driver name provides the overall count of all active
|
||||
// ResourceClaims regardless of the driver.
|
||||
for driverName, count := range claimsInUse {
|
||||
ch <- metrics.NewLazyConstMetric(kubeletmetrics.DRAResourceClaimsInUseDesc, metrics.GaugeValue, float64(count), driverName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ import (
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/kubernetes/pkg/kubelet/cm/dra/state"
|
||||
"k8s.io/kubernetes/test/utils/ktesting"
|
||||
"k8s.io/kubernetes/test/utils/ktesting/initoption"
|
||||
)
|
||||
|
||||
// ClaimInfo test cases
|
||||
@@ -436,7 +438,8 @@ func TestNewClaimInfoCache(t *testing.T) {
|
||||
},
|
||||
} {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
result, err := newClaimInfoCache(test.stateDir, test.checkpointName)
|
||||
tCtx := ktesting.Init(t)
|
||||
result, err := newClaimInfoCache(tCtx.Logger(), test.stateDir, test.checkpointName)
|
||||
if test.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
@@ -494,7 +497,8 @@ func TestClaimInfoCacheWithLock(t *testing.T) {
|
||||
},
|
||||
} {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
cache, err := newClaimInfoCache(t.TempDir(), "test-checkpoint")
|
||||
tCtx := ktesting.Init(t)
|
||||
cache, err := newClaimInfoCache(tCtx.Logger(), t.TempDir(), "test-checkpoint")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, cache)
|
||||
err = cache.withLock(test.funcGen(cache))
|
||||
@@ -554,7 +558,8 @@ func TestClaimInfoCacheWithRLock(t *testing.T) {
|
||||
},
|
||||
} {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
cache, err := newClaimInfoCache(t.TempDir(), "test-checkpoint")
|
||||
tCtx := ktesting.Init(t)
|
||||
cache, err := newClaimInfoCache(tCtx.Logger(), t.TempDir(), "test-checkpoint")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, cache)
|
||||
err = cache.withRLock(test.funcGen(cache))
|
||||
@@ -569,8 +574,11 @@ func TestClaimInfoCacheWithRLock(t *testing.T) {
|
||||
|
||||
func TestClaimInfoCacheAdd(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
description string
|
||||
claimInfo *ClaimInfo
|
||||
description string
|
||||
initialClaimInfo []*ClaimInfo
|
||||
claimInfo *ClaimInfo
|
||||
expectMetrics string
|
||||
expectLog string
|
||||
}{
|
||||
{
|
||||
description: "claimInfo successfully added",
|
||||
@@ -580,14 +588,90 @@ func TestClaimInfoCacheAdd(t *testing.T) {
|
||||
Namespace: namespace,
|
||||
},
|
||||
},
|
||||
expectMetrics: `# HELP dra_resource_claims_in_use [ALPHA] The number of ResourceClaims that are currently in use on the node, by driver name (driver_name label value) and across all drivers (special value <any> for driver_name). Note that the sum of all by-driver counts is not the total number of in-use ResourceClaims because the same ResourceClaim might use devices from different drivers. Instead, use the count for the <any> driver_name.
|
||||
# TYPE dra_resource_claims_in_use gauge
|
||||
dra_resource_claims_in_use{driver_name="<any>"} 0
|
||||
`,
|
||||
},
|
||||
{
|
||||
description: "prepared claimInfo",
|
||||
claimInfo: &ClaimInfo{
|
||||
ClaimInfoState: state.ClaimInfoState{
|
||||
ClaimName: claimName,
|
||||
Namespace: namespace,
|
||||
DriverState: map[string]state.DriverState{
|
||||
"test-driver": {},
|
||||
"other-test-driver": {},
|
||||
},
|
||||
},
|
||||
prepared: true,
|
||||
},
|
||||
expectMetrics: `# HELP dra_resource_claims_in_use [ALPHA] The number of ResourceClaims that are currently in use on the node, by driver name (driver_name label value) and across all drivers (special value <any> for driver_name). Note that the sum of all by-driver counts is not the total number of in-use ResourceClaims because the same ResourceClaim might use devices from different drivers. Instead, use the count for the <any> driver_name.
|
||||
# TYPE dra_resource_claims_in_use gauge
|
||||
dra_resource_claims_in_use{driver_name="<any>"} 1
|
||||
dra_resource_claims_in_use{driver_name="other-test-driver"} 1
|
||||
dra_resource_claims_in_use{driver_name="test-driver"} 1
|
||||
`,
|
||||
expectLog: `INFO ResourceClaim usage changed claimsInUse=<
|
||||
<any>: 1 (+1)
|
||||
other-test-driver: 1 (+1)
|
||||
test-driver: 1 (+1)
|
||||
>
|
||||
`,
|
||||
},
|
||||
{
|
||||
description: "add more prepared claimInfo",
|
||||
initialClaimInfo: []*ClaimInfo{{
|
||||
ClaimInfoState: state.ClaimInfoState{
|
||||
ClaimName: claimName + "-old",
|
||||
Namespace: namespace,
|
||||
DriverState: map[string]state.DriverState{
|
||||
"test-driver": {},
|
||||
},
|
||||
},
|
||||
prepared: true,
|
||||
}},
|
||||
claimInfo: &ClaimInfo{
|
||||
ClaimInfoState: state.ClaimInfoState{
|
||||
ClaimName: claimName,
|
||||
Namespace: namespace,
|
||||
DriverState: map[string]state.DriverState{
|
||||
"test-driver": {},
|
||||
"other-test-driver": {},
|
||||
},
|
||||
},
|
||||
prepared: true,
|
||||
},
|
||||
expectMetrics: `# HELP dra_resource_claims_in_use [ALPHA] The number of ResourceClaims that are currently in use on the node, by driver name (driver_name label value) and across all drivers (special value <any> for driver_name). Note that the sum of all by-driver counts is not the total number of in-use ResourceClaims because the same ResourceClaim might use devices from different drivers. Instead, use the count for the <any> driver_name.
|
||||
# TYPE dra_resource_claims_in_use gauge
|
||||
dra_resource_claims_in_use{driver_name="<any>"} 2
|
||||
dra_resource_claims_in_use{driver_name="other-test-driver"} 1
|
||||
dra_resource_claims_in_use{driver_name="test-driver"} 2
|
||||
`,
|
||||
expectLog: `INFO ResourceClaim usage changed claimsInUse=<
|
||||
<any>: 2 (+1)
|
||||
other-test-driver: 1 (+1)
|
||||
test-driver: 2 (+1)
|
||||
>
|
||||
`,
|
||||
},
|
||||
} {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
cache, err := newClaimInfoCache(t.TempDir(), "test-checkpoint")
|
||||
tCtx := ktesting.Init(t, initoption.BufferLogs(true))
|
||||
cache, err := newClaimInfoCache(tCtx.Logger(), t.TempDir(), "test-checkpoint")
|
||||
for _, claimInfo := range test.initialClaimInfo {
|
||||
cache.add(claimInfo)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, cache)
|
||||
cache.add(test.claimInfo)
|
||||
_ = cache.withLock(func() error {
|
||||
cache.add(test.claimInfo)
|
||||
return nil
|
||||
})
|
||||
assert.True(t, cache.contains(test.claimInfo.ClaimName, test.claimInfo.Namespace))
|
||||
testClaimsInUseMetric(tCtx, cache, test.expectMetrics)
|
||||
logOutput := tCtx.Logger().GetSink().(ktesting.Underlier).GetBuffer()
|
||||
assert.Equal(t, test.expectLog, logOutput.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -682,28 +766,69 @@ func TestClaimInfoCacheDelete(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
description string
|
||||
claimInfoCache *claimInfoCache
|
||||
expectMetrics string
|
||||
expectLog string
|
||||
}{
|
||||
{
|
||||
description: "item in cache",
|
||||
claimInfoCache: &claimInfoCache{
|
||||
claimInfo: map[string]*ClaimInfo{
|
||||
claimName + namespace: {
|
||||
namespace + "/" + claimName: {
|
||||
ClaimInfoState: state.ClaimInfoState{
|
||||
ClaimName: claimName,
|
||||
Namespace: namespace,
|
||||
DriverState: map[string]state.DriverState{
|
||||
"test-driver": {},
|
||||
},
|
||||
},
|
||||
prepared: true,
|
||||
},
|
||||
namespace + "/" + claimName + "-old": {
|
||||
ClaimInfoState: state.ClaimInfoState{
|
||||
ClaimName: claimName,
|
||||
Namespace: namespace,
|
||||
DriverState: map[string]state.DriverState{
|
||||
"test-driver": {},
|
||||
"other-test-driver": {},
|
||||
},
|
||||
},
|
||||
prepared: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectMetrics: `# HELP dra_resource_claims_in_use [ALPHA] The number of ResourceClaims that are currently in use on the node, by driver name (driver_name label value) and across all drivers (special value <any> for driver_name). Note that the sum of all by-driver counts is not the total number of in-use ResourceClaims because the same ResourceClaim might use devices from different drivers. Instead, use the count for the <any> driver_name.
|
||||
# TYPE dra_resource_claims_in_use gauge
|
||||
dra_resource_claims_in_use{driver_name="<any>"} 1
|
||||
dra_resource_claims_in_use{driver_name="test-driver"} 1
|
||||
dra_resource_claims_in_use{driver_name="other-test-driver"} 1
|
||||
`,
|
||||
expectLog: `INFO ResourceClaim usage changed claimsInUse=<
|
||||
<any>: 1 (-1)
|
||||
other-test-driver: 1 (+0)
|
||||
test-driver: 1 (-1)
|
||||
>
|
||||
`,
|
||||
},
|
||||
{
|
||||
description: "item not in cache",
|
||||
claimInfoCache: &claimInfoCache{},
|
||||
expectMetrics: `# HELP dra_resource_claims_in_use [ALPHA] The number of ResourceClaims that are currently in use on the node, by driver name (driver_name label value) and across all drivers (special value <any> for driver_name). Note that the sum of all by-driver counts is not the total number of in-use ResourceClaims because the same ResourceClaim might use devices from different drivers. Instead, use the count for the <any> driver_name.
|
||||
# TYPE dra_resource_claims_in_use gauge
|
||||
dra_resource_claims_in_use{driver_name="<any>"} 0
|
||||
`,
|
||||
},
|
||||
} {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
test.claimInfoCache.delete(claimName, namespace)
|
||||
tCtx := ktesting.Init(t, initoption.BufferLogs(true))
|
||||
test.claimInfoCache.logger = tCtx.Logger()
|
||||
_ = test.claimInfoCache.withLock(func() error {
|
||||
test.claimInfoCache.delete(claimName, namespace)
|
||||
return nil
|
||||
})
|
||||
assert.False(t, test.claimInfoCache.contains(claimName, namespace))
|
||||
testClaimsInUseMetric(tCtx, test.claimInfoCache, test.expectMetrics)
|
||||
logOutput := tCtx.Logger().GetSink().(ktesting.Underlier).GetBuffer()
|
||||
assert.Equal(t, test.expectLog, logOutput.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -718,7 +843,7 @@ func TestClaimInfoCacheHasPodReference(t *testing.T) {
|
||||
description: "uid is referenced",
|
||||
claimInfoCache: &claimInfoCache{
|
||||
claimInfo: map[string]*ClaimInfo{
|
||||
claimName + namespace: {
|
||||
namespace + "/" + claimName: {
|
||||
ClaimInfoState: state.ClaimInfoState{
|
||||
ClaimName: claimName,
|
||||
Namespace: namespace,
|
||||
@@ -754,7 +879,8 @@ func TestSyncToCheckpoint(t *testing.T) {
|
||||
},
|
||||
} {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
cache, err := newClaimInfoCache(test.stateDir, test.checkpointName)
|
||||
tCtx := ktesting.Init(t)
|
||||
cache, err := newClaimInfoCache(tCtx.Logger(), test.stateDir, test.checkpointName)
|
||||
require.NoError(t, err)
|
||||
err = cache.syncToCheckpoint()
|
||||
if test.wantErr {
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
"k8s.io/component-base/metrics"
|
||||
"k8s.io/dynamic-resource-allocation/resourceclaim"
|
||||
"k8s.io/klog/v2"
|
||||
drapb "k8s.io/kubelet/pkg/apis/dra/v1beta1"
|
||||
@@ -36,7 +37,7 @@ import (
|
||||
"k8s.io/kubernetes/pkg/kubelet/cm/dra/state"
|
||||
"k8s.io/kubernetes/pkg/kubelet/config"
|
||||
kubecontainer "k8s.io/kubernetes/pkg/kubelet/container"
|
||||
"k8s.io/kubernetes/pkg/kubelet/metrics"
|
||||
kubeletmetrics "k8s.io/kubernetes/pkg/kubelet/metrics"
|
||||
"k8s.io/kubernetes/pkg/kubelet/pluginmanager/cache"
|
||||
)
|
||||
|
||||
@@ -101,8 +102,8 @@ type Manager struct {
|
||||
// - Don't include the namespace, it can be inferred from the context.
|
||||
// - Avoid repeated "failed to ...: failed to ..." when wrapping errors.
|
||||
// - Avoid wrapping when it does not provide relevant additional information to keep the user-visible error short.
|
||||
func NewManager(kubeClient clientset.Interface, stateFileDirectory string) (*Manager, error) {
|
||||
claimInfoCache, err := newClaimInfoCache(stateFileDirectory, draManagerStateFileName)
|
||||
func NewManager(logger klog.Logger, kubeClient clientset.Interface, stateFileDirectory string) (*Manager, error) {
|
||||
claimInfoCache, err := newClaimInfoCache(logger, stateFileDirectory, draManagerStateFileName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create ResourceClaim cache: %w", err)
|
||||
}
|
||||
@@ -122,6 +123,10 @@ func NewManager(kubeClient clientset.Interface, stateFileDirectory string) (*Man
|
||||
return manager, nil
|
||||
}
|
||||
|
||||
func (m *Manager) NewMetricsCollector() metrics.StableCollector {
|
||||
return &claimInfoCollector{cache: m.cache}
|
||||
}
|
||||
|
||||
// GetWatcherHandler must be called after Start, it indirectly depends
|
||||
// on parameters which only get passed to Start, for example the context.
|
||||
func (m *Manager) GetWatcherHandler() cache.PluginHandler {
|
||||
@@ -199,7 +204,7 @@ func (m *Manager) reconcileLoop(ctx context.Context) {
|
||||
func (m *Manager) PrepareResources(ctx context.Context, pod *v1.Pod) error {
|
||||
startTime := time.Now()
|
||||
err := m.prepareResources(ctx, pod)
|
||||
metrics.DRAOperationsDuration.WithLabelValues("PrepareResources", strconv.FormatBool(err == nil)).Observe(time.Since(startTime).Seconds())
|
||||
kubeletmetrics.DRAOperationsDuration.WithLabelValues("PrepareResources", strconv.FormatBool(err == nil)).Observe(time.Since(startTime).Seconds())
|
||||
if err != nil {
|
||||
return fmt.Errorf("prepare dynamic resources: %w", err)
|
||||
}
|
||||
@@ -481,7 +486,7 @@ func (m *Manager) GetResources(pod *v1.Pod, container *v1.Container) (*Container
|
||||
func (m *Manager) UnprepareResources(ctx context.Context, pod *v1.Pod) error {
|
||||
startTime := time.Now()
|
||||
err := m.unprepareResourcesForPod(ctx, pod)
|
||||
metrics.DRAOperationsDuration.WithLabelValues("UnprepareResources", strconv.FormatBool(err == nil)).Observe(time.Since(startTime).Seconds())
|
||||
kubeletmetrics.DRAOperationsDuration.WithLabelValues("UnprepareResources", strconv.FormatBool(err == nil)).Observe(time.Since(startTime).Seconds())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unprepare dynamic resources: %w", err)
|
||||
}
|
||||
|
||||
@@ -198,7 +198,8 @@ func TestNewManagerImpl(t *testing.T) {
|
||||
},
|
||||
} {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
manager, err := NewManager(kubeClient, test.stateFileDirectory)
|
||||
tCtx := ktesting.Init(t)
|
||||
manager, err := NewManager(tCtx.Logger(), kubeClient, test.stateFileDirectory)
|
||||
if test.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
@@ -362,7 +363,8 @@ func TestGetResources(t *testing.T) {
|
||||
},
|
||||
} {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
manager, err := NewManager(kubeClient, t.TempDir())
|
||||
tCtx := ktesting.Init(t)
|
||||
manager, err := NewManager(tCtx.Logger(), kubeClient, t.TempDir())
|
||||
require.NoError(t, err)
|
||||
|
||||
if test.claimInfo != nil {
|
||||
@@ -558,7 +560,7 @@ func TestPrepareResources(t *testing.T) {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
tCtx := ktesting.Init(t)
|
||||
|
||||
manager, err := NewManager(fakeKubeClient, t.TempDir())
|
||||
manager, err := NewManager(tCtx.Logger(), fakeKubeClient, t.TempDir())
|
||||
require.NoError(t, err, "create DRA manager")
|
||||
manager.initDRAPluginManager(tCtx, getFakeNode, time.Second /* very short wiping delay for testing */)
|
||||
|
||||
@@ -714,7 +716,7 @@ func TestUnprepareResources(t *testing.T) {
|
||||
}
|
||||
defer draServerInfo.teardownFn()
|
||||
|
||||
manager, err := NewManager(fakeKubeClient, t.TempDir())
|
||||
manager, err := NewManager(tCtx.Logger(), fakeKubeClient, t.TempDir())
|
||||
require.NoError(t, err, "create DRA manager")
|
||||
manager.initDRAPluginManager(tCtx, getFakeNode, time.Second /* very short wiping delay for testing */)
|
||||
|
||||
@@ -758,8 +760,9 @@ func TestUnprepareResources(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPodMightNeedToUnprepareResources(t *testing.T) {
|
||||
tCtx := ktesting.Init(t)
|
||||
fakeKubeClient := fake.NewSimpleClientset()
|
||||
manager, err := NewManager(fakeKubeClient, t.TempDir())
|
||||
manager, err := NewManager(tCtx.Logger(), fakeKubeClient, t.TempDir())
|
||||
require.NoError(t, err, "create DRA manager")
|
||||
|
||||
claimInfo := &ClaimInfo{
|
||||
@@ -833,7 +836,8 @@ func TestGetContainerClaimInfos(t *testing.T) {
|
||||
},
|
||||
} {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
manager, err := NewManager(nil, t.TempDir())
|
||||
tCtx := ktesting.Init(t)
|
||||
manager, err := NewManager(tCtx.Logger(), nil, t.TempDir())
|
||||
require.NoError(t, err, "create DRA manager")
|
||||
|
||||
if test.claimInfo != nil {
|
||||
@@ -871,7 +875,7 @@ func TestParallelPrepareUnprepareResources(t *testing.T) {
|
||||
|
||||
// Create fake Kube client and DRA manager
|
||||
fakeKubeClient := fake.NewSimpleClientset()
|
||||
manager, err := NewManager(fakeKubeClient, t.TempDir())
|
||||
manager, err := NewManager(tCtx.Logger(), fakeKubeClient, t.TempDir())
|
||||
require.NoError(t, err, "create DRA manager")
|
||||
manager.initDRAPluginManager(tCtx, getFakeNode, time.Second /* very short wiping delay for testing */)
|
||||
|
||||
|
||||
35
pkg/kubelet/cm/dra/metrics_test.go
Normal file
35
pkg/kubelet/cm/dra/metrics_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
Copyright 2025 The Kubernetes 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 dra
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"k8s.io/component-base/metrics/testutil"
|
||||
"k8s.io/kubernetes/test/utils/ktesting"
|
||||
)
|
||||
|
||||
func testClaimsInUseMetric(tCtx ktesting.TContext, claimInfoCache *claimInfoCache, expectedMetric string) {
|
||||
tCtx.Helper()
|
||||
// Must simulate registration which calls Create, otherwise collection crashes.
|
||||
collector := &claimInfoCollector{cache: claimInfoCache}
|
||||
collector.Create(nil, collector)
|
||||
err := testutil.CollectAndCompare(collector, strings.NewReader(expectedMetric), "dra_resource_claims_in_use")
|
||||
if err != nil {
|
||||
tCtx.Error(err)
|
||||
}
|
||||
}
|
||||
@@ -160,6 +160,9 @@ const (
|
||||
ImageVolumeRequestedTotalKey = "image_volume_requested_total"
|
||||
ImageVolumeMountedSucceedTotalKey = "image_volume_mounted_succeed_total"
|
||||
ImageVolumeMountedErrorsTotalKey = "image_volume_mounted_errors_total"
|
||||
|
||||
// Special label for [DRAResourceClaimsInUseDesc] which counts ResourceClaims regardless of the driver.
|
||||
DRAResourceClaimsInUseAnyDriver = "<any>"
|
||||
)
|
||||
|
||||
type imageSizeBucket struct {
|
||||
@@ -1028,6 +1031,14 @@ var (
|
||||
[]string{"driver_name", "method_name", "grpc_status_code"},
|
||||
)
|
||||
|
||||
DRAResourceClaimsInUseDesc = metrics.NewDesc(DRASubsystem+"_resource_claims_in_use",
|
||||
"The number of ResourceClaims that are currently in use on the node, by driver name (driver_name label value) and across all drivers (special value <any> for driver_name). Note that the sum of all by-driver counts is not the total number of in-use ResourceClaims because the same ResourceClaim might use devices from different drivers. Instead, use the count for the <any> driver_name.",
|
||||
[]string{"driver_name"},
|
||||
nil,
|
||||
metrics.ALPHA,
|
||||
"",
|
||||
)
|
||||
|
||||
// AdmissionRejectionsTotal tracks the number of failed admission times, currently, just record it for pod additions
|
||||
AdmissionRejectionsTotal = metrics.NewCounterVec(
|
||||
&metrics.CounterOpts{
|
||||
@@ -1164,8 +1175,10 @@ func Register(collectors ...metrics.StableCollector) {
|
||||
legacyregistry.MustRegister(CgroupVersion)
|
||||
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.DynamicResourceAllocation) {
|
||||
legacyregistry.MustRegister(DRAOperationsDuration)
|
||||
legacyregistry.MustRegister(DRAGRPCOperationsDuration)
|
||||
legacyregistry.MustRegister(
|
||||
DRAOperationsDuration,
|
||||
DRAGRPCOperationsDuration,
|
||||
)
|
||||
}
|
||||
|
||||
legacyregistry.MustRegister(AdmissionRejectionsTotal)
|
||||
|
||||
@@ -38,8 +38,20 @@ var (
|
||||
QuantileLabel model.LabelName = model.QuantileLabel
|
||||
)
|
||||
|
||||
// Metrics is generic metrics for other specific metrics
|
||||
type Metrics map[string]model.Samples
|
||||
// Metrics is generic metrics for other specific metrics.
|
||||
// This directly exposes Prometheus types. Test code may
|
||||
// use those via type aliases provided by this package
|
||||
// instead of imorting the Prometheus packages (https://github.com/kubernetes/kubernetes/issues/89267).
|
||||
type Metrics map[string]Samples
|
||||
type Samples = model.Samples
|
||||
type Sample = model.Sample
|
||||
type Metric = model.Metric
|
||||
type LabelValue = model.LabelValue
|
||||
type LabelName = model.LabelName
|
||||
type SampleHistogram = model.SampleHistogram
|
||||
type FloatString = model.FloatString
|
||||
type HistogramBuckets = model.HistogramBuckets
|
||||
type HistogramBucket = model.HistogramBucket
|
||||
|
||||
// Equal returns true if all metrics are the same as the arguments.
|
||||
func (m *Metrics) Equal(o Metrics) bool {
|
||||
|
||||
@@ -36,6 +36,7 @@ import (
|
||||
|
||||
"github.com/onsi/ginkgo/v2"
|
||||
"github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gstruct"
|
||||
"github.com/onsi/gomega/types"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
@@ -45,6 +46,7 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/component-base/metrics/testutil"
|
||||
"k8s.io/klog/v2"
|
||||
admissionapi "k8s.io/pod-security-admission/api"
|
||||
"k8s.io/utils/ptr"
|
||||
@@ -479,6 +481,43 @@ var _ = framework.SIGDescribe("node")(framework.WithLabel("DRA"), feature.Dynami
|
||||
gomega.Eventually(kubeletPlugin2.GetGRPCCalls).WithTimeout(retryTestTimeout).Should(testdrivergomega.NodeUnprepareResourcesSucceeded)
|
||||
})
|
||||
|
||||
ginkgo.It("must provide metrics", func(ctx context.Context) {
|
||||
kubeletPlugin1, kubeletPlugin2 := start(ctx)
|
||||
|
||||
pod := createTestObjects(ctx, f.ClientSet, getNodeName(ctx, f), f.Namespace.Name, "draclass", "external-claim", "drasleeppod", false, []string{kubeletPlugin1Name, kubeletPlugin2Name})
|
||||
|
||||
ginkgo.By("wait for pod to succeed")
|
||||
err := e2epod.WaitForPodRunningInNamespace(ctx, f.ClientSet, pod)
|
||||
framework.ExpectNoError(err)
|
||||
gomega.Expect(kubeletPlugin1.GetGRPCCalls()).Should(testdrivergomega.NodePrepareResourcesSucceeded, "Plugin 1 should have prepared resources.")
|
||||
gomega.Expect(kubeletPlugin2.GetGRPCCalls()).Should(testdrivergomega.NodePrepareResourcesSucceeded, "Plugin 2 should have prepared resources.")
|
||||
driverName := func(element any) string {
|
||||
el := element.(*testutil.Sample)
|
||||
return string(el.Metric[testutil.LabelName("driver_name")])
|
||||
}
|
||||
|
||||
gomega.Expect(getKubeletMetrics(ctx)).Should(gstruct.MatchKeys(gstruct.IgnoreExtras, gstruct.Keys{
|
||||
"dra_resource_claims_in_use": gstruct.MatchAllElements(driverName, gstruct.Elements{
|
||||
"<any>": timelessSample(1),
|
||||
kubeletPlugin1Name: timelessSample(1),
|
||||
kubeletPlugin2Name: timelessSample(1),
|
||||
}),
|
||||
}), "metrics while pod is running")
|
||||
|
||||
ginkgo.By("delete pod")
|
||||
err = f.ClientSet.CoreV1().Pods(pod.Namespace).Delete(ctx, pod.Name, metav1.DeleteOptions{})
|
||||
framework.ExpectNoError(err)
|
||||
err = e2epod.WaitForPodNotFoundInNamespace(ctx, f.ClientSet, pod.Name, pod.Namespace, f.Timeouts.PodDelete)
|
||||
framework.ExpectNoError(err)
|
||||
gomega.Expect(kubeletPlugin1.GetGRPCCalls()).Should(testdrivergomega.NodeUnprepareResourcesSucceeded, "Plugin 2 should have unprepared resources.")
|
||||
gomega.Expect(kubeletPlugin2.GetGRPCCalls()).Should(testdrivergomega.NodeUnprepareResourcesSucceeded, "Plugin 2 should have unprepared resources.")
|
||||
gomega.Expect(getKubeletMetrics(ctx)).Should(gstruct.MatchKeys(gstruct.IgnoreExtras, gstruct.Keys{
|
||||
"dra_resource_claims_in_use": gstruct.MatchAllElements(driverName, gstruct.Elements{
|
||||
"<any>": timelessSample(0),
|
||||
}),
|
||||
}), "metrics while pod is running")
|
||||
})
|
||||
|
||||
ginkgo.It("must run pod if NodePrepareResources fails for one plugin and then succeeds", func(ctx context.Context) {
|
||||
_, kubeletPlugin2 := start(ctx)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user