coster

package
v0.0.0-...-bea9c9c Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 12, 2020 License: Apache-2.0 Imports: 25 Imported by: 0

Documentation

Index

Constants

View Source
const (
	// StrategyNameCPU is used whenever we derive a cost metric using the CPUPricingStrategy.
	StrategyNameCPU = "CPUPricingStrategy"
	// StrategyNameMemory is used whenever we derive a cost metric using the MemoryPricingStrategy.
	StrategyNameMemory = "MemoryPricingStrategy"
	// StrategyNameNode is used whenever we derive a cost metric using the NodePricingStrategy.
	StrategyNameNode = "NodePricingStrategy"
	// StrategyNameWeighted is used whenever we derive a cost metric using the WeightedPricingStrategy.
	StrategyNameWeighted = "WeightedPricingStrategy"
	// StrategyNameGPU is used whenever we derive a cost metric using the GPUPricingStrategy.
	StrategyNameGPU = "GPUPricingStrategy"
	// ResourceGPU is used for gpu resources, coinciding with modern versions of the nvidia-device-plugin.
	ResourceGPU = core_v1.ResourceName("nvidia.com/gpu")
)

Variables

View Source
var (
	// ResourceCostCPU is a cost metric derived from CPU utilization.
	ResourceCostCPU = ResourceCostKind("cpu")
	// ResourceCostMemory is a cost metric derived from memory utilization.
	ResourceCostMemory = ResourceCostKind("memory")
	// ResourceCostGPU is a cost metric derived from GPU utilization. At the present
	// time kostanza assumes all GPU's in your cluster are homogenous.
	ResourceCostGPU = ResourceCostKind("gpu")
	// ResourceCostWeighted is a cost metric derived from a weighted average of memory and cpu utilization.
	ResourceCostWeighted = ResourceCostKind("weighted")
	// ResourceCostNode represents the overall cost of a node.
	ResourceCostNode = ResourceCostKind("node")
	// TagStatus indicates the success or failure of an operation.
	TagStatus, _ = tag.NewKey("status")
)
View Source
var (
	// ErrNoPodNode may be returned during races where the pod containing a given
	// node has disappeared.
	ErrNoPodNode = errors.New("could not lookup node for pod")
	// ErrSenselessInterval is returned if the difference since our last run time
	// is less than 0. Obviously, this should never since time moves forward.
	ErrSenselessInterval = errors.New("senseless interval since last calculation")
)
View Source
var (
	// MeasureCost is the stat for tracking costs in millionths of a cent.
	MeasureCost = stats.Int64("kostanza/measures/cost", "Cost in millionths of a cent", "µ¢")
	// MeasureCycles is the number of tracking loops conducted.
	MeasureCycles = stats.Int64("kostanza/measures/cycles", "Iterations executed", stats.UnitDimensionless)
	// MeasureLag is the discrepancy between the ideal interval and actual interval between calculations.
	MeasureLag = stats.Float64("kostanza/measures/lag", "Lag time in calculation intervals", stats.UnitMilliseconds)
)
View Source
var CPUPricingStrategy = PricingStrategyFunc(func(table CostTable, duration time.Duration, pods []*core_v1.Pod, nodes []*core_v1.Node) []CostItem {
	nm := buildNodeMap(nodes)
	cis := []CostItem{}
	for _, p := range pods {
		cpu := sumPodResource(p, core_v1.ResourceCPU)
		node, ok := nm[p.Spec.NodeName]
		if !ok {
			log.Log.Warnw("could not find nodeResourceMap for node", zap.String("nodeName", p.Spec.NodeName))
			continue
		}

		te, err := table.FindByLabels(node.Labels)
		if err != nil {
			log.Log.Warnw("could not find pricing entry for node", zap.String("nodeName", node.ObjectMeta.Name))
			continue
		}

		ci := CostItem{
			Kind:     ResourceCostCPU,
			Value:    te.CPUCostMicroCents(float64(cpu), duration),
			Pod:      p,
			Node:     node,
			Strategy: StrategyNameCPU,
		}
		log.Log.Debugw(
			"generated cost item",
			zap.String("pod", ci.Pod.ObjectMeta.Name),
			zap.String("strategy", ci.Strategy),
			zap.Int64("value", ci.Value),
		)
		cis = append(cis, ci)
	}
	return cis
})

