Skip to content

Commit da3a050

Browse files
authored
perf: memory optimizations in cluster cost tracking (kubernetes-sigs#3068)
1 parent 961af74 commit da3a050

2 files changed

Lines changed: 51 additions & 119 deletions

File tree

pkg/controllers/state/informer/pricing.go

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,7 @@ import (
2323

2424
"github.com/awslabs/operatorpkg/reconciler"
2525
"github.com/awslabs/operatorpkg/singleton"
26-
"github.com/samber/lo"
2726
"go.uber.org/multierr"
28-
"k8s.io/apimachinery/pkg/types"
2927

3028
controllerruntime "sigs.k8s.io/controller-runtime"
3129
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -44,7 +42,6 @@ type PricingController struct {
4442
client client.Client
4543
cloudProvider cloudprovider.CloudProvider
4644
clusterCost *cost.ClusterCost
47-
npOfMap map[types.NamespacedName]map[cost.OfferingKey]float64
4845
}
4946

5047
func NewPricingController(client client.Client, cloudProvider cloudprovider.CloudProvider, clusterCost *cost.ClusterCost) *PricingController {
@@ -62,54 +59,22 @@ func (c *PricingController) Reconcile(ctx context.Context) (reconciler.Result, e
6259
return reconciler.Result{}, err
6360
}
6461

65-
newNpOfMap := make(map[types.NamespacedName]map[cost.OfferingKey]float64)
6662
var errs error
6763
for _, np := range npl.Items {
68-
oldOfs, exists := c.npOfMap[client.ObjectKeyFromObject(&np)]
6964
newIts, err := c.cloudProvider.GetInstanceTypes(ctx, &np)
7065
if err != nil {
7166
errs = multierr.Append(errs, err)
7267
continue
7368
}
74-
75-
newNpOfMap[client.ObjectKeyFromObject(&np)] = make(map[cost.OfferingKey]float64)
76-
77-
for _, it := range newIts {
78-
for _, o := range it.Offerings {
79-
offeringKey := cost.OfferingKey{InstanceName: it.Name, Zone: o.Zone(), CapacityType: o.CapacityType()}
80-
newNpOfMap[client.ObjectKeyFromObject(&np)][offeringKey] = o.Price
81-
}
82-
}
83-
84-
if exists && equal(oldOfs, newNpOfMap[client.ObjectKeyFromObject(&np)]) {
85-
continue
86-
}
8769
c.clusterCost.UpdateOfferings(ctx, &np, newIts)
8870
}
8971
if errs != nil {
9072
return reconciler.Result{}, fmt.Errorf("refreshing pricing info, %w", errs)
9173
}
92-
c.npOfMap = newNpOfMap
9374

9475
return reconciler.Result{RequeueAfter: 1 * time.Hour}, nil
9576
}
9677

97-
func equal(oldOfs map[cost.OfferingKey]float64, newOfs map[cost.OfferingKey]float64) bool {
98-
if len(lo.Values(oldOfs)) != len(newOfs) {
99-
return false
100-
}
101-
for newOf, newPrice := range newOfs {
102-
oldPrice, exists := oldOfs[newOf]
103-
if !exists {
104-
return false
105-
}
106-
if oldPrice != newPrice {
107-
return false
108-
}
109-
}
110-
return true
111-
}
112-
11378
func (c *PricingController) Name() string {
11479
return "state.pricing"
11580
}

pkg/state/cost/cost.go

Lines changed: 51 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ var (
6868
type ClusterCost struct {
6969
sync.RWMutex
7070
npCostMap map[string]*NodePoolCost // nodepool.Name -> NodePoolCost
71-
// nodeClaimSet tracks which NodeClaims are currently being monitored for cost
71+
// nodeClaimMap tracks which NodeClaims are currently being monitored for cost
7272
nodeClaimMap map[types.NamespacedName]NodeClaimMetaData // nodeClaim object key -> NodeClaimMetaData
7373

7474
cloudProvider cloudprovider.CloudProvider
@@ -79,7 +79,7 @@ type ClusterCost struct {
7979
// It maintains the current cost, available instance types, and count of active offerings.
8080
type NodePoolCost struct {
8181
cost float64
82-
// offeringCounts tracks how many instances of each offering type are currently active
82+
// offeringCounts tracks how many instances of each offering type are currently active (Count > 0)
8383
offeringCounts map[OfferingKey]OfferingCount
8484
}
8585

@@ -103,7 +103,7 @@ type NodeClaimMetaData struct {
103103

104104
// NewClusterCost creates and initializes a new ClusterCost instance for tracking
105105
// compute costs across the cluster. It requires a cloud provider for accessing
106-
// instance type and pricing information, and a Kubernetes client for NodePool loofferingKeyups.
106+
// instance type and pricing information, and a Kubernetes client for NodePool lookups.
107107
func NewClusterCost(ctx context.Context, cloudProvider cloudprovider.CloudProvider, client client.Client) *ClusterCost {
108108
return &ClusterCost{
109109
npCostMap: make(map[string]*NodePoolCost),
@@ -125,75 +125,35 @@ func (cc *ClusterCost) UpdateOfferings(ctx context.Context, np *v1.NodePool, ins
125125
cc.internalUpdateOfferings(np, instanceTypes)
126126
}
127127

128-
func (cc *ClusterCost) internalNodepoolUpdate(ctx context.Context, np *v1.NodePool) error {
129-
instanceTypes, err := cc.cloudProvider.GetInstanceTypes(ctx, np)
130-
if err != nil {
131-
return fmt.Errorf("failed to get instance types for nodepool %q, %w", np.Name, err)
132-
}
133-
cc.internalUpdateOfferings(np, instanceTypes)
134-
return nil
135-
}
136-
137128
func (cc *ClusterCost) internalUpdateOfferings(np *v1.NodePool, instanceTypes []*cloudprovider.InstanceType) {
138-
npCost, exists := cc.npCostMap[np.Name]
129+
instanceTypes = lo.Filter(instanceTypes, func(it *cloudprovider.InstanceType, _ int) bool {
130+
return it != nil
131+
})
139132

133+
npCost, exists := cc.npCostMap[np.Name]
140134
if !exists {
141-
cc.createNewNodePoolCost(np.Name, instanceTypes)
142-
} else {
143-
instanceTypes = lo.Filter(instanceTypes, func(it *cloudprovider.InstanceType, _ int) bool {
144-
return it != nil
145-
})
146-
newMap := map[OfferingKey]OfferingCount{}
147-
for _, it := range instanceTypes {
148-
for _, o := range it.Offerings {
149-
offeringKey := OfferingKey{InstanceName: it.Name, Zone: o.Zone(), CapacityType: o.CapacityType()}
150-
oldCount, exists := npCost.offeringCounts[offeringKey]
151-
newMap[offeringKey] = OfferingCount{
152-
Count: lo.Ternary(exists, oldCount.Count, 0),
153-
Price: o.Price,
154-
}
155-
}
156-
}
157-
// Add back all of the offering counts that don't exist in the new instance types
158-
// This can't occur on container restart, so we may lose cost data from offerings that are no longer returned
159-
// from the cloud provider but still have nodeclaims.
160-
for key, count := range npCost.offeringCounts {
161-
_, exists := newMap[key]
162-
if !exists {
163-
newMap[key] = count
164-
}
165-
}
166-
167-
npCost.offeringCounts = newMap
168-
// re-calculate the cost as the instances have changed
169-
cost := npCost.updateCost()
170-
cc.npCostMap[np.Name].cost = cost
171-
}
172-
}
173-
174-
func (npc *NodePoolCost) updateCost() float64 {
175-
cost := 0.0
176-
for _, oc := range npc.offeringCounts {
177-
// add the new price times the count of that offering
178-
cost = cost + (float64(oc.Count) * oc.Price)
135+
cc.npCostMap[np.Name] = &NodePoolCost{offeringCounts: make(map[OfferingKey]OfferingCount), cost: 0.0}
136+
return
179137
}
180-
return cost
181-
}
182138

183-
func (cc *ClusterCost) createNewNodePoolCost(npName string, instanceTypes []*cloudprovider.InstanceType) {
184-
// create the new npc
185-
cc.npCostMap[npName] = &NodePoolCost{
186-
offeringCounts: make(map[OfferingKey]OfferingCount),
187-
cost: 0.0,
188-
}
139+
// Build a temporary price index from the new instance types
140+
prices := make(map[OfferingKey]float64, len(instanceTypes)*3)
189141
for _, it := range instanceTypes {
190142
for _, o := range it.Offerings {
191-
cc.npCostMap[npName].offeringCounts[OfferingKey{InstanceName: it.Name, Zone: o.Zone(), CapacityType: o.CapacityType()}] = OfferingCount{
192-
Count: 0,
193-
Price: o.Price,
194-
}
143+
prices[OfferingKey{InstanceName: it.Name, Zone: o.Zone(), CapacityType: o.CapacityType()}] = o.Price
144+
}
145+
}
146+
147+
// Update prices on active offerings and recalculate cost
148+
cost := 0.0
149+
for key, oc := range npCost.offeringCounts {
150+
if newPrice, ok := prices[key]; ok {
151+
oc.Price = newPrice
152+
npCost.offeringCounts[key] = oc
195153
}
154+
cost += float64(oc.Count) * oc.Price
196155
}
156+
npCost.cost = cost
197157
}
198158

199159
// UpdateNodeClaim adds a NodeClaim to cost tracking. The NodeClaim must have
@@ -286,38 +246,30 @@ func (cc *ClusterCost) internalAddOffering(ctx context.Context, npName string, o
286246
return err
287247
}
288248

289-
_, exists := cc.npCostMap[npName]
290-
if !exists {
291-
// create the new npc
292-
instanceTypes, err := cc.cloudProvider.GetInstanceTypes(ctx, np)
293-
if err != nil {
294-
return fmt.Errorf("failed to get instance types for new nodepool %q while adding offering for instance %q, %w", np.Name, offeringKey.InstanceName, err)
295-
}
296-
cc.createNewNodePoolCost(npName, instanceTypes)
249+
if _, exists := cc.npCostMap[npName]; !exists {
250+
cc.npCostMap[npName] = &NodePoolCost{offeringCounts: make(map[OfferingKey]OfferingCount), cost: 0.0}
297251
}
298252

299253
oc, exists := cc.npCostMap[npName].offeringCounts[offeringKey]
300254
if !exists {
301-
// our offerings must be out of date, we should update and retry
302-
err := cc.internalNodepoolUpdate(ctx, np)
255+
instanceTypes, err := cc.cloudProvider.GetInstanceTypes(ctx, np)
303256
if err != nil {
304-
return fmt.Errorf("failed to update nodepool %q during retry while searching for offering for instance %q in zone %q with capacity %q, %w", np.Name, offeringKey.InstanceName, offeringKey.Zone, offeringKey.CapacityType, err)
257+
return fmt.Errorf("failed to get instance types for nodepool %q while adding offering for instance %q, %w", np.Name, offeringKey.InstanceName, err)
305258
}
306-
oc, exists = cc.npCostMap[npName].offeringCounts[offeringKey]
307-
if !exists {
308-
// Start at 0; the unconditional oc.Count += 1 below accounts for this add.
309-
oc = OfferingCount{Count: 0, Price: 0.0}
310-
log.FromContext(ctx).Error(fmt.Errorf("failed to find offering %q during retry while searching for instance %q in zone %q with capacity %q in nodepool %q", offeringKey, offeringKey.InstanceName, offeringKey.Zone, offeringKey.CapacityType, npName), "offering price unknown after retry — cost tracking will undercount for this nodeclaim until next UpdateOfferings")
259+
price, found := findOfferingPrice(instanceTypes, offeringKey)
260+
if !found {
261+
log.FromContext(ctx).Error(fmt.Errorf("failed to find offering for instance %q in zone %q with capacity %q in nodepool %q", offeringKey.InstanceName, offeringKey.Zone, offeringKey.CapacityType, npName), "offering price unknown — cost tracking will undercount for this nodeclaim until next update")
311262
}
263+
oc = OfferingCount{Count: 0, Price: price}
312264
}
313-
oc.Count += 1
265+
oc.Count++
314266
cc.npCostMap[npName].offeringCounts[offeringKey] = oc
315267
cc.npCostMap[npName].cost += oc.Price
316268
return nil
317269
}
318270

319271
// internalRemoveOffering updates the internal clusterCost state to remove an existing offering for a given nodepool.
320-
// It is used to decrement the overall cost when a node leeaves the cluster. It is only called by DeleteNodeClaim
272+
// It is used to decrement the overall cost when a node leaves the cluster. It is only called by DeleteNodeClaim
321273
// after that function has determined if a nodeclaim is already being accounted for.
322274
func (cc *ClusterCost) internalRemoveOffering(npName string, offeringKey OfferingKey) error {
323275
npc, exists := cc.npCostMap[npName]
@@ -330,13 +282,14 @@ func (cc *ClusterCost) internalRemoveOffering(npName string, offeringKey Offerin
330282
return fmt.Errorf("attempted to remove nonexistent offering from nodepool %q (instance, %q, zone, %q, capacity, %q)", npName, offeringKey.InstanceName, offeringKey.Zone, offeringKey.CapacityType)
331283
}
332284

333-
oc.Count -= 1
334-
npc.offeringCounts[offeringKey] = oc
285+
oc.Count--
335286
npc.cost -= oc.Price
336287
if oc.Count == 0 {
337288
delete(npc.offeringCounts, offeringKey)
289+
} else {
290+
npc.offeringCounts[offeringKey] = oc
338291
}
339-
if len(lo.Values(npc.offeringCounts)) == 0 {
292+
if len(npc.offeringCounts) == 0 {
340293
delete(cc.npCostMap, npName)
341294
}
342295
return nil
@@ -369,6 +322,20 @@ func (cc *ClusterCost) GetNodepoolCost(np *v1.NodePool) float64 {
369322
return npc.cost
370323
}
371324

325+
func findOfferingPrice(instanceTypes []*cloudprovider.InstanceType, key OfferingKey) (float64, bool) {
326+
for _, it := range instanceTypes {
327+
if it == nil || it.Name != key.InstanceName {
328+
continue
329+
}
330+
for _, o := range it.Offerings {
331+
if o.Zone() == key.Zone && o.CapacityType() == key.CapacityType {
332+
return o.Price, true
333+
}
334+
}
335+
}
336+
return 0, false
337+
}
338+
372339
func nodeClaimMissingLabels(nc v1.NodeClaim) bool {
373340
var missingLabels []string
374341
for _, key := range NecessaryLabels {

0 commit comments

Comments
 (0)