aggregations.go 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. package costmodel
  2. import (
  3. "math"
  4. "sort"
  5. "strconv"
  6. "time"
  7. costAnalyzerCloud "github.com/kubecost/cost-model/cloud"
  8. prometheusClient "github.com/prometheus/client_golang/api"
  9. )
  10. type Aggregation struct {
  11. Aggregator string `json:"aggregation"`
  12. AggregatorSubField string `json:"aggregationSubfield"`
  13. Environment string `json:"environment"`
  14. Cluster string `json:"cluster"`
  15. CPUAllocation []*Vector `json:"-"`
  16. CPUCostVector []*Vector `json:"-"`
  17. RAMAllocation []*Vector `json:"-"`
  18. RAMCostVector []*Vector `json:"-"`
  19. PVCostVector []*Vector `json:"-"`
  20. GPUAllocation []*Vector `json:"-"`
  21. GPUCostVector []*Vector `json:"-"`
  22. CPUCost float64 `json:"cpuCost"`
  23. RAMCost float64 `json:"ramCost"`
  24. GPUCost float64 `json:"gpuCost"`
  25. PVCost float64 `json:"pvCost"`
  26. NetworkCost float64 `json:"networkCost"`
  27. SharedCost float64 `json:"sharedCost"`
  28. TotalCost float64 `json:"totalCost"`
  29. }
  30. type SharedResourceInfo struct {
  31. ShareResources bool
  32. SharedNamespace map[string]bool
  33. LabelSelectors map[string]string
  34. }
  35. func (s *SharedResourceInfo) IsSharedResource(costDatum *CostData) bool {
  36. if _, ok := s.SharedNamespace[costDatum.Namespace]; ok {
  37. return true
  38. }
  39. for labelName, labelValue := range s.LabelSelectors {
  40. if val, ok := costDatum.Labels[labelName]; ok {
  41. if val == labelValue {
  42. return true
  43. }
  44. }
  45. }
  46. return false
  47. }
  48. func NewSharedResourceInfo(shareResources bool, sharedNamespaces []string, labelnames []string, labelvalues []string) *SharedResourceInfo {
  49. sr := &SharedResourceInfo{
  50. ShareResources: shareResources,
  51. SharedNamespace: make(map[string]bool),
  52. LabelSelectors: make(map[string]string),
  53. }
  54. for _, ns := range sharedNamespaces {
  55. sr.SharedNamespace[ns] = true
  56. }
  57. sr.SharedNamespace["kube-system"] = true // kube-system should be split by default
  58. for i := range labelnames {
  59. sr.LabelSelectors[labelnames[i]] = labelvalues[i]
  60. }
  61. return sr
  62. }
  63. func ComputeIdleCoefficient(costData map[string]*CostData, cli prometheusClient.Client, cloud costAnalyzerCloud.Provider, discount float64, windowString, offset string) (float64, error) {
  64. windowDuration, err := time.ParseDuration(windowString)
  65. if err != nil {
  66. return 0.0, err
  67. }
  68. totals, err := ClusterCosts(cli, cloud, windowString, offset)
  69. if err != nil {
  70. return 0.0, err
  71. }
  72. totalClusterCost, err := strconv.ParseFloat(totals.TotalCost[0][1], 64)
  73. if err != nil || totalClusterCost == 0.0 {
  74. return 0.0, err
  75. }
  76. totalClusterCostOverWindow := (totalClusterCost / 730) * windowDuration.Hours() * (1 - discount)
  77. totalContainerCost := 0.0
  78. for _, costDatum := range costData {
  79. cpuv, ramv, gpuv, pvvs := getPriceVectors(costDatum, discount, 1)
  80. totalContainerCost += totalVector(cpuv)
  81. totalContainerCost += totalVector(ramv)
  82. totalContainerCost += totalVector(gpuv)
  83. for _, pv := range pvvs {
  84. totalContainerCost += totalVector(pv)
  85. }
  86. }
  87. return (totalContainerCost / totalClusterCostOverWindow), nil
  88. }
  89. func AggregateCostModel(costData map[string]*CostData, discount float64, idleCoefficient float64, sr *SharedResourceInfo, aggregationField string, aggregationSubField string) map[string]*Aggregation {
  90. aggregations := make(map[string]*Aggregation)
  91. sharedResourceCost := 0.0
  92. for _, costDatum := range costData {
  93. if sr != nil && sr.ShareResources && sr.IsSharedResource(costDatum) {
  94. cpuv, ramv, gpuv, pvvs := getPriceVectors(costDatum, discount, idleCoefficient)
  95. sharedResourceCost += totalVector(cpuv)
  96. sharedResourceCost += totalVector(ramv)
  97. sharedResourceCost += totalVector(gpuv)
  98. for _, pv := range pvvs {
  99. sharedResourceCost += totalVector(pv)
  100. }
  101. } else {
  102. if aggregationField == "cluster" {
  103. aggregationHelper(costDatum, aggregationField, aggregationSubField, costDatum.ClusterID, aggregations, discount, idleCoefficient)
  104. } else if aggregationField == "namespace" {
  105. aggregationHelper(costDatum, aggregationField, aggregationSubField, costDatum.Namespace, aggregations, discount, idleCoefficient)
  106. } else if aggregationField == "service" {
  107. if len(costDatum.Services) > 0 {
  108. aggregationHelper(costDatum, aggregationField, aggregationSubField, costDatum.Services[0], aggregations, discount, idleCoefficient)
  109. }
  110. } else if aggregationField == "deployment" {
  111. if len(costDatum.Deployments) > 0 {
  112. aggregationHelper(costDatum, aggregationField, aggregationSubField, costDatum.Deployments[0], aggregations, discount, idleCoefficient)
  113. }
  114. } else if aggregationField == "label" {
  115. if costDatum.Labels != nil {
  116. if subfieldName, ok := costDatum.Labels[aggregationSubField]; ok {
  117. aggregationHelper(costDatum, aggregationField, aggregationSubField, subfieldName, aggregations, discount, idleCoefficient)
  118. }
  119. }
  120. }
  121. }
  122. }
  123. for _, agg := range aggregations {
  124. agg.CPUCost = totalVector(agg.CPUCostVector)
  125. agg.RAMCost = totalVector(agg.RAMCostVector)
  126. agg.GPUCost = totalVector(agg.GPUCostVector)
  127. agg.PVCost = totalVector(agg.PVCostVector)
  128. agg.SharedCost = sharedResourceCost / float64(len(aggregations))
  129. agg.TotalCost = agg.CPUCost + agg.RAMCost + agg.GPUCost + agg.PVCost + agg.SharedCost
  130. }
  131. return aggregations
  132. }
  133. func aggregationHelper(costDatum *CostData, aggregator string, aggregatorSubField string, key string, aggregations map[string]*Aggregation, discount float64, idleCoefficient float64) {
  134. if _, ok := aggregations[key]; !ok {
  135. agg := &Aggregation{}
  136. agg.Aggregator = aggregator
  137. agg.AggregatorSubField = aggregatorSubField
  138. agg.Environment = key
  139. agg.Cluster = costDatum.ClusterID
  140. aggregations[key] = agg
  141. }
  142. mergeVectors(costDatum, aggregations[key], discount, idleCoefficient)
  143. }
  144. func mergeVectors(costDatum *CostData, aggregation *Aggregation, discount float64, idleCoefficient float64) {
  145. aggregation.CPUAllocation = addVectors(costDatum.CPUAllocation, aggregation.CPUAllocation)
  146. aggregation.RAMAllocation = addVectors(costDatum.RAMAllocation, aggregation.RAMAllocation)
  147. aggregation.GPUAllocation = addVectors(costDatum.GPUReq, aggregation.GPUAllocation)
  148. cpuv, ramv, gpuv, pvvs := getPriceVectors(costDatum, discount, idleCoefficient)
  149. aggregation.CPUCostVector = addVectors(cpuv, aggregation.CPUCostVector)
  150. aggregation.RAMCostVector = addVectors(ramv, aggregation.RAMCostVector)
  151. aggregation.GPUCostVector = addVectors(gpuv, aggregation.GPUCostVector)
  152. for _, vectorList := range pvvs {
  153. aggregation.PVCostVector = addVectors(aggregation.PVCostVector, vectorList)
  154. }
  155. }
  156. func getPriceVectors(costDatum *CostData, discount float64, idleCoefficient float64) ([]*Vector, []*Vector, []*Vector, [][]*Vector) {
  157. cpuv := make([]*Vector, 0, len(costDatum.CPUAllocation))
  158. for _, val := range costDatum.CPUAllocation {
  159. cost, _ := strconv.ParseFloat(costDatum.NodeData.VCPUCost, 64)
  160. cpuv = append(cpuv, &Vector{
  161. Timestamp: math.Round(val.Timestamp/10) * 10,
  162. Value: val.Value * cost * (1 - discount) * 1 / idleCoefficient,
  163. })
  164. }
  165. ramv := make([]*Vector, 0, len(costDatum.RAMAllocation))
  166. for _, val := range costDatum.RAMAllocation {
  167. cost, _ := strconv.ParseFloat(costDatum.NodeData.RAMCost, 64)
  168. ramv = append(ramv, &Vector{
  169. Timestamp: math.Round(val.Timestamp/10) * 10,
  170. Value: (val.Value / 1024 / 1024 / 1024) * cost * (1 - discount) * 1 / idleCoefficient,
  171. })
  172. }
  173. gpuv := make([]*Vector, 0, len(costDatum.GPUReq))
  174. for _, val := range costDatum.GPUReq {
  175. cost, _ := strconv.ParseFloat(costDatum.NodeData.GPUCost, 64)
  176. gpuv = append(gpuv, &Vector{
  177. Timestamp: math.Round(val.Timestamp/10) * 10,
  178. Value: val.Value * cost * (1 - discount) * 1 / idleCoefficient,
  179. })
  180. }
  181. pvvs := make([][]*Vector, 0, len(costDatum.PVCData))
  182. for _, pvcData := range costDatum.PVCData {
  183. pvv := make([]*Vector, 0, len(pvcData.Values))
  184. if pvcData.Volume != nil {
  185. cost, _ := strconv.ParseFloat(pvcData.Volume.Cost, 64)
  186. for _, val := range pvcData.Values {
  187. pvv = append(pvv, &Vector{
  188. Timestamp: math.Round(val.Timestamp/10) * 10,
  189. Value: (val.Value / 1024 / 1024 / 1024) * cost * (1 - discount) * 1 / idleCoefficient,
  190. })
  191. }
  192. pvvs = append(pvvs, pvv)
  193. }
  194. }
  195. return cpuv, ramv, gpuv, pvvs
  196. }
  197. func totalVector(vectors []*Vector) float64 {
  198. total := 0.0
  199. for _, vector := range vectors {
  200. total += vector.Value
  201. }
  202. return total
  203. }
  204. func addVectors(req []*Vector, used []*Vector) []*Vector {
  205. if req == nil || len(req) == 0 {
  206. for _, usedV := range used {
  207. if usedV.Timestamp == 0 {
  208. continue
  209. }
  210. usedV.Timestamp = math.Round(usedV.Timestamp/10) * 10
  211. }
  212. return used
  213. }
  214. if used == nil || len(used) == 0 {
  215. for _, reqV := range req {
  216. if reqV.Timestamp == 0 {
  217. continue
  218. }
  219. reqV.Timestamp = math.Round(reqV.Timestamp/10) * 10
  220. }
  221. return req
  222. }
  223. var allocation []*Vector
  224. var timestamps []float64
  225. reqMap := make(map[float64]float64)
  226. for _, reqV := range req {
  227. if reqV.Timestamp == 0 {
  228. continue
  229. }
  230. reqV.Timestamp = math.Round(reqV.Timestamp/10) * 10
  231. reqMap[reqV.Timestamp] = reqV.Value
  232. timestamps = append(timestamps, reqV.Timestamp)
  233. }
  234. usedMap := make(map[float64]float64)
  235. for _, usedV := range used {
  236. if usedV.Timestamp == 0 {
  237. continue
  238. }
  239. usedV.Timestamp = math.Round(usedV.Timestamp/10) * 10
  240. usedMap[usedV.Timestamp] = usedV.Value
  241. if _, ok := reqMap[usedV.Timestamp]; !ok { // no need to double add, since we'll range over sorted timestamps and check.
  242. timestamps = append(timestamps, usedV.Timestamp)
  243. }
  244. }
  245. sort.Float64s(timestamps)
  246. for _, t := range timestamps {
  247. rv, okR := reqMap[t]
  248. uv, okU := usedMap[t]
  249. allocationVector := &Vector{
  250. Timestamp: t,
  251. }
  252. if okR && okU {
  253. allocationVector.Value = rv + uv
  254. } else if okR {
  255. allocationVector.Value = rv
  256. } else if okU {
  257. allocationVector.Value = uv
  258. }
  259. allocation = append(allocation, allocationVector)
  260. }
  261. return allocation
  262. }