aggregations.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500
  1. package costmodel
  2. import (
  3. "math"
  4. "sort"
  5. "strconv"
  6. "time"
  7. "github.com/kubecost/cost-model/cloud"
  8. prometheusClient "github.com/prometheus/client_golang/api"
  9. "k8s.io/klog"
  10. )
  11. type Aggregation struct {
  12. Aggregator string `json:"aggregation"`
  13. Subfields []string `json:"subfields,omitempty"`
  14. Environment string `json:"environment"`
  15. Cluster string `json:"cluster,omitempty"`
  16. CPUAllocationVectors []*Vector `json:"-"`
  17. CPUCost float64 `json:"cpuCost"`
  18. CPUCostVector []*Vector `json:"cpuCostVector,omitempty"`
  19. CPUEfficiency float64 `json:"cpuEfficiency"`
  20. CPURequestedVectors []*Vector `json:"-"`
  21. CPUUsedVectors []*Vector `json:"-"`
  22. Efficiency float64 `json:"efficiency"`
  23. GPUAllocation []*Vector `json:"-"`
  24. GPUCost float64 `json:"gpuCost"`
  25. GPUCostVector []*Vector `json:"gpuCostVector,omitempty"`
  26. RAMAllocationVectors []*Vector `json:"-"`
  27. RAMCost float64 `json:"ramCost"`
  28. RAMCostVector []*Vector `json:"ramCostVector,omitempty"`
  29. RAMEfficiency float64 `json:"ramEfficiency"`
  30. RAMRequestedVectors []*Vector `json:"-"`
  31. RAMUsedVectors []*Vector `json:"-"`
  32. PVCost float64 `json:"pvCost"`
  33. PVCostVector []*Vector `json:"pvCostVector,omitempty"`
  34. NetworkCost float64 `json:"networkCost"`
  35. NetworkCostVector []*Vector `json:"networkCostVector,omitempty"`
  36. SharedCost float64 `json:"sharedCost"`
  37. TotalCost float64 `json:"totalCost"`
  38. }
  39. const (
  40. hoursPerDay = 24.0
  41. hoursPerMonth = 730.0
  42. )
  43. type SharedResourceInfo struct {
  44. ShareResources bool
  45. SharedNamespace map[string]bool
  46. LabelSelectors map[string]string
  47. }
  48. func (s *SharedResourceInfo) IsSharedResource(costDatum *CostData) bool {
  49. if _, ok := s.SharedNamespace[costDatum.Namespace]; ok {
  50. return true
  51. }
  52. for labelName, labelValue := range s.LabelSelectors {
  53. if val, ok := costDatum.Labels[labelName]; ok {
  54. if val == labelValue {
  55. return true
  56. }
  57. }
  58. }
  59. return false
  60. }
  61. func NewSharedResourceInfo(shareResources bool, sharedNamespaces []string, labelnames []string, labelvalues []string) *SharedResourceInfo {
  62. sr := &SharedResourceInfo{
  63. ShareResources: shareResources,
  64. SharedNamespace: make(map[string]bool),
  65. LabelSelectors: make(map[string]string),
  66. }
  67. for _, ns := range sharedNamespaces {
  68. sr.SharedNamespace[ns] = true
  69. }
  70. sr.SharedNamespace["kube-system"] = true // kube-system should be split by default
  71. for i := range labelnames {
  72. sr.LabelSelectors[labelnames[i]] = labelvalues[i]
  73. }
  74. return sr
  75. }
  76. func ComputeIdleCoefficient(costData map[string]*CostData, cli prometheusClient.Client, cp cloud.Provider, discount float64, windowString, offset string) (float64, error) {
  77. windowDuration, err := time.ParseDuration(windowString)
  78. if err != nil {
  79. return 0.0, err
  80. }
  81. totals, err := ClusterCosts(cli, cp, windowString, offset)
  82. if err != nil {
  83. return 0.0, err
  84. }
  85. cpuCost, err := strconv.ParseFloat(totals.CPUCost[0][1], 64)
  86. if err != nil {
  87. return 0.0, err
  88. }
  89. memCost, err := strconv.ParseFloat(totals.MemCost[0][1], 64)
  90. if err != nil {
  91. return 0.0, err
  92. }
  93. storageCost, err := strconv.ParseFloat(totals.StorageCost[0][1], 64)
  94. if err != nil {
  95. return 0.0, err
  96. }
  97. totalClusterCost := (cpuCost * (1 - discount)) + (memCost * (1 - discount)) + storageCost
  98. if err != nil || totalClusterCost == 0.0 {
  99. return 0.0, err
  100. }
  101. totalClusterCostOverWindow := (totalClusterCost / 730) * windowDuration.Hours()
  102. totalContainerCost := 0.0
  103. for _, costDatum := range costData {
  104. cpuv, ramv, gpuv, pvvs, _ := getPriceVectors(cp, costDatum, "", discount, 1)
  105. totalContainerCost += totalVectors(cpuv)
  106. totalContainerCost += totalVectors(ramv)
  107. totalContainerCost += totalVectors(gpuv)
  108. for _, pv := range pvvs {
  109. totalContainerCost += totalVectors(pv)
  110. }
  111. }
  112. return (totalContainerCost / totalClusterCostOverWindow), nil
  113. }
  114. // AggregationOptions provides optional parameters to AggregateCostData, allowing callers to perform more complex operations
  115. type AggregationOptions struct {
  116. DataCount int64 // number of cost data points expected; ensures proper rate calculation if data is incomplete
  117. Discount float64 // percent by which to discount CPU, RAM, and GPU cost
  118. IdleCoefficient float64 // scales costs by amount of idle resources
  119. IncludeEfficiency bool // set to true to receive efficiency/usage data
  120. IncludeTimeSeries bool // set to true to receive time series data
  121. Rate string // set to "hourly", "daily", or "monthly" to receive cost rate, rather than cumulative cost
  122. SharedResourceInfo *SharedResourceInfo
  123. }
  124. // AggregateCostData aggregates raw cost data by field; e.g. namespace, cluster, service, or label. In the case of label, callers
  125. // must pass a slice of subfields indicating the labels by which to group. Provider is used to define custom resource pricing.
  126. // See AggregationOptions for optional parameters.
  127. // TODO: Can we restructure custom pricing code to allow that to be optional? Having to pass an entire Provider instance is way
  128. // overkill and tightly couples this code to the cloud package.
  129. func AggregateCostData(costData map[string]*CostData, field string, subfields []string, cp cloud.Provider, opts *AggregationOptions) map[string]*Aggregation {
  130. dataCount := opts.DataCount
  131. discount := opts.Discount
  132. idleCoefficient := opts.IdleCoefficient
  133. includeTimeSeries := opts.IncludeTimeSeries
  134. includeEfficiency := opts.IncludeEfficiency
  135. rate := opts.Rate
  136. sr := opts.SharedResourceInfo
  137. // aggregations collects key-value pairs of resource group-to-aggregated data
  138. // e.g. namespace-to-data or label-value-to-data
  139. aggregations := make(map[string]*Aggregation)
  140. // sharedResourceCost is the running total cost of resources that should be reported
  141. // as shared across all other resources, rather than reported as a stand-alone category
  142. sharedResourceCost := 0.0
  143. for _, costDatum := range costData {
  144. if sr != nil && sr.ShareResources && sr.IsSharedResource(costDatum) {
  145. cpuv, ramv, gpuv, pvvs, netv := getPriceVectors(cp, costDatum, rate, discount, idleCoefficient)
  146. sharedResourceCost += totalVectors(cpuv)
  147. sharedResourceCost += totalVectors(ramv)
  148. sharedResourceCost += totalVectors(gpuv)
  149. sharedResourceCost += totalVectors(netv)
  150. for _, pv := range pvvs {
  151. sharedResourceCost += totalVectors(pv)
  152. }
  153. } else {
  154. if field == "cluster" {
  155. aggregateDatum(cp, aggregations, costDatum, field, subfields, rate, costDatum.ClusterID, discount, idleCoefficient)
  156. } else if field == "namespace" {
  157. aggregateDatum(cp, aggregations, costDatum, field, subfields, rate, costDatum.Namespace, discount, idleCoefficient)
  158. } else if field == "service" {
  159. if len(costDatum.Services) > 0 {
  160. aggregateDatum(cp, aggregations, costDatum, field, subfields, rate, costDatum.Services[0], discount, idleCoefficient)
  161. }
  162. } else if field == "deployment" {
  163. if len(costDatum.Deployments) > 0 {
  164. aggregateDatum(cp, aggregations, costDatum, field, subfields, rate, costDatum.Deployments[0], discount, idleCoefficient)
  165. }
  166. } else if field == "label" {
  167. if costDatum.Labels != nil {
  168. for _, sf := range subfields {
  169. if subfieldName, ok := costDatum.Labels[sf]; ok {
  170. aggregateDatum(cp, aggregations, costDatum, field, subfields, rate, subfieldName, discount, idleCoefficient)
  171. break
  172. }
  173. }
  174. }
  175. }
  176. }
  177. }
  178. for _, agg := range aggregations {
  179. agg.CPUCost = totalVectors(agg.CPUCostVector)
  180. agg.RAMCost = totalVectors(agg.RAMCostVector)
  181. agg.GPUCost = totalVectors(agg.GPUCostVector)
  182. agg.PVCost = totalVectors(agg.PVCostVector)
  183. agg.NetworkCost = totalVectors(agg.NetworkCostVector)
  184. agg.SharedCost = sharedResourceCost / float64(len(aggregations))
  185. if rate != "" && dataCount > 0 {
  186. agg.CPUCost /= float64(dataCount)
  187. agg.RAMCost /= float64(dataCount)
  188. agg.GPUCost /= float64(dataCount)
  189. agg.PVCost /= float64(dataCount)
  190. agg.NetworkCost /= float64(dataCount)
  191. agg.SharedCost /= float64(dataCount)
  192. }
  193. agg.TotalCost = agg.CPUCost + agg.RAMCost + agg.GPUCost + agg.PVCost + agg.NetworkCost + agg.SharedCost
  194. if includeEfficiency {
  195. // Default both RAM and CPU to 100% efficiency so that a 0-requested, 0-allocated, 0-used situation
  196. // returns 100% efficiency, which should be a red-flag.
  197. //
  198. // If non-zero numbers are available, then efficiency is defined as:
  199. // idlePercentage = (requested - used) / allocated
  200. // efficiency = (1.0 - idlePercentage)
  201. //
  202. // It is possible to score > 100% efficiency, which is meant to be interpreted as a red flag.
  203. // It is not possible to score < 0% efficiency.
  204. klog.V(1).Infof("\n\tlen(CPU allocation): %d\n\tlen(CPU requested): %d\n\tlen(CPU used): %d",
  205. len(agg.CPUAllocationVectors),
  206. len(agg.CPURequestedVectors),
  207. len(agg.CPUUsedVectors))
  208. agg.CPUEfficiency = 1.0
  209. CPUIdle := 0.0
  210. avgCPUAllocation := totalVectors(agg.CPUAllocationVectors) / float64(len(agg.CPUAllocationVectors))
  211. if avgCPUAllocation > 0.0 {
  212. avgCPURequested := averageVectors(agg.CPURequestedVectors)
  213. avgCPUUsed := averageVectors(agg.CPUUsedVectors)
  214. CPUIdle = ((avgCPURequested - avgCPUUsed) / avgCPUAllocation)
  215. agg.CPUEfficiency = 1.0 - CPUIdle
  216. }
  217. klog.V(1).Infof("\n\tlen(RAM allocation): %d\n\tlen(RAM requested): %d\n\tlen(RAM used): %d",
  218. len(agg.RAMAllocationVectors),
  219. len(agg.RAMRequestedVectors),
  220. len(agg.RAMUsedVectors))
  221. agg.RAMEfficiency = 1.0
  222. RAMIdle := 0.0
  223. avgRAMAllocation := totalVectors(agg.RAMAllocationVectors) / float64(len(agg.RAMAllocationVectors))
  224. if avgRAMAllocation > 0.0 {
  225. avgRAMRequested := averageVectors(agg.RAMRequestedVectors)
  226. avgRAMUsed := averageVectors(agg.RAMUsedVectors)
  227. RAMIdle = ((avgRAMRequested - avgRAMUsed) / avgRAMAllocation)
  228. agg.RAMEfficiency = 1.0 - RAMIdle
  229. }
  230. // Score total efficiency by the sum of CPU and RAM efficiency, weighted by their
  231. // respective total costs.
  232. agg.Efficiency = 1.0
  233. if (agg.CPUCost + agg.RAMCost) > 0 {
  234. agg.Efficiency = 1.0 - ((agg.CPUCost*CPUIdle)+(agg.RAMCost*RAMIdle))/(agg.CPUCost+agg.RAMCost)
  235. }
  236. }
  237. // remove time series data if it is not explicitly requested
  238. if !includeTimeSeries {
  239. agg.CPUCostVector = nil
  240. agg.RAMCostVector = nil
  241. agg.GPUCostVector = nil
  242. agg.PVCostVector = nil
  243. agg.NetworkCostVector = nil
  244. }
  245. }
  246. return aggregations
  247. }
  248. func aggregateDatum(cp cloud.Provider, aggregations map[string]*Aggregation, costDatum *CostData, field string, subfields []string, rate string, key string, discount float64, idleCoefficient float64) {
  249. // add new entry to aggregation results if a new key is encountered
  250. if _, ok := aggregations[key]; !ok {
  251. agg := &Aggregation{}
  252. agg.Aggregator = field
  253. if len(subfields) > 0 {
  254. agg.Subfields = subfields
  255. }
  256. agg.Environment = key
  257. aggregations[key] = agg
  258. }
  259. klog.V(1).Infoln(costDatum)
  260. mergeVectors(cp, costDatum, aggregations[key], rate, discount, idleCoefficient)
  261. }
  262. func mergeVectors(cp cloud.Provider, costDatum *CostData, aggregation *Aggregation, rate string, discount float64, idleCoefficient float64) {
  263. aggregation.CPUAllocationVectors = addVectors(costDatum.CPUAllocation, aggregation.CPUAllocationVectors)
  264. aggregation.CPURequestedVectors = addVectors(costDatum.CPUReq, aggregation.CPURequestedVectors)
  265. aggregation.CPUUsedVectors = addVectors(costDatum.CPUUsed, aggregation.CPUUsedVectors)
  266. aggregation.RAMAllocationVectors = addVectors(costDatum.RAMAllocation, aggregation.RAMAllocationVectors)
  267. aggregation.RAMRequestedVectors = addVectors(costDatum.RAMReq, aggregation.RAMRequestedVectors)
  268. aggregation.RAMUsedVectors = addVectors(costDatum.RAMUsed, aggregation.RAMUsedVectors)
  269. aggregation.GPUAllocation = addVectors(costDatum.GPUReq, aggregation.GPUAllocation)
  270. cpuv, ramv, gpuv, pvvs, netv := getPriceVectors(cp, costDatum, rate, discount, idleCoefficient)
  271. aggregation.CPUCostVector = addVectors(cpuv, aggregation.CPUCostVector)
  272. aggregation.RAMCostVector = addVectors(ramv, aggregation.RAMCostVector)
  273. aggregation.GPUCostVector = addVectors(gpuv, aggregation.GPUCostVector)
  274. aggregation.NetworkCostVector = addVectors(netv, aggregation.NetworkCostVector)
  275. for _, vectorList := range pvvs {
  276. aggregation.PVCostVector = addVectors(aggregation.PVCostVector, vectorList)
  277. }
  278. }
  279. func getPriceVectors(cp cloud.Provider, costDatum *CostData, rate string, discount float64, idleCoefficient float64) ([]*Vector, []*Vector, []*Vector, [][]*Vector, []*Vector) {
  280. cpuCostStr := costDatum.NodeData.VCPUCost
  281. ramCostStr := costDatum.NodeData.RAMCost
  282. gpuCostStr := costDatum.NodeData.GPUCost
  283. pvCostStr := costDatum.NodeData.StorageCost
  284. // If custom pricing is enabled and can be retrieved, replace
  285. // default cost values with custom values
  286. customPricing, err := cp.GetConfig()
  287. if err != nil {
  288. klog.Errorf("failed to load custom pricing: %s", err)
  289. }
  290. if cloud.CustomPricesEnabled(cp) && err == nil {
  291. if costDatum.NodeData.IsSpot() {
  292. cpuCostStr = customPricing.SpotCPU
  293. ramCostStr = customPricing.SpotRAM
  294. gpuCostStr = customPricing.SpotGPU
  295. } else {
  296. cpuCostStr = customPricing.CPU
  297. ramCostStr = customPricing.RAM
  298. gpuCostStr = customPricing.GPU
  299. }
  300. pvCostStr = customPricing.Storage
  301. }
  302. cpuCost, _ := strconv.ParseFloat(cpuCostStr, 64)
  303. ramCost, _ := strconv.ParseFloat(ramCostStr, 64)
  304. gpuCost, _ := strconv.ParseFloat(gpuCostStr, 64)
  305. pvCost, _ := strconv.ParseFloat(pvCostStr, 64)
  306. // rateCoeff scales the individual time series data values by the appropriate
  307. // number. Each value is, by default, the daily value, so the scales convert
  308. // from daily to the target rate.
  309. rateCoeff := 1.0
  310. switch rate {
  311. case "daily":
  312. rateCoeff = hoursPerDay
  313. case "monthly":
  314. rateCoeff = hoursPerMonth
  315. case "hourly":
  316. default:
  317. }
  318. cpuv := make([]*Vector, 0, len(costDatum.CPUAllocation))
  319. for _, val := range costDatum.CPUAllocation {
  320. cpuv = append(cpuv, &Vector{
  321. Timestamp: math.Round(val.Timestamp/10) * 10,
  322. Value: (val.Value * cpuCost * (1 - discount) / idleCoefficient) * rateCoeff,
  323. })
  324. }
  325. ramv := make([]*Vector, 0, len(costDatum.RAMAllocation))
  326. for _, val := range costDatum.RAMAllocation {
  327. ramv = append(ramv, &Vector{
  328. Timestamp: math.Round(val.Timestamp/10) * 10,
  329. Value: ((val.Value / 1024 / 1024 / 1024) * ramCost * (1 - discount) / idleCoefficient) * rateCoeff,
  330. })
  331. }
  332. gpuv := make([]*Vector, 0, len(costDatum.GPUReq))
  333. for _, val := range costDatum.GPUReq {
  334. gpuv = append(gpuv, &Vector{
  335. Timestamp: math.Round(val.Timestamp/10) * 10,
  336. Value: (val.Value * gpuCost * (1 - discount) / idleCoefficient) * rateCoeff,
  337. })
  338. }
  339. pvvs := make([][]*Vector, 0, len(costDatum.PVCData))
  340. for _, pvcData := range costDatum.PVCData {
  341. pvv := make([]*Vector, 0, len(pvcData.Values))
  342. if pvcData.Volume != nil {
  343. cost, _ := strconv.ParseFloat(pvcData.Volume.Cost, 64)
  344. // override with custom pricing if enabled
  345. if cloud.CustomPricesEnabled(cp) {
  346. cost = pvCost
  347. }
  348. for _, val := range pvcData.Values {
  349. pvv = append(pvv, &Vector{
  350. Timestamp: math.Round(val.Timestamp/10) * 10,
  351. Value: ((val.Value / 1024 / 1024 / 1024) * cost / idleCoefficient) * rateCoeff,
  352. })
  353. }
  354. pvvs = append(pvvs, pvv)
  355. }
  356. }
  357. netv := costDatum.NetworkData
  358. return cpuv, ramv, gpuv, pvvs, netv
  359. }
  360. func averageVectors(vectors []*Vector) float64 {
  361. if len(vectors) == 0 {
  362. return 0.0
  363. }
  364. return totalVectors(vectors) / float64(len(vectors))
  365. }
  366. func totalVectors(vectors []*Vector) float64 {
  367. total := 0.0
  368. for _, vector := range vectors {
  369. total += vector.Value
  370. }
  371. return total
  372. }
  373. // roundTimestamp rounds the given timestamp to the given precision; e.g. a
  374. // timestamp given in seconds, rounded to precision 10, will be rounded
  375. // to the nearest value dividible by 10 (24 goes to 20, but 25 goes to 30).
  376. func roundTimestamp(ts float64, precision float64) float64 {
  377. return math.Round(ts/precision) * precision
  378. }
  379. // addVectors adds two slices of Vectors. Vector timestamps are rounded to the
  380. // nearest ten seconds to allow matching of Vectors within a delta allowance.
  381. // Matching Vectors are summed, while unmatched Vectors are passed through.
  382. // e.g. [(t=1, 1), (t=2, 2)] + [(t=2, 2), (t=3, 3)] = [(t=1, 1), (t=2, 4), (t=3, 3)]
  383. func addVectors(xvs []*Vector, yvs []*Vector) []*Vector {
  384. // round all non-zero timestamps to the nearest 10 second mark
  385. for _, yv := range yvs {
  386. if yv.Timestamp != 0 {
  387. yv.Timestamp = roundTimestamp(yv.Timestamp, 10.0)
  388. }
  389. }
  390. for _, xv := range xvs {
  391. if xv.Timestamp != 0 {
  392. xv.Timestamp = roundTimestamp(xv.Timestamp, 10.0)
  393. }
  394. }
  395. // if xvs is empty, return yvs
  396. if xvs == nil || len(xvs) == 0 {
  397. return yvs
  398. }
  399. // if yvs is empty, return xvs
  400. if yvs == nil || len(yvs) == 0 {
  401. return xvs
  402. }
  403. // sum stores the sum of the vector slices xvs and yvs
  404. var sum []*Vector
  405. // timestamps stores all timestamps present in both vector slices
  406. // without duplicates
  407. var timestamps []float64
  408. // turn each vector slice into a map of timestamp-to-value so that
  409. // values at equal timestamps can be lined-up and summed
  410. xMap := make(map[float64]float64)
  411. for _, xv := range xvs {
  412. if xv.Timestamp == 0 {
  413. continue
  414. }
  415. xMap[xv.Timestamp] = xv.Value
  416. timestamps = append(timestamps, xv.Timestamp)
  417. }
  418. yMap := make(map[float64]float64)
  419. for _, yv := range yvs {
  420. if yv.Timestamp == 0 {
  421. continue
  422. }
  423. yMap[yv.Timestamp] = yv.Value
  424. if _, ok := xMap[yv.Timestamp]; !ok {
  425. // no need to double add, since we'll range over sorted timestamps and check.
  426. timestamps = append(timestamps, yv.Timestamp)
  427. }
  428. }
  429. // iterate over each timestamp to produce a final summed vector slice
  430. sort.Float64s(timestamps)
  431. for _, t := range timestamps {
  432. x, okX := xMap[t]
  433. y, okY := yMap[t]
  434. sv := &Vector{Timestamp: t}
  435. if okX && okY {
  436. sv.Value = x + y
  437. } else if okX {
  438. sv.Value = x
  439. } else if okY {
  440. sv.Value = y
  441. }
  442. sum = append(sum, sv)
  443. }
  444. return sum
  445. }