CPUPricingStrategy calculates the cost of a pod based strictly on it's share of CPU requests as a fraction of all CPU available on the node onto which it is allocated.

View Source
var (
	// ErrNoCostEntry is returned when we cannot find a suitable CostEntry in a CostTable.
	ErrNoCostEntry = errors.New("could not find an appropriate cost entry")
)
View Source
var GPUPricingStrategy = PricingStrategyFunc(func(table CostTable, duration time.Duration, pods []*core_v1.Pod, nodes []*core_v1.Node) []CostItem {
	nm := buildNodeMap(nodes)
	cis := []CostItem{}
	for _, p := range pods {
		gpu := sumPodResource(p, ResourceGPU)
		node, ok := nm[p.Spec.NodeName]

		if gpu == 0 {
			log.Log.Debugw("skipping pod that does not utilize gpu", zap.String("pod", p.ObjectMeta.Name))
			continue
		}

		if !ok {
			log.Log.Warnw("could not find nodeResourceMap for node", zap.String("nodeName", p.Spec.NodeName))
			continue
		}

		te, err := table.FindByLabels(node.Labels)
		if err != nil {
			log.Log.Warnw("could not find pricing entry for node", zap.String("nodeName", node.ObjectMeta.Name))
			continue
		}

		ci := CostItem{
			Kind:     ResourceCostGPU,
			Value:    te.GPUCostMicroCents(float64(gpu), duration),
			Pod:      p,
			Node:     node,
			Strategy: StrategyNameGPU,
		}
		log.Log.Debugw(
			"generated cost item",
			zap.String("pod", ci.Pod.ObjectMeta.Name),
			zap.String("strategy", ci.Strategy),
			zap.Int64("value", ci.Value),
		)
		cis = append(cis, ci)
	}
	return cis
})

GPUPricingStrategy generates cost metrics that account for the cost of GPUs consumed by pods.

View Source
var (
	// MeasurePubsubPublishErrors tracks publishing errors in the PubsubCostExporter.
	MeasurePubsubPublishErrors = stats.Int64("kostanza/measures/pubsub_errors", "Number of pubsub publish error", stats.UnitDimensionless)
)
View Source
var MemoryPricingStrategy = PricingStrategyFunc(func(table CostTable, duration time.Duration, pods []*core_v1.Pod, nodes []*core_v1.Node) []CostItem {
	nm := buildNodeMap(nodes)
	cis := []CostItem{}
	for _, p := range pods {
		mem := sumPodResource(p, core_v1.ResourceMemory)
		node, ok := nm[p.Spec.NodeName]
		if !ok {
			log.Log.Warnw("could not find nodeResourceMap for node", zap.String("nodeName", p.Spec.NodeName))
			continue
		}

		te, err := table.FindByLabels(node.Labels)
		if err != nil {
			log.Log.Warnw("could not find pricing entry for node", zap.String("nodeName", node.ObjectMeta.Name))
			continue
		}

		ci := CostItem{
			Kind:     ResourceCostMemory,
			Value:    te.MemoryCostMicroCents(float64(mem), duration),
			Pod:      p,
			Node:     node,
			Strategy: StrategyNameMemory,
		}
		log.Log.Debugw(
			"generated cost item",
			zap.String("pod", ci.Pod.ObjectMeta.Name),
			zap.String("strategy", ci.Strategy),
			zap.Int64("value", ci.Value),
		)
		cis = append(cis, ci)
	}
	return cis
})

MemoryPricingStrategy calculates the cost of a pod based strictly on it's share of memory requests as a fraction of all memory on the node onto which it was scheduled.

