Procházet zdrojové kódy

PV allocation reconciliation

Sean Holcomb před 5 roky
rodič
revize
5b12d4ab7f

+ 9 - 1
pkg/costmodel/allocation.go

@@ -361,7 +361,6 @@ func (cm *CostModel) ComputeAllocation(start, end time.Time, resolution time.Dur
 			alloc.CPUCost = alloc.CPUCoreHours * node.CostPerCPUHr
 			alloc.RAMCost = (alloc.RAMByteHours / 1024 / 1024 / 1024) * node.CostPerRAMGiBHr
 			alloc.GPUCost = alloc.GPUHours * node.CostPerGPUHr
-
 			if pvcs, ok := podPVCMap[podKey]; ok {
 				for _, pvc := range pvcs {
 					// Determine the (start, end) of the relationship between the
@@ -389,6 +388,15 @@ func (cm *CostModel) ComputeAllocation(start, end time.Time, resolution time.Dur
 					// weighted by count (i.e. the number of containers in the pod)
 					alloc.PVByteHours += pvc.Bytes * hrs / count
 					alloc.PVCost += cost / count
+
+					// record the amount of total PVBytes Hours attributable to a given PV
+					if alloc.Properties.PVBreakDown == nil {
+						alloc.Properties.PVBreakDown = map[string]kubecost.PVUsage{}
+					}
+					alloc.Properties.PVBreakDown[pvc.Volume.Name] = kubecost.PVUsage{
+						ByteHours: pvc.Bytes * hrs / count,
+						Cost:      cost / count,
+					}
 				}
 			}
 

+ 108 - 58
pkg/kubecost/allocation.go

@@ -67,6 +67,7 @@ type Allocation struct {
 	LoadBalancerCost       float64               `json:"loadBalancerCost"`
 	PVByteHours            float64               `json:"pvByteHours"`
 	PVCost                 float64               `json:"pvCost"`
+	PVAdjustment           float64               `json:"pvAdjustment"`
 	RAMByteHours           float64               `json:"ramByteHours"`
 	RAMBytesRequestAverage float64               `json:"ramByteRequestAverage"`
 	RAMBytesUsageAverage   float64               `json:"ramByteUsageAverage"`
@@ -74,7 +75,6 @@ type Allocation struct {
 	RAMAdjustment          float64               `json:"ramAdjustment"`
 	SharedCost             float64               `json:"sharedCost"`
 	ExternalCost           float64               `json:"externalCost"`
-
 	// RawAllocationOnly is a pointer so if it is not present it will be
 	// marshalled as null rather than as an object with Go default values.
 	RawAllocationOnly *RawAllocationOnlyData `json:"rawAllocationOnly"`
@@ -155,6 +155,7 @@ func (a *Allocation) Clone() *Allocation {
 		LoadBalancerCost:       a.LoadBalancerCost,
 		PVByteHours:            a.PVByteHours,
 		PVCost:                 a.PVCost,
+		PVAdjustment:           a.PVAdjustment,
 		RAMByteHours:           a.RAMByteHours,
 		RAMBytesRequestAverage: a.RAMBytesRequestAverage,
 		RAMBytesUsageAverage:   a.RAMBytesUsageAverage,
@@ -232,6 +233,9 @@ func (a *Allocation) Equal(that *Allocation) bool {
 	if !util.IsApproximately(a.PVCost, that.PVCost) {
 		return false
 	}
+	if !util.IsApproximately(a.PVAdjustment, that.PVAdjustment) {
+		return false
+	}
 	if !util.IsApproximately(a.RAMByteHours, that.RAMByteHours) {
 		return false
 	}
@@ -269,7 +273,7 @@ func (a *Allocation) Equal(that *Allocation) bool {
 
 // TotalCost is the total cost of the Allocation
 func (a *Allocation) TotalCost() float64 {
-	return a.CPUTotalCost() + a.GPUTotalCost() + a.RAMTotalCost() + a.PVCost + a.NetworkCost + a.SharedCost + a.ExternalCost + a.LoadBalancerCost
+	return a.CPUTotalCost() + a.GPUTotalCost() + a.RAMTotalCost() + a.PVTotalCost() + a.NetworkCost + a.SharedCost + a.ExternalCost + a.LoadBalancerCost
 }
 
 func (a *Allocation) CPUTotalCost() float64 {
@@ -284,6 +288,10 @@ func (a *Allocation) RAMTotalCost() float64 {
 	return a.RAMCost + a.RAMAdjustment
 }
 
+func (a *Allocation) PVTotalCost() float64 {
+	return a.PVCost + a.PVAdjustment
+}
+
 // CPUEfficiency is the ratio of usage to request. If there is no request and
 // no usage or cost, then efficiency is zero. If there is no request, but there
 // is usage or cost, then efficiency is 100%.
@@ -374,6 +382,7 @@ func (a *Allocation) MarshalJSON() ([]byte, error) {
 	jsonEncodeFloat64(buffer, "pvBytes", a.PVBytes(), ",")
 	jsonEncodeFloat64(buffer, "pvByteHours", a.PVByteHours, ",")
 	jsonEncodeFloat64(buffer, "pvCost", a.PVCost, ",")
+	jsonEncodeFloat64(buffer, "pvAdjustment", a.PVAdjustment, ",")
 	jsonEncodeFloat64(buffer, "ramBytes", a.RAMBytes(), ",")
 	jsonEncodeFloat64(buffer, "ramByteRequestAverage", a.RAMBytesRequestAverage, ",")
 	jsonEncodeFloat64(buffer, "ramByteUsageAverage", a.RAMBytesUsageAverage, ",")
@@ -513,6 +522,7 @@ func (a *Allocation) add(that *Allocation) {
 	a.CPUAdjustment += that.CPUAdjustment
 	a.RAMAdjustment += that.RAMAdjustment
 	a.GPUAdjustment += that.GPUAdjustment
+	a.PVAdjustment += that.PVAdjustment
 
 	// Any data that is in a "raw allocation only" is not valid in any
 	// sort of cumulative Allocation (like one that is added).
@@ -1179,7 +1189,7 @@ func computeIdleCoeffs(options *AllocationAggregationOptions, as *AllocationSet,
 		} else {
 			coeffs[clusterID][name]["cpu"] += alloc.CPUTotalCost()
 			coeffs[clusterID][name]["gpu"] += alloc.GPUTotalCost()
-			coeffs[clusterID][name]["ram"] += alloc.RAMCost
+			coeffs[clusterID][name]["ram"] += alloc.RAMTotalCost()
 
 			totals[clusterID]["cpu"] += alloc.CPUTotalCost()
 			totals[clusterID]["gpu"] += alloc.GPUTotalCost()
@@ -1513,77 +1523,117 @@ func (as *AllocationSet) ReconcileAllocations(assetSet *AssetSet) error {
 	// Build map of Assets with type Node by their ProviderId so that they can be matched to Allocations to determine
 	// proper CPU GPU and Ram prices
 	nodeByProviderID := map[string]*Node{}
+	diskByName := map[string]*Disk{}
 	assetSet.Each(func(key string, a Asset) {
 		if node, ok := a.(*Node); ok {
 			nodeByProviderID[node.properties.ProviderID] = node
 		}
+		if disk, ok := a.(*Disk); ok {
+			diskByName[disk.properties.Name] = disk
+		}
 	})
 
 	// Match Assets against allocations and adjust allocation cost based on the proportion of the asset that they used
 	as.Each(func(name string, a *Allocation) {
-		providerId := a.Properties.ProviderID
+		a.reconcileNodes(nodeByProviderID)
+		a.reconcileDisks(diskByName)
+	})
+
+	return nil
+}
+
+func (a *Allocation) reconcileNodes(nodeByProviderID map[string]*Node) {
+	providerId := a.Properties.ProviderID
+
+	// Reconcile with node Assets
+	node, ok := nodeByProviderID[providerId]
+	if !ok {
+		// Failed to find node for allocation
+		return
+	}
+
+	// adjustmentRate is used to scale resource costs proportionally
+	// by the adjustment. This is necessary because we only get one
+	// adjustment per Node, not one per-resource-per-Node.
+	//
+	// e.g. total cost = $90, adjustment = -$10 => 0.9
+	// e.g. total cost = $150, adjustment = -$300 => 0.3333
+	// e.g. total cost = $150, adjustment = $50 => 1.5
+	adjustmentRate := 1.0
+	if node.TotalCost()-node.Adjustment() == 0 {
+		// If (totalCost - adjustment) is 0.0 then adjustment cancels
+		// the entire node cost and we should make everything 0
+		// without dividing by 0.
+		adjustmentRate = 0.0
+	} else if node.Adjustment() != 0.0 {
+		// adjustmentRate is the ratio of cost-with-adjustment (i.e. TotalCost)
+		// to cost-without-adjustment (i.e. TotalCost - Adjustment).
+		adjustmentRate = node.TotalCost() / (node.TotalCost() - node.Adjustment())
+	}
+
+	// Find total cost of each node resource for the window
+	cpuCost := node.CPUCost * (1.0 - node.Discount) * adjustmentRate
+	ramCost := node.RAMCost * (1.0 - node.Discount) * adjustmentRate
+	gpuCost := node.GPUCost * adjustmentRate
+
+	// Find the proportion of resource hours used by the allocation, checking for 0 denominators
+	cpuUsageProportion := 0.0
+	if node.CPUCoreHours != 0 {
+		cpuUsageProportion = a.CPUCoreHours / node.CPUCoreHours
+	} else {
+		log.Warningf("Missing CPU Hours for node Provider ID: %s", providerId)
+	}
+	ramUsageProportion := 0.0
+	if node.RAMByteHours != 0 {
+		ramUsageProportion = a.RAMByteHours / node.RAMByteHours
+	} else {
+		log.Warningf("Missing Ram Byte Hours for node Provider ID: %s", providerId)
+	}
+	gpuUsageProportion := 0.0
+	if node.GPUCount != 0 && node.Minutes() != 0 {
+		gpuUsageProportion = a.GPUHours / (node.GPUCount * node.Minutes() / 60)
+	}
+	// No log for GPU because not all nodes have GPU
+
+	// Calculate the allocation's resource costs by the proportion of resources used and total costs
+	allocCPUCost := cpuUsageProportion * cpuCost
+	allocRAMCost := ramUsageProportion * ramCost
+	allocGPUCost := gpuUsageProportion * gpuCost
 
-		// Reconcile with node Assets
-		node, ok := nodeByProviderID[providerId]
+	a.CPUAdjustment = allocCPUCost - a.CPUCost
+	a.RAMAdjustment = allocRAMCost - a.RAMCost
+	a.GPUAdjustment = allocGPUCost - a.GPUCost
+}
+
+func (a *Allocation) reconcileDisks(diskByName map[string]*Disk) {
+	pvBreakDown := a.Properties.PVBreakDown
+	if pvBreakDown == nil {
+		// No PV usage to reconcile
+		return
+	}
+	// Set PV Adjustment for allocation to 0 for idempotency
+	a.PVAdjustment = 0.0
+	for pvName, pvUsage := range pvBreakDown {
+		disk, ok := diskByName[pvName]
 		if !ok {
-			// Failed to find node for allocation
-			return
+			// Failed to find disk in assets
+			continue
 		}
 
-		// adjustmentRate is used to scale resource costs proportionally
-		// by the adjustment. This is necessary because we only get one
-		// adjustment per Node, not one per-resource-per-Node.
-		//
-		// e.g. total cost = $90, adjustment = -$10 => 0.9
-		// e.g. total cost = $150, adjustment = -$300 => 0.3333
-		// e.g. total cost = $150, adjustment = $50 => 1.5
-		adjustmentRate := 1.0
-		if node.TotalCost()-node.Adjustment() == 0 {
-			// If (totalCost - adjustment) is 0.0 then adjustment cancels
-			// the entire node cost and we should make everything 0
-			// without dividing by 0.
-			adjustmentRate = 0.0
-		} else if node.Adjustment() != 0.0 {
-			// adjustmentRate is the ratio of cost-with-adjustment (i.e. TotalCost)
-			// to cost-without-adjustment (i.e. TotalCost - Adjustment).
-			adjustmentRate = node.TotalCost() / (node.TotalCost() - node.Adjustment())
-		}
-
-		// Find total cost of each node resource for the window
-		cpuCost := node.CPUCost * (1.0 - node.Discount) * adjustmentRate
-		ramCost := node.RAMCost * (1.0 - node.Discount) * adjustmentRate
-		gpuCost := node.GPUCost * adjustmentRate
-
-		// Find the proportion of resource hours used by the allocation, checking for 0 denominators
-		cpuUsageProportion := 0.0
-		if node.CPUCoreHours != 0 {
-			cpuUsageProportion = a.CPUCoreHours / node.CPUCoreHours
+		// Check the proportion of disk that is being used by
+		pvUsageProportion := 0.0
+		if disk.ByteHours != 0 {
+			pvUsageProportion = pvUsage.ByteHours / disk.ByteHours
 		} else {
-			log.Warningf("Missing CPU Hours for node Provider ID: %s", providerId)
-		}
-		ramUsageProportion := 0.0
-		if node.RAMByteHours != 0 {
-			ramUsageProportion = a.RAMByteHours / node.RAMByteHours
-		} else {
-			log.Warningf("Missing Ram Byte Hours for node Provider ID: %s", providerId)
-		}
-		gpuUsageProportion := 0.0
-		if node.GPUCount != 0 && node.Minutes() != 0 {
-			gpuUsageProportion = a.GPUHours / (node.GPUCount * node.Minutes() / 60)
+			log.Warningf("Missing Byte Hours for disk: %s", pvName)
 		}
-		// No log for GPU because not all nodes have GPU
 
-		// Calculate the allocation's resource costs by the proportion of resources used and total costs
-		allocCPUCost := cpuUsageProportion * cpuCost
-		allocRAMCost := ramUsageProportion * ramCost
-		allocGPUCost := gpuUsageProportion * gpuCost
+		// take proportion of disk adjusted cost
+		allocPVCost := pvUsageProportion * disk.TotalCost()
 
-		a.CPUAdjustment = allocCPUCost - a.CPUCost
-		a.RAMAdjustment = allocRAMCost - a.RAMCost
-		a.GPUAdjustment = allocGPUCost - a.GPUCost
-	})
-
-	return nil
+		// PVAdjustment is cumulative as there can be many PVs for each Allocation
+		a.PVAdjustment += allocPVCost - pvUsage.Cost
+	}
 }
 
 // Delete removes the allocation with the given name from the set

+ 32 - 4
pkg/kubecost/allocation_test.go

@@ -118,6 +118,7 @@ func TestAllocation_Add(t *testing.T) {
 		GPUAdjustment:          2.0,
 		PVByteHours:            100.0 * gib * hrs1,
 		PVCost:                 100.0 * hrs1 * pvPrice,
+		PVAdjustment:           4.0,
 		RAMByteHours:           8.0 * gib * hrs1,
 		RAMBytesRequestAverage: 8.0 * gib,
 		RAMBytesUsageAverage:   4.0 * gib,
@@ -287,6 +288,7 @@ func TestAllocation_Share(t *testing.T) {
 		GPUAdjustment:          2.0,
 		PVByteHours:            100.0 * gib * hrs1,
 		PVCost:                 100.0 * hrs1 * pvPrice,
+		PVAdjustment:           4.0,
 		RAMByteHours:           8.0 * gib * hrs1,
 		RAMBytesRequestAverage: 8.0 * gib,
 		RAMBytesUsageAverage:   4.0 * gib,
@@ -354,8 +356,8 @@ func TestAllocation_Share(t *testing.T) {
 	if !util.IsApproximately(a1.RAMTotalCost(), act.RAMTotalCost()) {
 		t.Fatalf("Allocation.Share: expected %f; actual %f", a1.RAMTotalCost(), act.RAMTotalCost())
 	}
-	if !util.IsApproximately(a1.PVCost, act.PVCost) {
-		t.Fatalf("Allocation.Share: expected %f; actual %f", a1.PVCost, act.PVCost)
+	if !util.IsApproximately(a1.PVTotalCost(), act.PVTotalCost()) {
+		t.Fatalf("Allocation.Share: expected %f; actual %f", a1.PVTotalCost(), act.PVTotalCost())
 	}
 	if !util.IsApproximately(a1.NetworkCost, act.NetworkCost) {
 		t.Fatalf("Allocation.Share: expected %f; actual %f", a1.NetworkCost, act.NetworkCost)
@@ -448,6 +450,7 @@ func TestAllocation_MarshalJSON(t *testing.T) {
 		LoadBalancerCost:       0.02,
 		PVByteHours:            100.0 * gib * hrs,
 		PVCost:                 100.0 * hrs * pvPrice,
+		PVAdjustment:           4.0,
 		RAMByteHours:           8.0 * gib * hrs,
 		RAMBytesRequestAverage: 8.0 * gib,
 		RAMBytesUsageAverage:   4.0 * gib,
@@ -697,6 +700,10 @@ func generateAllocationSet(start time.Time) *AllocationSet {
 	a12jkl6.Properties.Services = []string{"service1"}
 	a22pqr6.Properties.Services = []string{"service1"}
 
+	// PV BreakDown
+	a22mno4.Properties.PVBreakDown = map[string]PVUsage{"disk1": {Cost: 2.5, ByteHours: 2.5 * gb}, "disk2": {Cost: 5, ByteHours: 5 * gb}}
+	a22mno5.Properties.PVBreakDown = map[string]PVUsage{"disk1": {Cost: 2.5, ByteHours: 2.5 * gb}, "disk2": {Cost: 5, ByteHours: 5 * gb}}
+
 	return NewAllocationSet(start, start.Add(day),
 		// idle
 		a1i, a2i,
@@ -782,8 +789,15 @@ func generateAssetSets(start, end time.Time) []*AssetSet {
 
 	cluster2Disk1 := NewDisk("disk1", "cluster2", "disk1", start, end, NewWindow(&start, &end))
 	cluster2Disk1.Cost = 5.0
+	cluster2Disk1.adjustment = 1.0
+	cluster2Disk1.ByteHours = 5 * gb
+
+	cluster2Disk2 := NewDisk("disk2", "cluster2", "disk2", start, end, NewWindow(&start, &end))
+	cluster2Disk2.Cost = 10.0
+	cluster2Disk2.adjustment = 3.0
+	cluster2Disk2.ByteHours = 10 * gb
 
-	assetSet1 := NewAssetSet(start, end, cluster1Nodes, cluster2Node1, cluster2Node2, cluster2Node3, cluster2Disk1)
+	assetSet1 := NewAssetSet(start, end, cluster1Nodes, cluster2Node1, cluster2Node2, cluster2Node3, cluster2Disk1, cluster2Disk2)
 	assetSets = append(assetSets, assetSet1)
 
 	// NOTE: we're re-using generateAllocationSet so this has to line up with
@@ -848,8 +862,15 @@ func generateAssetSets(start, end time.Time) []*AssetSet {
 
 	cluster2Disk1 = NewDisk("disk1", "cluster2", "disk1", start, end, NewWindow(&start, &end))
 	cluster2Disk1.Cost = 5.0
+	cluster2Disk1.adjustment = 1.0
+	cluster2Disk1.ByteHours = 5 * gb
 
-	assetSet2 := NewAssetSet(start, end, cluster1Nodes, cluster2Node1, cluster2Node2, cluster2Node3, cluster2Disk1)
+	cluster2Disk2 = NewDisk("disk2", "cluster2", "disk2", start, end, NewWindow(&start, &end))
+	cluster2Disk2.Cost = 12.0
+	cluster2Disk2.adjustment = 4.0
+	cluster2Disk2.ByteHours = 20 * gb
+
+	assetSet2 := NewAssetSet(start, end, cluster1Nodes, cluster2Node1, cluster2Node2, cluster2Node3, cluster2Disk1, cluster2Disk2)
 	assetSets = append(assetSets, assetSet2)
 	return assetSets
 }
@@ -1814,11 +1835,13 @@ func TestAllocationSet_ReconcileAllocations(t *testing.T) {
 					CPUAdjustment: 4.0,
 					RAMAdjustment: 4.0,
 					GPUAdjustment: -1.0,
+					PVAdjustment:  2.0,
 				},
 				"cluster2/namespace2/pod-mno/container5": {
 					CPUAdjustment: 4.0,
 					RAMAdjustment: 4.0,
 					GPUAdjustment: -1.0,
+					PVAdjustment:  2.0,
 				},
 				// ADJUSTMENT_RATE: 1.0
 				// Type | NODE_COST | NODE_HOURs | ALLOC_COST | ALLOC_HOURS
@@ -1905,11 +1928,13 @@ func TestAllocationSet_ReconcileAllocations(t *testing.T) {
 					CPUAdjustment: 4.0,
 					RAMAdjustment: 4.0,
 					GPUAdjustment: -1.0,
+					PVAdjustment:  -0.5,
 				},
 				"cluster2/namespace2/pod-mno/container5": {
 					CPUAdjustment: 4.0,
 					RAMAdjustment: 4.0,
 					GPUAdjustment: -1.0,
+					PVAdjustment:  -0.5,
 				},
 				// ADJUSTMENT_RATE: 1.0
 				// Type | NODE_COST | NODE_HOURs | ALLOC_COST | ALLOC_HOURS
@@ -1967,6 +1992,9 @@ func TestAllocationSet_ReconcileAllocations(t *testing.T) {
 				if !util.IsApproximately(reconAllocs[allocationName].GPUAdjustment, testAlloc.GPUAdjustment) {
 					t.Fatalf("expected GPU Adjustment for %s to be %f; got %f", allocationName, testAlloc.GPUAdjustment, reconAllocs[allocationName].GPUAdjustment)
 				}
+				if !util.IsApproximately(reconAllocs[allocationName].PVAdjustment, testAlloc.PVAdjustment) {
+					t.Fatalf("expected PV Adjustment for %s to be %f; got %f", allocationName, testAlloc.PVAdjustment, reconAllocs[allocationName].PVAdjustment)
+				}
 			}
 		})
 	}

+ 8 - 0
pkg/kubecost/allocationprops.go

@@ -74,6 +74,7 @@ type AllocationProperties struct {
 	ProviderID     string                `json:"providerID,omitempty"`
 	Labels         AllocationLabels      `json:"allocationLabels,omitempty"`
 	Annotations    AllocationAnnotations `json:"allocationAnnotations,omitempty"`
+	PVBreakDown    map[string]PVUsage    `json:"pvBreakDown,omitempty"`
 }
 
 // AllocationLabels is a schema-free mapping of key/value pairs that can be
@@ -84,6 +85,13 @@ type AllocationLabels map[string]string
 // attributed to an Allocation
 type AllocationAnnotations map[string]string
 
+// PVUsage is a mapping between the name of the PV and the byte hour usage
+// and cost of an Allocation
+type PVUsage struct {
+	ByteHours float64
+	Cost      float64
+}
+
 func (p *AllocationProperties) Clone() *AllocationProperties {
 	if p == nil {
 		return nil