feat(service): add iamge monitor to watch pull image error and slow pull (#6100)

* feat(service): add iamge monitor to watch pull image error and slow pull

* fix: replace request

* fix: ci lint

* fix: comments convert to en
This commit is contained in:
zijiren
2025-10-17 14:36:21 +08:00
committed by GitHub
parent 11e54e495d
commit 627131c2ed
13 changed files with 1304 additions and 22 deletions
+10 -7
View File
@@ -4,21 +4,24 @@ use (
.
./account
./database
./devbox
./exceptionmonitor
./hubble
./imagemonitor
./launchpad
./pay
./devbox
./vlogs
./hubble
)
replace (
github.com/google/gnostic-models => github.com/google/gnostic-models v0.6.8
github.com/labring/sealos/controllers/account => ../controllers/account
github.com/labring/sealos/controllers/user => ../controllers/user
k8s.io/api => k8s.io/api v0.32.3
k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.32.1
k8s.io/apimachinery => k8s.io/apimachinery v0.32.1
k8s.io/client-go => k8s.io/client-go v0.32.1
sigs.k8s.io/controller-runtime => sigs.k8s.io/controller-runtime v0.20.4
k8s.io/api => k8s.io/api v0.32.1
k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.32.1
k8s.io/apimachinery => k8s.io/apimachinery v0.32.1
k8s.io/client-go => k8s.io/client-go v0.32.1
k8s.io/kube-openapi => k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f
sigs.k8s.io/controller-runtime => sigs.k8s.io/controller-runtime v0.20.4
)
+328 -15
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -0,0 +1,3 @@
/.idea
/imagemonitor
/imagemonitor.exe
+126
View File
@@ -0,0 +1,126 @@
# Image Monitor Service
A real-time Kubernetes image pull monitoring service for Sealos, designed to track and analyze container image pulling issues with Prometheus metrics integration.
## Overview
Image Monitor Service acts as a monitoring layer for Kubernetes cluster image pull operations, providing:
- **Real-time Pod Monitoring**: Continuously watches Pod events using Kubernetes Informer mechanism
- **Image Pull Failure Detection**: Automatically detects and classifies various image pull failures
- **Slow Pull Alerts**: Tracks and alerts on slow image pull operations (>3 minutes)
- **Prometheus Metrics**: Exports detailed metrics for monitoring and alerting
- **Intelligent Classification**: Categorizes failures by root cause (network errors, auth issues, image not found, etc.)
- **Graceful Shutdown**: Proper signal handling for zero-downtime deployments
## Architecture
```
┌─────────────────┐
│ Kubernetes │
│ API Server │
└────────┬────────┘
│ Watch Pods
┌──────────────────┐ ┌──────────────────┐
│ Image Monitor │────▶│ Prometheus │
│ Service │ │ Metrics │
└──────────────────┘ └──────────────────┘
┌──────────────┐
│ Failure │
│ Analyzer │
│ - Network │
│ - Auth │
│ - Not Found │
│ - Slow Pull │
└──────────────┘
```
### Key Components
- **Pod Informer**: Watches all Pod resources across the cluster, tracking image pull status changes
- **Failure Analyzer** (`analyzer.go`): Analyzes container status and identifies image pull errors
- **Failure Classifier** (`classifier.go`): Categorizes failures into specific types using regex pattern matching
- **Slow Pull Tracker** (`slow_pull.go`): Monitors pulling duration and triggers alerts for slow operations
- **Metrics Exporter** (`metrics.go`): Exposes Prometheus metrics at `:8080/metrics`
## Features
- **Comprehensive Failure Classification**: Automatically identifies and categorizes:
- Image not found (404, manifest unknown)
- Proxy connection errors
- Authentication/authorization failures
- TLS handshake failures
- I/O timeout issues
- Connection refused errors
- Network request failures
- Back-off states
- **Slow Pull Detection**:
- Tracks images in "ContainerCreating" state
- Alerts when pulling exceeds 3-minute threshold
- Clears alert when image finally succeeds or fails
- **Smart State Management**:
- Preserves specific failure reasons over generic back-off states
- Cleans up metrics when Pods are deleted
- Handles both init containers and regular containers
- **Public Registry Focus**: Monitors only public registry images to avoid exposing private infrastructure
## Prerequisites
- Kubernetes cluster (v1.20+)
- In-cluster deployment with appropriate RBAC permissions
- Prometheus for metrics collection (optional but recommended)
- Go 1.24+ (for development)
## Metrics
The service exposes two main Prometheus metrics:
### `image_pull_failure`
Tracks active image pull failures with detailed labels.
**Type**: Gauge
**Labels**:
- `namespace`: Pod namespace
- `pod`: Pod name
- `node`: Node where pull is failing
- `registry`: Container registry (e.g., docker.io, ghcr.io)
- `image`: Full image reference
- `reason`: Classified failure reason
**Example**:
```promql
image_pull_failure{
namespace="default",
pod="my-app-xyz",
node="node-1",
registry="docker.io",
image="nginx:latest",
reason="image_not_found"
} 1
```
### `image_pull_slow_alert`
Tracks slow image pull operations (>3 minutes).
**Type**: Gauge
**Labels**:
- `namespace`: Pod namespace
- `pod`: Pod name
- `container`: Container name
- `image`: Full image reference
**Example**:
```promql
image_pull_slow_alert{
namespace="default",
pod="my-app-xyz",
container="app",
image="docker.io/myimage:v1.0"
} 1
```
+55
View File
@@ -0,0 +1,55 @@
package main
import (
"fmt"
"sync"
corev1 "k8s.io/api/core/v1"
"k8s.io/client-go/kubernetes"
)
var (
podFailures sync.Map // key namespace/pod -> *podInfo
clientset *kubernetes.Clientset
)
func checkImagePullError(
statuses []corev1.ContainerStatus,
nodeName string,
pod *corev1.Pod,
reasons map[string]failureInfo,
) {
for _, cs := range statuses {
if cs.State.Waiting == nil ||
!isImagePullFailureReason(cs.State.Waiting.Reason) ||
!isPublicRegistry(cs.Image) {
continue
}
classified := classifyFailureReason(
cs.State.Waiting.Reason,
cs.State.Waiting.Message,
)
registry := parseRegistry(cs.Image)
// Use container name as key
reasons[cs.Name] = failureInfo{
registry: registry,
nodeName: nodeName, // Record current node
image: cs.Image, // Record image info
reason: classified, // Record failure reason
}
// If in failure state, clean up corresponding slow pull state
key := fmt.Sprintf("%s/%s/%s", pod.Namespace, pod.Name, cs.Name)
cleanupSlowPull(key)
}
}
func analyzePodImagePullErrors(nodeName string, pod *corev1.Pod) map[string]failureInfo {
reasons := make(map[string]failureInfo)
checkImagePullError(pod.Status.InitContainerStatuses, nodeName, pod, reasons)
checkImagePullError(pod.Status.ContainerStatuses, nodeName, pod, reasons)
return reasons
}
+102
View File
@@ -0,0 +1,102 @@
package main
import (
"log"
"regexp"
"strings"
)
// Predefined regular expressions for different failure reasons, used to classify error messages
var (
reImageNotFound = regexp.MustCompile(
`(?i)not found|NotFound|manifest unknown|repository does not exist`,
)
reProxyError = regexp.MustCompile(`(?i)proxyconnect|proxy error`)
reUnauthorized = regexp.MustCompile(
`(?i)unauthorized|authentication require|failed to authorize|authorization failed`,
)
reTLS = regexp.MustCompile(`(?i)tls handshake|failed to verify certificate`)
reIOTimeout = regexp.MustCompile(`(?i)i/o timeout`)
reConnectionRefused = regexp.MustCompile(`(?i)connection refused`)
reNetworkError = regexp.MustCompile(`(?i)failed to do request`)
)
// isBackOffPullingImage checks if the state is back-off pulling image
func isBackOffPullingImage(reason, message string) bool {
if strings.ToLower(reason) == "imagepullbackoff" {
return true
}
if strings.Contains(strings.ToLower(message), "back-off pulling image") {
return true
}
return false
}
func isImagePullFailureReason(reason string) bool {
switch reason {
case "ErrImagePull", "ImagePullBackOff", "Cancelled", "RegistryUnavailable":
return true
default:
return false
}
}
func isImagePullSlowReason(reason string) bool {
switch reason {
case "ContainerCreating":
return true
default:
return false
}
}
func classifyFailureReason(r, message string) reason {
lowMsg := strings.ToLower(message)
switch strings.ToLower(r) {
case "errimagepull", "imagepullbackoff":
if reImageNotFound.MatchString(lowMsg) {
return ReasonImageNotFound
}
if reProxyError.MatchString(lowMsg) {
return ReasonProxyError
}
if reUnauthorized.MatchString(lowMsg) {
return ReasonUnauthorized
}
if reTLS.MatchString(lowMsg) {
return ReasonTLSHandshake
}
if reIOTimeout.MatchString(lowMsg) {
return ReasonIOTimeout
}
if reConnectionRefused.MatchString(lowMsg) {
return ReasonConnectionRefused
}
if reNetworkError.MatchString(lowMsg) {
return ReasonNetworkError
}
if strings.HasPrefix(lowMsg, "back-off pulling image") {
return ReasonBackOff
}
log.Printf("[Classify] Unknown error classification reason=%s message=%s", r, message)
return ReasonUnknown
default:
return strings.ToLower(r)
}
}
// isSpecificReason determines if it's a specific failure reason (not back_off_pulling_image)
func isSpecificReason(reason string) bool {
return reason != ReasonBackOff && reason != ReasonUnknown
}
+52
View File
@@ -0,0 +1,52 @@
module github.com/labring/sealos/service/imagemonitor
go 1.24.0
require (
github.com/prometheus/client_golang v1.23.2
k8s.io/api v0.34.1
k8s.io/apimachinery v0.34.1
k8s.io/client-go v12.0.0+incompatible
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/spf13/pflag v1.0.6-0.20210604193023-d5e0c0615ace // indirect
github.com/x448/float16 v0.8.4 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.41.0 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/term v0.34.0 // indirect
golang.org/x/text v0.28.0 // indirect
golang.org/x/time v0.9.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.2.1 // indirect
k8s.io/klog v0.3.1 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)
+265
View File
@@ -0,0 +1,265 @@
package main
import (
"fmt"
"log"
corev1 "k8s.io/api/core/v1"
"k8s.io/client-go/tools/cache"
)
func onPodAddOrUpdate(obj any) {
pod, ok := obj.(*corev1.Pod)
if !ok {
return
}
currentNodeName := getNodeName(pod)
if currentNodeName == "" {
// not schedule to any node, skip
return
}
log.Printf(
"[onPodAddOrUpdate] phase=%s uid=%s node=%s namespace=%s pod=%s containers=%d",
pod.Status.Phase,
string(pod.UID),
currentNodeName,
pod.Namespace,
pod.Name,
len(pod.Status.ContainerStatuses),
)
// Iterate through InitContainerStatuses + ContainerStatuses
for _, cs := range pod.Status.InitContainerStatuses {
checkSlowPull(pod.Namespace, pod.Name, cs, cs.Image)
}
for _, cs := range pod.Status.ContainerStatuses {
checkSlowPull(pod.Namespace, pod.Name, cs, cs.Image)
}
reasons := analyzePodImagePullErrors(currentNodeName, pod)
podKey := fmt.Sprintf("%s/%s", pod.Namespace, pod.Name)
piVal, _ := podFailures.LoadOrStore(podKey, &podInfo{
reasons: make(map[string]failureInfo),
podName: pod.Name,
namespace: pod.Namespace,
})
pi, ok := piVal.(*podInfo)
if !ok {
log.Printf("[onPodAddOrUpdate] Unable to parse added object type: %T", piVal)
return
}
pi.mu.Lock()
defer pi.mu.Unlock()
pi.namespace = pod.Namespace
pi.podName = pod.Name
updateReasons(pi, reasons)
}
func onPodDelete(obj any) {
pod, ok := obj.(*corev1.Pod)
if !ok {
tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
if !ok {
log.Printf("[onPodDelete] Unable to parse deleted object type: %T", obj)
return
}
pod, ok = tombstone.Obj.(*corev1.Pod)
if !ok {
log.Printf("[onPodDelete] Unable to convert tombstone object: %T", tombstone.Obj)
return
}
}
log.Printf("[onPodDelete] namespace=%s pod=%s", pod.Namespace, pod.Name)
key := fmt.Sprintf("%s/%s", pod.Namespace, pod.Name)
reasonsVal, loaded := podFailures.LoadAndDelete(key)
if loaded {
pi, ok := reasonsVal.(*podInfo)
if !ok {
log.Printf("[onPodDelete] Unable to parse deleted object type: %T", reasonsVal)
return
}
pi.mu.Lock()
defer pi.mu.Unlock()
log.Printf(
"[onPodDelete] namespace=%s pod=%s cleanup %d reasons",
pi.namespace,
pi.podName,
len(pi.reasons),
)
// Use stored node information for Dec operation to ensure execution on correct node
for containerName, info := range pi.reasons {
log.Printf(
"[onPodDelete] Dec gauge: namespace=%s pod=%s container=%s node=%s registry=%s image=%s reason=%s",
pi.namespace,
pi.podName,
containerName,
info.nodeName,
info.registry,
info.image,
info.reason,
)
imagePullFailureGauge.DeleteLabelValues(
pi.namespace,
pi.podName,
info.nodeName,
info.registry,
info.image,
info.reason,
)
}
} else {
log.Printf("[onPodDelete] pod %s not found in podFailures", key)
}
// Clean up slow pull related state
prefix := key + "/"
slowPullTimers.Range(newCleanupSlowPullWithPrefixFunc(prefix))
slowPullTracking.Range(newCleanupSlowPullWithPrefixFunc(prefix))
}
func updateReasons(
pi *podInfo,
reasons map[string]failureInfo,
) {
// Remove old reasons - use stored node information
for containerName, oldInfo := range pi.reasons {
if _, found := reasons[containerName]; !found {
log.Printf(
"[UpdateReasons] Dec gauge: namespace=%s pod=%s container=%s node=%s registry=%s image=%s reason=%s",
pi.namespace,
pi.podName,
containerName,
oldInfo.nodeName,
oldInfo.registry,
oldInfo.image,
oldInfo.reason,
)
imagePullFailureGauge.DeleteLabelValues(
pi.namespace,
pi.podName,
oldInfo.nodeName,
oldInfo.registry,
oldInfo.image,
oldInfo.reason,
)
delete(pi.reasons, containerName)
}
}
// Add new reasons
for containerName, info := range reasons {
oldInfo, found := pi.reasons[containerName]
if found {
// Check if we need to preserve the existing specific reason
finalReason := info.reason
// If the new reason is back_off_pulling_image, and other info hasn't changed, and there was a specific reason before, preserve the specific reason
if info.reason == ReasonBackOff &&
oldInfo.nodeName == info.nodeName &&
oldInfo.image == info.image &&
oldInfo.registry == info.registry &&
isSpecificReason(oldInfo.reason) {
finalReason = oldInfo.reason
log.Printf(
"[UpdateReasons] Preserving specific reason for %s/%s container=%s: keeping '%s' instead of 'back_off_pulling_image'",
pi.namespace,
pi.podName,
containerName,
oldInfo.reason,
)
}
// Check if there are changes (node, failure reason, image, etc.)
if oldInfo.nodeName != info.nodeName || oldInfo.reason != finalReason ||
oldInfo.image != info.image {
log.Printf(
"[UpdateReasons] Info changed for %s/%s container=%s: node=%s->%s, reason=%s->%s, image=%s->%s",
pi.namespace,
pi.podName,
containerName,
oldInfo.nodeName,
info.nodeName,
oldInfo.reason,
finalReason,
oldInfo.image,
info.image,
)
// Dec on old information
imagePullFailureGauge.DeleteLabelValues(
pi.namespace,
pi.podName,
oldInfo.nodeName,
oldInfo.registry,
oldInfo.image,
oldInfo.reason,
)
// Inc on new information
imagePullFailureGauge.WithLabelValues(pi.namespace, pi.podName, info.nodeName, info.registry, info.image, finalReason).
Set(1)
// Update stored information using the final determined reason
info.reason = finalReason
pi.reasons[containerName] = info
} else if oldInfo.reason != finalReason {
// Case where only the reason has changed
log.Printf(
"[UpdateReasons] Only reason changed for %s/%s container=%s: %s->%s",
pi.namespace,
pi.podName,
containerName,
oldInfo.reason,
finalReason,
)
imagePullFailureGauge.DeleteLabelValues(
pi.namespace,
pi.podName,
oldInfo.nodeName,
oldInfo.registry,
oldInfo.image,
oldInfo.reason,
)
imagePullFailureGauge.WithLabelValues(pi.namespace, pi.podName, info.nodeName, info.registry, info.image, finalReason).
Set(1)
info.reason = finalReason
pi.reasons[containerName] = info
}
continue
}
// Brand new failed container
log.Printf(
"[UpdateReasons] Inc gauge: namespace=%s pod=%s container=%s node=%s registry=%s image=%s reason=%s",
pi.namespace,
pi.podName,
containerName,
info.nodeName,
info.registry,
info.image,
info.reason,
)
imagePullFailureGauge.WithLabelValues(pi.namespace, pi.podName, info.nodeName, info.registry, info.image, info.reason).
Set(1)
pi.reasons[containerName] = info
}
}
+84
View File
@@ -0,0 +1,84 @@
package main
import (
"context"
"errors"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/cache"
)
func main() {
// Register Prometheus metrics
prometheus.MustRegister(imagePullFailureGauge)
prometheus.MustRegister(imagePullSlowAlertGauge)
// Create in-cluster config
config, err := rest.InClusterConfig()
if err != nil {
log.Fatalf("Error creating in-cluster config: %v", err)
}
clientset, err = kubernetes.NewForConfig(config)
if err != nil {
log.Fatalf("Error creating Kubernetes clientset: %v", err)
}
// Create informer
factory := informers.NewSharedInformerFactory(clientset, 0)
podInformer := factory.Core().V1().Pods().Informer()
_, _ = podInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: onPodAddOrUpdate,
UpdateFunc: func(_, obj any) { onPodAddOrUpdate(obj) },
DeleteFunc: onPodDelete,
})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go podInformer.Run(ctx.Done())
if !cache.WaitForCacheSync(ctx.Done(), podInformer.HasSynced) {
//nolint:gocritic
log.Fatalf("Timed out waiting for caches to sync")
}
// HTTP server and graceful shutdown
srv := &http.Server{
Addr: ":8080",
Handler: promhttp.Handler(),
ReadHeaderTimeout: 5 * time.Second,
}
go func() {
log.Println("Starting metrics server at :8080")
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("Metrics server error: %v", err)
}
}()
// Capture signals
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
log.Println("Shutdown signal received, exiting...")
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer shutdownCancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Printf("Error during server shutdown: %v", err)
}
log.Println("Server gracefully stopped")
}
+26
View File
@@ -0,0 +1,26 @@
package main
import (
"github.com/prometheus/client_golang/prometheus"
)
// Define Prometheus metrics
var (
//nolint:promlinter
imagePullFailureGauge = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "k8s_pod_image_pull_failure_total",
Help: "Number of pods with image pull failures categorized by exported_namespace, exported_pod, node, image and reason",
},
[]string{"exported_namespace", "exported_pod", "node", "registry", "image", "reason"},
)
// Changed to Gauge type, allowing Inc and Dec operations
//nolint:promlinter
imagePullSlowAlertGauge = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "k8s_pod_image_pull_slow_total",
Help: "Number of pods with slow image pull (>=5m), by exported_namespace, exported_pod, node, registry and image",
},
[]string{"exported_namespace", "exported_pod", "node", "registry", "image"},
)
)
+165
View File
@@ -0,0 +1,165 @@
package main
import (
"context"
"fmt"
"log"
"strings"
"sync"
"time"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// slowPullTimers stores image pull timers
var slowPullTimers sync.Map // key:string -> *time.Timer
// slowPullTracking tracks current slow pull state
var slowPullTracking sync.Map // key: namespace/pod/container -> slowPullInfo
func newCheckSlowPullHandler(
slowPullTimerKey, ns, podName string,
cs corev1.ContainerStatus,
image string,
) func() {
return func() {
slowPullTimers.Delete(slowPullTimerKey)
p2, err := clientset.CoreV1().
Pods(ns).
Get(context.Background(), podName, metav1.GetOptions{})
if err != nil {
log.Printf("[SlowPull] Failed to get Pod %s/%s: %v", ns, podName, err)
return
}
nodeName := getNodeName(p2)
if nodeName == "" {
// not schedule to any node, skip
return
}
for _, newCs := range p2.Status.ContainerStatuses {
if newCs.Name != cs.Name {
continue
}
// Check if container is still waiting and not in failure state
if newCs.ContainerID != "" ||
newCs.State.Waiting == nil ||
!isImagePullSlowReason(newCs.State.Waiting.Reason) ||
isBackOffPullingImage(
newCs.State.Waiting.Reason,
newCs.State.Waiting.Message,
) {
break
}
registry := parseRegistry(image)
// Record slow pull state
slowPullInfo := slowPullInfo{
namespace: ns,
podName: podName,
nodeName: nodeName,
registry: registry,
image: image,
}
slowPullTracking.Store(slowPullTimerKey, slowPullInfo)
// Increment slow pull metric
imagePullSlowAlertGauge.WithLabelValues(ns, podName, nodeName, registry, image).
Set(1)
log.Printf(
"[SlowPullAlert] %s/%s container=%s node=%s registry=%s image=%s",
ns,
podName,
cs.Name,
nodeName,
registry,
image,
)
break
}
}
}
func checkSlowPull(ns, podName string, cs corev1.ContainerStatus, image string) {
slowPullTimerKey := fmt.Sprintf("%s/%s/%s", ns, podName, cs.Name)
// Check if in image pull slow state
if cs.ContainerID != "" ||
cs.State.Waiting == nil ||
!isImagePullSlowReason(cs.State.Waiting.Reason) ||
isBackOffPullingImage(cs.State.Waiting.Reason, cs.State.Waiting.Message) {
cleanupSlowPull(slowPullTimerKey)
return
}
// Check if a timer is already running
if _, exists := slowPullTimers.Load(slowPullTimerKey); exists {
// Timer already exists, no need to create duplicate
return
}
timer := time.AfterFunc(
5*time.Minute,
newCheckSlowPullHandler(slowPullTimerKey, ns, podName, cs, image),
)
_, loaded := slowPullTimers.LoadOrStore(slowPullTimerKey, timer)
if loaded {
timer.Stop()
log.Printf(
"[SlowPull] Timer already exists for %s/%s container=%s, stopped duplicate timer",
ns,
podName,
cs.Name,
)
}
}
func newCleanupSlowPullWithPrefixFunc(prefix string) func(k, v any) bool {
return func(k, v any) bool {
if sk, ok := k.(string); ok && strings.HasPrefix(sk, prefix) {
cleanupSlowPull(sk)
}
return true
}
}
// cleanupSlowPull cleans up slow pull state
func cleanupSlowPull(slowPullTimerKey string) {
if val, exists := slowPullTracking.LoadAndDelete(slowPullTimerKey); exists {
if info, ok := val.(slowPullInfo); ok {
imagePullSlowAlertGauge.DeleteLabelValues(
info.namespace,
info.podName,
info.nodeName,
info.registry,
info.image,
)
log.Printf(
"[SlowPullCleanup] Dec slow pull gauge: namespace=%s pod=%s node=%s registry=%s image=%s",
info.namespace,
info.podName,
info.nodeName,
info.registry,
info.image,
)
}
}
// Also clean up timer
if val, exists := slowPullTimers.LoadAndDelete(slowPullTimerKey); exists {
if t, ok := val.(*time.Timer); ok {
t.Stop()
log.Printf("[SlowPullCleanup] Stopped timer for key: %s", slowPullTimerKey)
}
}
}
+44
View File
@@ -0,0 +1,44 @@
package main
import (
"sync"
)
type reason = string
const (
ReasonImageNotFound reason = "image_not_found"
ReasonProxyError reason = "proxy_error"
ReasonUnauthorized reason = "unauthorized"
ReasonTLSHandshake reason = "tls_handshake_error"
ReasonIOTimeout reason = "io_timeout"
ReasonConnectionRefused reason = "connection_refused"
ReasonNetworkError reason = "network_error"
ReasonBackOff reason = "back_off_pulling_image"
ReasonUnknown reason = "unknown"
)
// podInfo contains failure reasons, node information and lock
type podInfo struct {
mu sync.Mutex
reasons map[string]failureInfo // key: container name, value: failureInfo with node info
namespace string
podName string
}
// failureInfo now includes node and image information to ensure Dec operations execute on the correct node
type failureInfo struct {
registry string // Image registry
nodeName string // Node information
image string // Image information
reason reason // Failure reason
}
// slowPullInfo records slow pull information
type slowPullInfo struct {
namespace string
podName string
nodeName string
registry string
image string
}
+44
View File
@@ -0,0 +1,44 @@
package main
import (
"strings"
corev1 "k8s.io/api/core/v1"
)
func getNodeName(pod *corev1.Pod) string {
if pod.Spec.NodeName != "" {
return pod.Spec.NodeName
}
return ""
}
func isPublicRegistry(image string) bool {
if !strings.Contains(image, "/") {
return true
}
return strings.HasPrefix(image, "docker.io/") ||
strings.HasPrefix(image, "gcr.io/") ||
strings.HasPrefix(image, "ghcr.io/") ||
strings.HasPrefix(image, "k8s.gcr.io/") ||
strings.HasPrefix(image, "quay.io/") ||
strings.HasPrefix(image, "registry.k8s.io/") ||
(strings.HasPrefix(image, "registry.") && strings.Contains(image, ".aliyuncs.com/")) ||
(strings.HasPrefix(image, "hub.") && strings.Contains(image, ".sealos.run/")) ||
strings.HasPrefix(image, "sealos.hub") ||
strings.Contains(image, ".cr.aliyuncs.com/")
}
func parseRegistry(image string) string {
if image == "" {
return "unknown"
}
parts := strings.Split(image, "/")
if len(parts) > 1 && strings.Contains(parts[0], ".") {
return parts[0]
}
return "docker.io"
}