View Source
var NodePricingStrategy = PricingStrategyFunc(func(table CostTable, duration time.Duration, pods []*core_v1.Pod, nodes []*core_v1.Node) []CostItem {
	cis := []CostItem{}
	for _, n := range nodes {
		te, err := table.FindByLabels(n.Labels)
		if err != nil {
			log.Log.Warnw("could not find pricing entry for node", zap.String("nodeName", n.ObjectMeta.Name))
			continue
		}

		c := n.Status.Capacity.Cpu()
		if c == nil {
			log.Log.Warnw("could not get node cpu capacity, skipping", zap.String("nodeName", n.ObjectMeta.Name))
			continue
		}

		m := n.Status.Capacity.Memory()
		if m == nil {
			log.Log.Warnw("could not get node memory capacity, skipping", zap.String("nodeName", n.ObjectMeta.Name))
			continue
		}

		memcost := te.MemoryCostMicroCents(float64(m.MilliValue())/1000, duration)
		cpucost := te.CPUCostMicroCents(float64(c.MilliValue()), duration)

		gpucost := int64(0)
		if g := gpuCapacity(&n.Status.Capacity); g != nil {
			gpucost = te.GPUCostMicroCents(float64(g.Value()), duration)
		}

		ci := CostItem{
			Kind:     ResourceCostNode,
			Value:    memcost + cpucost + gpucost,
			Node:     n,
			Strategy: StrategyNameNode,
		}
		log.Log.Debugw(
			"generated cost item",
			zap.String("node", ci.Node.ObjectMeta.Name),
			zap.String("strategy", ci.Strategy),
			zap.Int64("value", ci.Value),
		)
		cis = append(cis, ci)
	}
	return cis
})

NodePricingStrategy generates cost metrics that represent the cost of an active node, regardless of pod. This is generally used to provide an overall cost metric that can be compared to per-pod costs.

View Source
var WeightedPricingStrategy = PricingStrategyFunc(func(table CostTable, duration time.Duration, pods []*core_v1.Pod, nodes []*core_v1.Node) []CostItem {
	nrm := buildNormalizedNodeResourceMap(pods, nodes)
	cis := []CostItem{}
	for _, p := range pods {
		cpu := sumPodResource(p, core_v1.ResourceCPU)
		mem := sumPodResource(p, core_v1.ResourceMemory)
		gpu := sumPodResource(p, ResourceGPU)

		nr, ok := nrm[p.Spec.NodeName]
		if !ok {
			log.Log.Warnw("could not find nodeResourceMap for node", zap.String("nodeName", p.Spec.NodeName))
			continue
		}

		te, err := table.FindByLabels(nr.node.Labels)
		if err != nil {
			log.Log.Warnw("could not find pricing entry for node", zap.String("nodeName", nr.node.ObjectMeta.Name))
			continue
		}

		cpucost := te.CPUCostMicroCents(float64(cpu)*nr.CPUScale(), duration)
		memcost := te.MemoryCostMicroCents(float64(mem)*nr.MemoryScale(), duration)
		gpucost := te.GPUCostMicroCents(float64(gpu)*nr.GPUScale(), duration)

		ci := CostItem{
			Kind:     ResourceCostWeighted,
			Value:    cpucost + memcost + gpucost,
			Pod:      p,
			Node:     nr.node,
			Strategy: StrategyNameWeighted,
		}
		log.Log.Debugw(
			"generated cost item",
			zap.String("pod", ci.Pod.ObjectMeta.Name),
			zap.String("strategy", ci.Strategy),
			zap.Int64("value", ci.Value),
		)
		cis = append(cis, ci)
	}
	return cis
})

WeightedPricingStrategy calculates the cost of a pod based on it's average use of the CPU and Memory requests as a fraction of all CPU and memory requests on the node onto which it has been allocated. This strategy ensures that unallocated resources do not go unattributed and has a tendency to punish pods that may occupy oddly shaped resources or those that frequently churn.

Functions

func NewKubernetesCoster

func NewKubernetesCoster(
	interval time.Duration,
	config *Config,
	client kubernetes.Interface,
	prometheusExporter *prometheus.Exporter,
	listenAddr string,
	costExporters []CostExporter,
) (*coster, error)

NewKubernetesCoster returns a new coster that talks to a kubernetes cluster via the provided client.

func RunningPodFilter

func RunningPodFilter(p *core_v1.Pod) bool

RunningPodFilter returns true if the Pod is running.

Types

type BufferingCostExporter

type BufferingCostExporter struct {
	// contains filtered or unexported fields
}

BufferingCostExporter is an exporter that locally merges similarly dimensioned data on the client before emitting to other exporters.

func NewBufferingCostExporter

func NewBufferingCostExporter(ctx context.Context, interval time.Duration, next CostExporter) (*BufferingCostExporter, error)

NewBufferingCostExporter returns a BufferingCostExporter that flushes on the provided interval. The backgrounded flush procedure can be cancelled by cancelling the provided context. On every interval we emit aggregated cost metrics to the provided `next` CostExporter.

