~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
177 lines · 4.4 KB Raw History
package scale

import (
	"fmt"
	"slices"
	"strings"
	"time"

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/labels"
)

const UnschedulableSettle = 5 * time.Second

type Unschedulable struct {
	Pod string

	SeenNodes int

	Message string
}

type Spread struct {
	Group string

	Pods int
}

type Want struct {
	Target int

	Wanted int

	Why string

	Waiting []string

	Hold string
}

func NodeReady(n *corev1.Node) (bool, time.Time) {
	for _, c := range n.Status.Conditions {
		if c.Type == corev1.NodeReady {
			return c.Status == corev1.ConditionTrue, c.LastTransitionTime.Time
		}
	}
	return false, time.Time{}
}

func unschedulable(p *corev1.Pod) (Unschedulable, bool) {
	for _, c := range p.Status.Conditions {
		if c.Type != corev1.PodScheduled || c.Status != corev1.ConditionFalse || c.Reason != corev1.PodReasonUnschedulable {
			continue
		}
		seen := -1
		var n int
		if _, err := fmt.Sscanf(c.Message, "0/%d nodes are available", &n); err == nil {
			seen = n
		}
		return Unschedulable{Pod: p.Namespace + "/" + p.Name, SeenNodes: seen, Message: c.Message}, true
	}
	return Unschedulable{}, false
}

func hostnameAntiAffinity(p *corev1.Pod) []string {
	a := p.Spec.Affinity
	if a == nil || a.PodAntiAffinity == nil {
		return nil
	}
	var out []string
	for _, term := range a.PodAntiAffinity.RequiredDuringSchedulingIgnoredDuringExecution {
		if term.TopologyKey != HostnameLabel || term.NamespaceSelector != nil || term.LabelSelector == nil {
			continue
		}
		if len(term.Namespaces) > 0 && !slices.Equal(term.Namespaces, []string{p.Namespace}) {
			continue
		}
		sel, err := metav1.LabelSelectorAsSelector(term.LabelSelector)
		if err != nil || sel.Empty() || !sel.Matches(labels.Set(p.Labels)) {
			continue
		}
		out = append(out, p.Namespace+"/"+metav1.FormatLabelSelector(term.LabelSelector))
	}
	return out
}

func spreadOf(pods []*corev1.Pod) Spread {
	groups := map[string]int{}
	for _, p := range pods {
		for _, g := range hostnameAntiAffinity(p) {
			groups[g]++
		}
	}
	var out Spread
	for g, n := range groups {
		if n > out.Pods || (n == out.Pods && g < out.Group) {
			out = Spread{Group: g, Pods: n}
		}
	}
	return out
}

func (p Placement) settled(now time.Time) (bool, string) {
	if len(p.Nodes) == 0 {
		return false, "no node yet"
	}
	for _, n := range p.Nodes {
		if !n.Ready {
			return false, fmt.Sprintf("node %s is not Ready", n.Name)
		}
		if age := now.Sub(n.ReadySince); age < UnschedulableSettle {
			return false, fmt.Sprintf("node %s has been Ready for %s, under the %s the scheduler "+
				"gets to try the Pending pods on it", n.Name, age.Round(time.Second), UnschedulableSettle)
		}
	}
	return true, ""
}

func (p Placement) blocked(now time.Time, inFlight int) ([]string, string) {
	if len(p.Unschedulable) == 0 {
		return nil, ""
	}
	if inFlight > 0 {
		return nil, fmt.Sprintf("%d node(s) are still joining", inFlight)
	}
	if ok, why := p.settled(now); !ok {
		return nil, why
	}
	var pods []string
	for _, u := range p.Unschedulable {
		if u.SeenNodes == len(p.Nodes) {
			pods = append(pods, u.Pod)
		}
	}
	if len(pods) == 0 {
		return nil, fmt.Sprintf("the scheduler has not yet tried the %d unschedulable pod(s) on all %d nodes",
			len(p.Unschedulable), len(p.Nodes))
	}
	for _, n := range p.Nodes {
		if n.idle() {
			return nil, fmt.Sprintf("pod(s) %s are unschedulable although node %s holds none of the "+
				"workload, so a fresh market node would refuse them too: no node is added for them",
				strings.Join(pods, ", "), n.Name)
		}
	}
	return pods, ""
}

func Wanted(o Observation, opts Options, now time.Time) Want {
	if !Intent(o) {
		return Want{}
	}
	per := opts.podsPerNode()
	w := Want{Wanted: (Demand(o) + per - 1) / per}
	w.Why = fmt.Sprintf("%d pod(s) of demand at %d per node", Demand(o), per)
	if w.Wanted < 1 {
		w.Wanted = 1
	}
	if s := o.Placement.Spread; s.Pods > w.Wanted {
		w.Wanted = s.Pods
		w.Why = fmt.Sprintf("%d pods of %s repel each other by required pod anti-affinity on %s",
			s.Pods, s.Group, HostnameLabel)
	}
	pods, wait := o.Placement.blocked(now, o.InFlight)
	if len(pods) > 0 && len(o.Placement.Nodes)+1 > w.Wanted {
		w.Wanted = len(o.Placement.Nodes) + 1
		w.Why = fmt.Sprintf("the scheduler refused pod(s) %s on all %d Ready nodes",
			strings.Join(pods, ", "), len(o.Placement.Nodes))
	}
	if len(pods) == 0 && wait != "" {
		w.Hold = "unschedulable pods add no node yet: " + wait
	}
	w.Waiting = pods
	w.Target = min(w.Wanted, opts.maxNodes())
	return w
}