|
|
@@ -782,8 +782,10 @@ func (as *AllocationSet) AggregateBy(aggregateBy []string, options *AllocationAg
|
|
|
// the output (i.e. they can be used to generate a valid key for
|
|
|
// the given properties) then aggregate; otherwise... ignore them?
|
|
|
// 10. If the merge idle option is enabled, merge any remaining idle
|
|
|
- // allocations into a single idle allocation
|
|
|
-
|
|
|
+ // allocations into a single idle allocation. If there was any idle
|
|
|
+ // whose costs were not distributed because there was no usage of a
|
|
|
+ // specific resource type, re-add the idle to the aggregation with
|
|
|
+ // only that type.
|
|
|
if as.IsEmpty() {
|
|
|
return nil
|
|
|
}
|
|
|
@@ -796,6 +798,8 @@ func (as *AllocationSet) AggregateBy(aggregateBy []string, options *AllocationAg
|
|
|
options.LabelConfig = NewLabelConfig()
|
|
|
}
|
|
|
|
|
|
+ var undistributedIdleMap map[string]bool
|
|
|
+
|
|
|
// If aggregateBy is nil, we don't aggregate anything. On the other hand,
|
|
|
// an empty slice implies that we should aggregate everything. See
|
|
|
// generateKey for why that makes sense.
|
|
|
@@ -919,7 +923,7 @@ func (as *AllocationSet) AggregateBy(aggregateBy []string, options *AllocationAg
|
|
|
// the shared allocations).
|
|
|
var idleCoefficients map[string]map[string]map[string]float64
|
|
|
if idleSet.Length() > 0 && options.ShareIdle != ShareNone {
|
|
|
- idleCoefficients, err = computeIdleCoeffs(options, as, shareSet)
|
|
|
+ idleCoefficients, undistributedIdleMap, err = computeIdleCoeffs(options, as, shareSet)
|
|
|
if err != nil {
|
|
|
log.Warningf("AllocationSet.AggregateBy: compute idle coeff: %s", err)
|
|
|
return fmt.Errorf("error computing idle coefficients: %s", err)
|
|
|
@@ -952,7 +956,7 @@ func (as *AllocationSet) AggregateBy(aggregateBy []string, options *AllocationAg
|
|
|
// need to track this on a per-cluster or per-node, per-allocation, per-resource basis.
|
|
|
var idleFiltrationCoefficients map[string]map[string]map[string]float64
|
|
|
if len(options.FilterFuncs) > 0 && options.ShareIdle == ShareNone {
|
|
|
- idleFiltrationCoefficients, err = computeIdleCoeffs(options, as, shareSet)
|
|
|
+ idleFiltrationCoefficients, _, err = computeIdleCoeffs(options, as, shareSet)
|
|
|
if err != nil {
|
|
|
return fmt.Errorf("error computing idle filtration coefficients: %s", err)
|
|
|
}
|
|
|
@@ -1214,6 +1218,66 @@ func (as *AllocationSet) AggregateBy(aggregateBy []string, options *AllocationAg
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // In the edge case that some idle has not been distributed because
|
|
|
+ // there is no usage of that resource type, add idle back to
|
|
|
+ // aggregations with only that cost applied.
|
|
|
+
|
|
|
+ // E.g. in the case where we have a result that looks like this on the
|
|
|
+ // frontend:
|
|
|
+
|
|
|
+ // Name CPU GPU RAM
|
|
|
+ // __idle__ $10 $12 $6
|
|
|
+ // kubecost $2 $0 $1
|
|
|
+
|
|
|
+ // Sharing idle weighted would result in no idle GPU cost being
|
|
|
+ // distributed, because the coefficient for the kubecost GPU cost would
|
|
|
+ // be zero. Thus, instead we re-add idle to the aggSet with distributed
|
|
|
+ // costs zeroed out but the undistributed costs left in.
|
|
|
+
|
|
|
+ // Name CPU GPU RAM
|
|
|
+ // __idle__ $0 $12 $0
|
|
|
+ // kubecost $12 $0 $7
|
|
|
+
|
|
|
+ if idleSet.Length() > 0 && !options.SplitIdle {
|
|
|
+ if undistributedIdleMap["cpu"] || undistributedIdleMap["gpu"] || undistributedIdleMap["ram"] {
|
|
|
+
|
|
|
+ for _, idleAlloc := range idleSet.allocations {
|
|
|
+
|
|
|
+ skip := false
|
|
|
+
|
|
|
+ // if the idle does not apply to the non-filtered values, skip it
|
|
|
+ for _, ff := range options.FilterFuncs {
|
|
|
+ if !ff(idleAlloc) {
|
|
|
+ skip = true
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if skip {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ // if the idle doesn't have a cost to be shared, also skip it
|
|
|
+ if idleAlloc.CPUCost != 0 && idleAlloc.GPUCost != 0 && idleAlloc.RAMCost != 0 {
|
|
|
+
|
|
|
+ // artificially set the already shared costs to zero
|
|
|
+ if !undistributedIdleMap["cpu"] {
|
|
|
+ idleAlloc.CPUCost = 0
|
|
|
+ }
|
|
|
+ if !undistributedIdleMap["gpu"] {
|
|
|
+ idleAlloc.GPUCost = 0
|
|
|
+ }
|
|
|
+ if !undistributedIdleMap["ram"] {
|
|
|
+ idleAlloc.RAMCost = 0
|
|
|
+ }
|
|
|
+
|
|
|
+ idleAlloc.Name = IdleSuffix
|
|
|
+ aggSet.Insert(idleAlloc)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
as.allocations = aggSet.allocations
|
|
|
|
|
|
return nil
|
|
|
@@ -1288,8 +1352,13 @@ func computeShareCoeffs(aggregateBy []string, options *AllocationAggregationOpti
|
|
|
return coeffs, nil
|
|
|
}
|
|
|
|
|
|
-func computeIdleCoeffs(options *AllocationAggregationOptions, as *AllocationSet, shareSet *AllocationSet) (map[string]map[string]map[string]float64, error) {
|
|
|
+func computeIdleCoeffs(options *AllocationAggregationOptions, as *AllocationSet, shareSet *AllocationSet) (map[string]map[string]map[string]float64, map[string]bool, error) {
|
|
|
types := []string{"cpu", "gpu", "ram"}
|
|
|
+ undistributedIdleMap := map[string]bool{
|
|
|
+ "cpu": true,
|
|
|
+ "gpu": true,
|
|
|
+ "ram": true,
|
|
|
+ }
|
|
|
|
|
|
// Compute idle coefficients, then save them in AllocationAggregationOptions
|
|
|
coeffs := map[string]map[string]map[string]float64{}
|
|
|
@@ -1345,6 +1414,7 @@ func computeIdleCoeffs(options *AllocationAggregationOptions, as *AllocationSet,
|
|
|
totals[idleId]["gpu"] += alloc.GPUTotalCost()
|
|
|
totals[idleId]["ram"] += alloc.RAMTotalCost()
|
|
|
}
|
|
|
+
|
|
|
}
|
|
|
|
|
|
// Do the same for shared allocations
|
|
|
@@ -1400,12 +1470,13 @@ func computeIdleCoeffs(options *AllocationAggregationOptions, as *AllocationSet,
|
|
|
for _, r := range types {
|
|
|
if coeffs[c][a][r] > 0 && totals[c][r] > 0 {
|
|
|
coeffs[c][a][r] /= totals[c][r]
|
|
|
+ undistributedIdleMap[r] = false
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- return coeffs, nil
|
|
|
+ return coeffs, undistributedIdleMap, nil
|
|
|
}
|
|
|
|
|
|
// getIdleId returns the providerId or cluster of an Allocation depending on the IdleByNode
|