func (*BufferingCostExporter) ExportCost

func (bce *BufferingCostExporter) ExportCost(cd CostData)

ExportCost enqueues the CostData provided for subsequent emission to the next cost exporter. This serves to debounce repeated cost events and reduce load on the system.

type Config

type Config struct {
	Mapper  Mapper
	Pricing CostTable
}

Config contains the configuration data necessary to drive the kubernetesCoster. It includes both mapping information to teach it how to derive metric dimensions from pod labels, as well as as a pricing table to instruct it how expensive nodes are.

func NewConfigFromReader

func NewConfigFromReader(reader io.Reader) (*Config, error)

NewConfigFromReader constructs a Config from an io.Reader.

type CostData

type CostData struct {
	// The kind of cost figure represented.
	Kind ResourceCostKind
	// The strategy the yielded this CostItem.
	Strategy string
	// The value in microcents that it costs.
	Value int64
	// Additional dimensions associated with the cost.
	Dimensions map[string]string
	// The interval for which this metric was created.
	EndTime time.Time
}

CostData models pubsub-exported cost metadata.

func (*CostData) MarshalLogObject

func (c *CostData) MarshalLogObject(enc zapcore.ObjectEncoder) error

MarshalLogObject exports CostData fields for the zap logger.

type CostDataKey

type CostDataKey struct {
	// The kind of cost figure represented.
	Kind ResourceCostKind
	// The strategy the yielded this CostItem.
	Strategy string
	// Additional dimensions associated with the cost.
	Dimensions string
}

CostDataKey groups related cost data. Note: this isn't very space efficient at the moment given the duplication between it and the CostDataRow. We could, for example use a hashing function instead but this ought to be friendly for debugging in the short term.

type CostExporter

type CostExporter interface {
	ExportCost(cd CostData)
}

CostExporter emits CostItems - for example, as a metric or to a third-party system.

type CostItem

type CostItem struct {
	// The kind of cost figure represented.
	Kind ResourceCostKind
	// The strategy the yielded this CostItem.
	Strategy string
	// The value in microcents that it costs.
	Value int64
	// Kubernetes pod metadata associated with the pod which we're pricing out.
	Pod *core_v1.Pod
	// Kubernetes pod metadata associated with the node which we're pricing out.
	Node *core_v1.Node
}

CostItem models the metadata associated with a pod and/or node cost. Generally, this is subsequently utilized in order to emit an associated cost metric with dimensions derived from an appropriately configured Mapper.

type CostTable

type CostTable struct {
	Entries []*CostTableEntry
}

CostTable is a collection of CostTableEntries, generally used to look up pricing data via a set of labels provided callers of it's FindByLabels method. The order of of entries determines precedence of potentially multiple applicable matches.

func (*CostTable) FindByLabels

func (ct *CostTable) FindByLabels(labels Labels) (*CostTableEntry, error)

FindByLabels returns the first matching CostTableEntry whose labels are a subset of those provided.

A CostTableEntry with labels:

