Untitled
unknown
plain_text
10 months ago
6.4 kB
18
Indexable
package main
import (
"context"
"flag"
"fmt"
"os"
"os/signal"
"sort"
"sync"
"syscall"
"time"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
cache "k8s.io/client-go/tools/cache"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
)
type rec struct {
created time.Time
sched *time.Time
ready *time.Time
reported bool
}
type ms struct{ Pending, Startup, Total int64 }
type agg struct {
mu sync.Mutex
list []ms
sumP int64
sumS int64
sumT int64
minP int64
minS int64
minT int64
maxP int64
maxS int64
maxT int64
count int64
}
func newAgg() *agg {
const big = int64(^uint64(0) >> 1) // max int64
return &agg{minP: big, minS: big, minT: big}
}
func (a *agg) add(m ms) {
a.mu.Lock()
defer a.mu.Unlock()
a.list = append(a.list, m)
a.count++
a.sumP += m.Pending
a.sumS += m.Startup
a.sumT += m.Total
if m.Pending < a.minP {
a.minP = m.Pending
}
if m.Startup < a.minS {
a.minS = m.Startup
}
if m.Total < a.minT {
a.minT = m.Total
}
if m.Pending > a.maxP {
a.maxP = m.Pending
}
if m.Startup > a.maxS {
a.maxS = m.Startup
}
if m.Total > a.maxT {
a.maxT = m.Total
}
}
func (a *agg) avg(x int64) int64 {
if a.count == 0 {
return 0
}
return x / a.count
}
func (a *agg) done(expect int) bool {
a.mu.Lock()
defer a.mu.Unlock()
return expect > 0 && int(a.count) >= expect
}
func mustConfig() *rest.Config {
// Try in-cluster, then kubeconfig
if cfg, err := rest.InClusterConfig(); err == nil {
return cfg
}
kc := os.Getenv("KUBECONFIG")
if kc == "" {
home, _ := os.UserHomeDir()
kc = home + "/.kube/config"
}
cfg, err := clientcmd.BuildConfigFromFlags("", kc)
if err != nil {
panic(err)
}
return cfg
}
func main() {
var (
ns = flag.String("namespace", "", "Namespace to watch (empty = all)")
sel = flag.String("selector", "app=kb-latency", "Label selector for pods to measure")
expect = flag.Int("expect", 0, "Stop after N pods measured (0 = never)")
timeout = flag.Duration("timeout", 0, "Hard stop after this duration (0 = no timeout)")
showEach = flag.Bool("per-pod", true, "Print per-pod measurements")
)
flag.Parse()
cfg := mustConfig()
cs, err := kubernetes.NewForConfig(cfg)
if err != nil {
panic(err)
}
ctx, cancel := context.WithCancel(context.Background())
if *timeout > 0 {
ctx, cancel = context.WithTimeout(ctx, *timeout)
}
defer cancel()
// Tweaked informer (namespace + label selector)
opts := []informers.SharedInformerOption{}
if *ns != "" {
opts = append(opts, informers.WithNamespace(*ns))
}
selector, err := labels.Parse(*sel)
if err != nil {
panic(fmt.Errorf("bad selector: %w", err))
}
opts = append(opts, informers.WithTweakListOptions(func(lo *metav1.ListOptions) {
lo.LabelSelector = selector.String()
}))
f := informers.NewSharedInformerFactoryWithOptions(cs, 0, opts...)
podInf := f.Core().V1().Pods().Informer()
var (
lock sync.Mutex
byKey = map[string]*rec{} // ns/name -> rec
stats = newAgg()
sigCh = make(chan os.Signal, 1)
)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
handle := func(pod *corev1.Pod) {
key := pod.Namespace + "/" + pod.Name
lock.Lock()
r, ok := byKey[key]
if !ok {
r = &rec{created: pod.CreationTimestamp.Time}
byKey[key] = r
}
// Update created if zero (shouldn't happen) or earlier
if pod.CreationTimestamp.Time.Before(r.created) {
r.created = pod.CreationTimestamp.Time
}
// PodScheduled condition
for _, c := range pod.Status.Conditions {
if c.Type == corev1.PodScheduled && c.Status == corev1.ConditionTrue {
t := c.LastTransitionTime.Time
r.sched = &t
}
if c.Type == corev1.PodReady && c.Status == corev1.ConditionTrue {
t := c.LastTransitionTime.Time
r.ready = &t
}
}
// If we have all timestamps and not reported → compute
if !r.reported && r.sched != nil && r.ready != nil {
pending := r.sched.Sub(r.created).Milliseconds()
startup := r.ready.Sub(*r.sched).Milliseconds()
total := r.ready.Sub(r.created).Milliseconds()
stats.add(ms{Pending: pending, Startup: startup, Total: total})
r.reported = true
if *showEach {
fmt.Printf("%s pending_ms=%d startup_ms=%d total_ms=%d (node=%s)\n",
key, pending, startup, total, pod.Spec.NodeName)
}
}
lock.Unlock()
}
podInf.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
if pod, ok := obj.(*corev1.Pod); ok {
handle(pod)
}
},
UpdateFunc: func(_, newObj interface{}) {
if pod, ok := newObj.(*corev1.Pod); ok {
handle(pod)
}
},
})
// Start informers
f.Start(ctx.Done())
f.WaitForCacheSync(ctx.Done())
// Wait until: expect reached OR signal OR timeout
tick := time.NewTicker(500 * time.Millisecond)
defer tick.Stop()
loop:
for {
select {
case <-ctx.Done():
break loop
case <-sigCh:
break loop
case <-tick.C:
if stats.done(*expect) && *expect > 0 {
break loop
}
}
}
// Final report
stats.mu.Lock()
defer stats.mu.Unlock()
if stats.count == 0 {
fmt.Println("no measurements collected (check selector/namespace or if pods reached Ready)")
return
}
// Optional percentiles (nice to have)
getP := func(values []int64, p float64) int64 {
if len(values) == 0 {
return 0
}
idx := int(float64(len(values)-1) * p)
return values[idx]
}
var pendings, startups, totals []int64
for _, m := range stats.list {
pendings = append(pendings, m.Pending)
startups = append(startups, m.Startup)
totals = append(totals, m.Total)
}
sort.Slice(pendings, func(i, j int) bool { return pendings[i] < pendings[j] })
sort.Slice(startups, func(i, j int) bool { return startups[i] < startups[j] })
sort.Slice(totals, func(i, j int) bool { return totals[i] < totals[j] })
fmt.Printf("\n--- SUMMARY (pods=%d) ---\n", stats.count)
fmt.Printf("Pending ms: min=%d p50=%d p95=%d max=%d avg=%d\n",
stats.minP, getP(pendings, 0.50), getP(pendings, 0.95), stats.maxP, stats.avg(stats.sumP))
fmt.Printf("Startup ms: min=%d p50=%d p95=%d max=%d avg=%d\n",
stats.minS, getP(startups, 0.50), getP(startups, 0.95), stats.maxS, stats.avg(stats.sumS))
fmt.Printf("Total ms: min=%d p50=%d p95=%d max=%d avg=%d\n",
stats.minT, getP(totals, 0.50), getP(totals, 0.95), stats.maxT, stats.avg(stats.sumT))
}Editor is loading...
Leave a Comment