Skip to content

Commit b85a8be

Browse files
committed
Add CohortSubtreeResourceUsage metric
1 parent da997b9 commit b85a8be

9 files changed

Lines changed: 356 additions & 35 deletions

File tree

pkg/cache/scheduler/cache.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,19 @@ type Cache struct {
141141
customLabels *metrics.CustomLabels
142142
}
143143

144+
func (c *Cache) GetDirectCohortNameForWorkload(wl *kueue.Workload) kueue.CohortReference {
145+
if wl == nil || wl.Status.Admission == nil {
146+
return ""
147+
}
148+
c.RLock()
149+
defer c.RUnlock()
150+
cq := c.hm.ClusterQueue(wl.Status.Admission.ClusterQueue)
151+
if cq == nil || !cq.HasParent() {
152+
return ""
153+
}
154+
return cq.Parent().Name
155+
}
156+
144157
func New(client client.Client, options ...Option) *Cache {
145158
cache := &Cache{
146159
client: client,

pkg/cache/scheduler/cache_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3753,3 +3753,64 @@ func TestGetWorkloadFromCache(t *testing.T) {
37533753
})
37543754
}
37553755
}
3756+
3757+
func TestGetDirectCohortNameForWorkload(t *testing.T) {
3758+
now := time.Now().Truncate(time.Second)
3759+
3760+
cases := map[string]struct {
3761+
clusterQueues []kueue.ClusterQueue
3762+
wl *kueue.Workload
3763+
wantCohort kueue.CohortReference
3764+
}{
3765+
"nil workload": {
3766+
wl: nil,
3767+
wantCohort: "",
3768+
},
3769+
"workload without admission": {
3770+
wl: utiltestingapi.MakeWorkload("wl", "ns").Obj(),
3771+
wantCohort: "",
3772+
},
3773+
"cluster queue not found": {
3774+
wl: utiltestingapi.MakeWorkload("wl", "ns").ReserveQuotaAt(&kueue.Admission{
3775+
ClusterQueue: "missing-cq",
3776+
}, now).Obj(),
3777+
wantCohort: "",
3778+
},
3779+
"cluster queue without cohort": {
3780+
clusterQueues: []kueue.ClusterQueue{
3781+
*utiltestingapi.MakeClusterQueue("cq").Obj(),
3782+
},
3783+
wl: utiltestingapi.MakeWorkload("wl", "ns").ReserveQuotaAt(&kueue.Admission{
3784+
ClusterQueue: "cq",
3785+
}, now).Obj(),
3786+
wantCohort: "",
3787+
},
3788+
"returns workload cohort": {
3789+
clusterQueues: []kueue.ClusterQueue{
3790+
*utiltestingapi.MakeClusterQueue("cq").Cohort("team-a").Obj(),
3791+
},
3792+
wl: utiltestingapi.MakeWorkload("wl", "ns").ReserveQuotaAt(&kueue.Admission{
3793+
ClusterQueue: "cq",
3794+
}, now).Obj(),
3795+
wantCohort: "team-a",
3796+
},
3797+
}
3798+
3799+
for name, tc := range cases {
3800+
t.Run(name, func(t *testing.T) {
3801+
ctx, _ := utiltesting.ContextWithLog(t)
3802+
cache := New(utiltesting.NewFakeClient())
3803+
3804+
for i := range tc.clusterQueues {
3805+
if err := cache.AddClusterQueue(ctx, &tc.clusterQueues[i]); err != nil {
3806+
t.Fatalf("failed adding clusterQueue %s: %v", tc.clusterQueues[i].Name, err)
3807+
}
3808+
}
3809+
3810+
got := cache.GetDirectCohortNameForWorkload(tc.wl)
3811+
if diff := cmp.Diff(tc.wantCohort, got); diff != "" {
3812+
t.Errorf("GetDirectCohortNameForWorkload() mismatch (-want,+got):\n%s", diff)
3813+
}
3814+
})
3815+
}
3816+
}

pkg/cache/scheduler/cohort_metrics.go

Lines changed: 66 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ limitations under the License.
1717
package scheduler
1818

1919
import (
20+
"maps"
21+
"slices"
22+
2023
"github.com/go-logr/logr"
24+
"k8s.io/apimachinery/pkg/util/sets"
2125

2226
kueue "sigs.k8s.io/kueue/apis/kueue/v1beta2"
2327
"sigs.k8s.io/kueue/pkg/cache/hierarchy"
@@ -28,7 +32,8 @@ import (
2832
type cohortMetricPoint struct {
2933
cohortName kueue.CohortReference
3034
flavorResource resources.FlavorResource
31-
qty int64
35+
quotaQty int64
36+
usageQty int64
3237
}
3338

3439
func (c *Cache) RecordCohortMetrics(log logr.Logger, cohortName kueue.CohortReference) {
@@ -73,21 +78,39 @@ func (c *Cache) collectCohortMetricPoints(cohortName kueue.CohortReference, simu
7378
}
7479

7580
removedQuota := ch.resourceNode.SubtreeQuota
81+
82+
// Cache total subtree usage calls during this run
83+
usageCache := make(map[*cohort]resources.FlavorResourceQuantities)
84+
removedUsage := totalSubtreeUsageWithCache(ch, usageCache)
85+
7686
var points []cohortMetricPoint
7787
for ancestor := range ch.PathSelfToRoot() {
7888
quotas := ancestor.resourceNode.SubtreeQuota
89+
usages := totalSubtreeUsageWithCache(ancestor, usageCache)
90+
91+
var baselineUsage resources.FlavorResourceQuantities
7992
if simulateRemoval {
8093
quotas = removedQuota
94+
usages = removedUsage
95+
baselineUsage = totalSubtreeUsageWithCache(ancestor, usageCache)
8196
}
8297

83-
for flr, qty := range quotas {
98+
keys := sets.New[resources.FlavorResource]()
99+
keys.Insert(slices.Collect(maps.Keys(quotas))...)
100+
keys.Insert(slices.Collect(maps.Keys(usages))...)
101+
102+
for fr := range keys {
103+
quotaQty := quotas[fr]
104+
usageQty := usages[fr]
84105
if simulateRemoval {
85-
qty = max(ancestor.resourceNode.SubtreeQuota[flr]-qty, 0)
106+
quotaQty = max(ancestor.resourceNode.SubtreeQuota[fr]-quotaQty, 0)
107+
usageQty = max(baselineUsage[fr]-usageQty, 0)
86108
}
87109
points = append(points, cohortMetricPoint{
88110
cohortName: ancestor.Name,
89-
flavorResource: flr,
90-
qty: qty,
111+
flavorResource: fr,
112+
quotaQty: quotaQty,
113+
usageQty: usageQty,
91114
})
92115
}
93116
}
@@ -99,19 +122,43 @@ func (c *Cache) withCohortLogger(log logr.Logger, cohortName kueue.CohortReferen
99122
}
100123

101124
func (c *Cache) applyCohortMetricPoint(p cohortMetricPoint) {
102-
if p.qty <= 0 {
103-
metrics.ClearCohortSubtreeQuota(
104-
p.cohortName,
105-
string(p.flavorResource.Flavor),
106-
string(p.flavorResource.Resource),
107-
)
108-
return
125+
flavor := p.flavorResource.Flavor
126+
resource := p.flavorResource.Resource
127+
128+
if p.quotaQty <= 0 {
129+
metrics.ClearCohortSubtreeQuota(p.cohortName, flavor, resource)
130+
} else {
131+
metrics.ReportCohortSubtreeQuota(p.cohortName, flavor, resource, p.quotaQty, c.roleTracker)
132+
}
133+
134+
if p.usageQty <= 0 {
135+
metrics.ClearCohortSubtreeResourceUsage(p.cohortName, flavor, resource)
136+
} else {
137+
metrics.ReportCohortSubtreeResourceUsage(p.cohortName, flavor, resource, p.usageQty, c.roleTracker)
138+
}
139+
}
140+
141+
func accumulateUsage(total, usage resources.FlavorResourceQuantities) {
142+
for fr, qty := range usage {
143+
total[fr] += qty
144+
}
145+
}
146+
147+
// totalSubtreeUsageWithCache returns the sum of *actual* resource usage in the subtree rooted at given cohort
148+
func totalSubtreeUsageWithCache(ch *cohort, cache map[*cohort]resources.FlavorResourceQuantities) resources.FlavorResourceQuantities {
149+
if cached, found := cache[ch]; found {
150+
return cached
109151
}
110-
metrics.ReportCohortSubtreeQuota(
111-
p.cohortName,
112-
string(p.flavorResource.Flavor),
113-
string(p.flavorResource.Resource),
114-
float64(p.qty),
115-
c.roleTracker,
116-
)
152+
153+
total := make(resources.FlavorResourceQuantities)
154+
for _, cq := range ch.ChildCQs() {
155+
accumulateUsage(total, cq.getResourceNode().Usage)
156+
}
157+
158+
for _, child := range ch.ChildCohorts() {
159+
accumulateUsage(total, totalSubtreeUsageWithCache(child, cache))
160+
}
161+
162+
cache[ch] = total
163+
return total
117164
}

pkg/controller/core/workload_controller.go

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ func (r *WorkloadReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
317317
return ctrl.Result{}, err
318318
}
319319
} else {
320-
if !r.cache.AddOrUpdateWorkload(log, wl.DeepCopy()) {
320+
if !r.addOrUpdateWorkload(log, wl.DeepCopy()) {
321321
log.V(2).Info("ClusterQueue for workload didn't exist; ignored for now")
322322
}
323323
}
@@ -607,7 +607,7 @@ func (r *WorkloadReconciler) deleteWorkloadFromCaches(ctx context.Context, names
607607
// by the scheduler, and leaving them blocks ClusterQueue finalizer removal.
608608
// The operation is idempotent if the workload was never in the cache.
609609
r.queues.QueueAssociatedInadmissibleWorkloadsAfter(ctx, wlRef, func() {
610-
if err := r.cache.DeleteWorkload(log, wlRef); err != nil {
610+
if err := r.deleteWorkload(log, wlRef); err != nil {
611611
log.Error(err, "Failed to delete workload from cache")
612612
}
613613
})
@@ -977,7 +977,7 @@ func (r *WorkloadReconciler) Create(e event.TypedCreateEvent[*kueue.Workload]) b
977977
}
978978
return true
979979
}
980-
if !r.cache.AddOrUpdateWorkload(log, wlCopy) {
980+
if !r.addOrUpdateWorkload(log, wlCopy) {
981981
log.V(2).Info("ClusterQueue for workload didn't exist; ignored for now")
982982
}
983983
r.queues.QueueSecondPassIfNeeded(ctx, e.Object, 0)
@@ -1050,7 +1050,7 @@ func (r *WorkloadReconciler) Update(e event.TypedUpdateEvent[*kueue.Workload]) b
10501050
// Delete the workload from cache while holding the queues lock
10511051
// to guarantee that requeued workloads are taken into account before
10521052
// the next scheduling cycle.
1053-
if err := r.cache.DeleteWorkload(log, wlKey); err != nil && prevStatus == workload.StatusAdmitted {
1053+
if err := r.deleteWorkload(log, wlKey); err != nil && prevStatus == workload.StatusAdmitted {
10541054
log.Error(err, "Failed to delete workload from cache")
10551055
}
10561056
})
@@ -1065,7 +1065,7 @@ func (r *WorkloadReconciler) Update(e event.TypedUpdateEvent[*kueue.Workload]) b
10651065
}
10661066
case prevStatus == workload.StatusPending && (status == workload.StatusQuotaReserved || status == workload.StatusAdmitted):
10671067
r.queues.DeleteWorkload(log, wlKey)
1068-
if !r.cache.AddOrUpdateWorkload(log, wlCopy) {
1068+
if !r.addOrUpdateWorkload(log, wlCopy) {
10691069
log.V(2).Info("ClusterQueue for workload didn't exist; ignored for now")
10701070
}
10711071
if afs.Enabled(r.admissionFSConfig) && status == workload.StatusAdmitted && r.cache.ClusterQueueUsesAdmissionFairSharing(wlCopy.Status.Admission.ClusterQueue) {
@@ -1083,7 +1083,7 @@ func (r *WorkloadReconciler) Update(e event.TypedUpdateEvent[*kueue.Workload]) b
10831083
// Delete the workload from cache while holding the queues lock
10841084
// to guarantee that requeued workloads are taken into account before
10851085
// the next scheduling cycle.
1086-
if err := r.cache.DeleteWorkload(log, wlKey); err != nil {
1086+
if err := r.deleteWorkload(log, wlKey); err != nil {
10871087
log.Error(err, "Failed to delete workload from cache")
10881088
}
10891089
// Here we don't take the lock as it is already taken by the wrapping function.
@@ -1111,13 +1111,13 @@ func (r *WorkloadReconciler) Update(e event.TypedUpdateEvent[*kueue.Workload]) b
11111111
// Update the workload from cache while holding the queues lock
11121112
// to guarantee that requeued workloads are taken into account before
11131113
// the next scheduling cycle.
1114-
r.cache.AddOrUpdateWorkload(log, wlCopy)
1114+
r.addOrUpdateWorkload(log, wlCopy)
11151115
})
11161116

11171117
default:
11181118
// Workload update in the cache is handled here; however, some fields are immutable
11191119
// and are not supposed to actually change anything.
1120-
r.cache.AddOrUpdateWorkload(log, wlCopy)
1120+
r.addOrUpdateWorkload(log, wlCopy)
11211121
}
11221122
r.queues.QueueSecondPassIfNeeded(ctx, wlCopy, 0)
11231123
return true
@@ -1452,3 +1452,23 @@ func (h *draEventHandler) Generic(ctx context.Context, e event.TypedGenericEvent
14521452
func (r *WorkloadReconciler) GetDRAReconcileChannel() chan<- event.TypedGenericEvent[*kueue.Workload] {
14531453
return r.draReconcileChannel
14541454
}
1455+
1456+
func (r *WorkloadReconciler) addOrUpdateWorkload(log logr.Logger, wl *kueue.Workload) bool {
1457+
updated := r.cache.AddOrUpdateWorkload(log, wl)
1458+
if wl != nil && workload.HasQuotaReservation(wl) {
1459+
r.cache.RecordCohortMetrics(log, r.cache.GetDirectCohortNameForWorkload(wl))
1460+
}
1461+
1462+
return updated
1463+
}
1464+
1465+
func (r *WorkloadReconciler) deleteWorkload(log logr.Logger, wlKey workload.Reference) error {
1466+
wl := r.cache.GetWorkloadFromCache(wlKey)
1467+
cohortName := r.cache.GetDirectCohortNameForWorkload(wl)
1468+
err := r.cache.DeleteWorkload(log, wlKey)
1469+
if err != nil {
1470+
return err
1471+
}
1472+
r.cache.RecordCohortMetrics(log, cohortName)
1473+
return nil
1474+
}

pkg/metrics/metrics.go

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"time"
2121

2222
"github.com/prometheus/client_golang/prometheus"
23+
corev1 "k8s.io/api/core/v1"
2324
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2425
"sigs.k8s.io/controller-runtime/pkg/metrics"
2526

@@ -254,6 +255,10 @@ var (
254255
// +metricsdoc:group=cohort
255256
// +metricsdoc:labels=cohort="the name of the Cohort",flavor="the resource flavor name",resource="the resource name",replica_role="one of `leader`, `follower`, or `standalone`"
256257
CohortSubtreeQuota *prometheus.GaugeVec
258+
259+
// +metricsdoc:group=cohort
260+
// +metricsdoc:labels=cohort="the name of the Cohort",flavor="the resource flavor name",resource="the resource name",replica_role="one of `leader`, `follower`, or `standalone`"
261+
CohortSubtreeResourceUsage *prometheus.GaugeVec
257262
)
258263

259264
func trackGaugeVec(g *prometheus.GaugeVec) *prometheus.GaugeVec {
@@ -728,6 +733,14 @@ If the Cohort has a weight of zero and is borrowing, this will return NaN.`,
728733
Help: `Reports the cohort's nominal quota aggregated within the cohort's subtree. The values are reported per resource and flavor`,
729734
}, []string{"cohort", "flavor", "resource", "replica_role"},
730735
))
736+
737+
CohortSubtreeResourceUsage = trackGaugeVec(prometheus.NewGaugeVec(
738+
prometheus.GaugeOpts{
739+
Subsystem: constants.KueueName,
740+
Name: "cohort_subtree_resource_usage",
741+
Help: `Reports the cohort's resource usage aggregated within the cohort's subtree. The values are reported per resource and flavor`,
742+
}, []string{"cohort", "flavor", "resource", "replica_role"},
743+
))
731744
}
732745

733746
func init() {
@@ -916,6 +929,7 @@ func ClearLocalQueueMetrics(lq LocalQueueReference) {
916929
func ClearCohortMetrics(cohortName string) {
917930
CohortSubtreeQuota.DeletePartialMatch(prometheus.Labels{"cohort": cohortName})
918931
CohortWeightedShare.DeletePartialMatch(prometheus.Labels{"cohort": cohortName})
932+
CohortSubtreeResourceUsage.DeletePartialMatch(prometheus.Labels{"cohort": cohortName})
919933
}
920934

921935
func ReportClusterQueueStatus(cqName kueue.ClusterQueueReference, cqStatus ClusterQueueStatus, customLabelValues []string, tracker *roletracker.RoleTracker) {
@@ -965,23 +979,35 @@ func ReportClusterQueueQuotas(cohort kueue.CohortReference, queue, flavor, resou
965979
ClusterQueueResourceLendingLimit.WithLabelValues(labels...).Set(lending)
966980
}
967981

968-
func ReportCohortSubtreeQuota(cohort kueue.CohortReference, flavor, resource string, quota float64, tracker *roletracker.RoleTracker) {
969-
CohortSubtreeQuota.WithLabelValues(string(cohort), flavor, resource, roletracker.GetRole(tracker)).Set(quota)
982+
func ReportCohortSubtreeQuota(cohort kueue.CohortReference, flavor kueue.ResourceFlavorReference, resource corev1.ResourceName, quota int64, tracker *roletracker.RoleTracker) {
983+
CohortSubtreeQuota.WithLabelValues(string(cohort), string(flavor), string(resource), roletracker.GetRole(tracker)).Set(float64(quota))
970984
}
971985

972-
func ClearCohortSubtreeQuota(cohort kueue.CohortReference, flavor, resource string) {
973-
lbls := prometheus.Labels{
974-
"cohort": string(cohort),
975-
}
986+
func cohortPartialMatchLabels(cohort kueue.CohortReference, flavor, resource string) prometheus.Labels {
987+
lbls := prometheus.Labels{"cohort": string(cohort)}
976988
if len(flavor) != 0 {
977989
lbls["flavor"] = flavor
978990
}
979991
if len(resource) != 0 {
980992
lbls["resource"] = resource
981993
}
994+
return lbls
995+
}
996+
997+
func ClearCohortSubtreeQuota(cohort kueue.CohortReference, flavor kueue.ResourceFlavorReference, resource corev1.ResourceName) {
998+
lbls := cohortPartialMatchLabels(cohort, string(flavor), string(resource))
982999
CohortSubtreeQuota.DeletePartialMatch(lbls)
9831000
}
9841001

1002+
func ReportCohortSubtreeResourceUsage(cohort kueue.CohortReference, flavor kueue.ResourceFlavorReference, resource corev1.ResourceName, usage int64, tracker *roletracker.RoleTracker) {
1003+
CohortSubtreeResourceUsage.WithLabelValues(string(cohort), string(flavor), string(resource), roletracker.GetRole(tracker)).Set(float64(usage))
1004+
}
1005+
1006+
func ClearCohortSubtreeResourceUsage(cohort kueue.CohortReference, flavor kueue.ResourceFlavorReference, resource corev1.ResourceName) {
1007+
lbls := cohortPartialMatchLabels(cohort, string(flavor), string(resource))
1008+
CohortSubtreeResourceUsage.DeletePartialMatch(lbls)
1009+
}
1010+
9851011
func ReportClusterQueueResourceReservations(cohort kueue.CohortReference, queue, flavor, resource string, usage float64, customLabelValues []string, tracker *roletracker.RoleTracker) {
9861012
labels := append([]string{string(cohort), queue, flavor, resource, roletracker.GetRole(tracker)}, customLabelValues...)
9871013
ClusterQueueResourceReservations.WithLabelValues(labels...).Set(usage)
@@ -1146,6 +1172,7 @@ func Register() {
11461172
ClusterQueueWeightedShare,
11471173
CohortWeightedShare,
11481174
CohortSubtreeQuota,
1175+
CohortSubtreeResourceUsage,
11491176
)
11501177
if features.Enabled(features.LocalQueueMetrics) {
11511178
RegisterLQMetrics()

0 commit comments

Comments
 (0)