{"size": "large", "region": usa"}

will match:

{"size": "large", "region": "usa"}

an will also match:

{"size": "large", "region": "usa", "foo": "bar"}

but will not match:

{"region": "usa"}

type CostTableEntry

type CostTableEntry struct {
	Labels                         Labels
	HourlyMemoryByteCostMicroCents float64
	HourlyMilliCPUCostMicroCents   float64
	HourlyGPUCostMicroCents        float64
}

CostTableEntry models the cost of a nodes resources. The labels are used to identify nodes.

func (*CostTableEntry) CPUCostMicroCents

func (e *CostTableEntry) CPUCostMicroCents(millicpu float64, duration time.Duration) int64

CPUCostMicroCents returns the cost of the provided cpu over a given duration in millionths of a cent.

func (*CostTableEntry) GPUCostMicroCents

func (e *CostTableEntry) GPUCostMicroCents(gpus float64, duration time.Duration) int64

GPUCostMicroCents returns the cost of the provided number of gpus over a given duration in millionths of a cent.

func (*CostTableEntry) Match

func (e *CostTableEntry) Match(labels Labels) bool

Match returns true if all of the CostTableEntry's labels match some subeset of the labels provided.

Additional labels can be used to increase the specificity of the selector and are generally useful for refining cost table configurations - e.g. from global, to per region pricing by using labels. For example, in GCP the following labels may be added to nodes in Kubernetes 1.11: - beta.kubernetes.io/instance-type: n1-standard-16 - failure-domain.beta.kubernetes.io/region: us-central1 - failure-domain.beta.kubernetes.io/zone: us-central1-b

Note: A special case of match against an empty list of labels will always match a CostTableEntry with no Labels.

func (*CostTableEntry) MemoryCostMicroCents

func (e *CostTableEntry) MemoryCostMicroCents(membytes float64, duration time.Duration) int64

MemoryCostMicroCents returns the cost of the provided memory in bytes over a given duration in millionths of a cent.

type Coster

type Coster interface {
	CalculateAndEmit() error
	Run(ctx context.Context) error
}

Coster is used to calculate and emit metrics for services and components running in a kubernetes cluster.

type Labels

type Labels map[string]string

Labels augments a slice ofa labels with matching functionality.

func (Labels) Match

func (l Labels) Match(key, value string) bool

Match checks if the provided label exists within the available labels.

type Mapper

type Mapper struct {
	Entries []Mapping
}

Mapper is a used to manage a set of mappings from source fields in a generic interface{} to a destination.

func (*Mapper) MapData

func (m *Mapper) MapData(obj interface{}) (map[string]string, error)

MapData returns a string map by applying the mappers rules to the obj provided. The resulting map should have a corresponding field for every source object.

func (*Mapper) TagKeys

func (m *Mapper) TagKeys() ([]tag.Key, error)

TagKeys returns a slice of tag.Key structs, useful when preparing your opencensus view to accept the dimensions derived from your mapping.

type Mapping

type Mapping struct {
	Default     string
	Destination string
	Source      string
}

Mapping models how to map a destination field from a source field within a kubernetes resource. The source is typically a jsonPath expression.

type PodFilter

type PodFilter func(p *core_v1.Pod) bool

PodFilter returns true if Pod should be included in filtered results.

type PodFilters

type PodFilters []PodFilter

PodFilters augments a slice of PodFilter functions with additional collection related functionality.

func (PodFilters) All

func (pf PodFilters) All(p *core_v1.Pod) bool

All returns true if all predicate functions in match the provided pod.

type PricingStrategy

type PricingStrategy interface {
	Calculate(t CostTable, duration time.Duration, pods []*core_v1.Pod, nodes []*core_v1.Node) []CostItem
}

PricingStrategy generates CostItems given the pods and nodes running in a cluster.

type PricingStrategyFunc

type PricingStrategyFunc func(table CostTable, duration time.Duration, pods []*core_v1.Pod, nodes []*core_v1.Node) []CostItem

PricingStrategyFunc is an interface wrapper to convert a function into valid PricingStrategy.

func (PricingStrategyFunc) Calculate

func (f PricingStrategyFunc) Calculate(table CostTable, duration time.Duration, pods []*core_v1.Pod, nodes []*core_v1.Node) []CostItem

Calculate returns CostItems given a pricing table of node costs, the duration we're costing out, and the pods as well as nodes running in a cluster.

type PubsubCostExporter

type PubsubCostExporter struct {
	// contains filtered or unexported fields
}

PubsubCostExporter emits data to pubsub.

func NewPubsubCostExporter

func NewPubsubCostExporter(ctx context.Context, topic string, project string) (*PubsubCostExporter, error)

NewPubsubCostExporter creates a new PubsubCostExporter, instantiating an internal client against google cloud APIs.

func (*PubsubCostExporter) ExportCost

func (pe *PubsubCostExporter) ExportCost(cd CostData)

ExportCost emits the CostItem to the PubsubCostExporter's configured pubsub topic.

type ResourceCostKind

type ResourceCostKind string

ResourceCostKind is used to indidicate what resource a cost was derived from.

type StatsCostExporter

type StatsCostExporter struct {
	// contains filtered or unexported fields
}

StatsCostExporter emits metrics to a stats system.

func NewStatsCostExporter

func NewStatsCostExporter(mapper *Mapper) *StatsCostExporter

NewStatsCostExporter returns a new StatsCostExporter.

func (*StatsCostExporter) ExportCost

func (sce *StatsCostExporter) ExportCost(cd CostData)

ExportCost emits cost data to the stats system.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL