Kubelet Source Code Deep Dive: Image Management and Garbage Collection
This article dissects Kubelet's ImageManager and ImageGCManager, detailing their interfaces, implementation structs, pull‑policy handling, parallel image pulling, GC policy thresholds, and the step‑by‑step execution flow that governs image lifecycle on a node.
ImageManager Interface
The ImageManager interface defines a single method that guarantees an image reference exists before a pod starts.
type ImageManager interface {
// EnsureImageExists ensures the image referenced by imgRef exists.
EnsureImageExists(ctx context.Context, objRef *v1.ObjectReference, pod *v1.Pod, imgRef string, pullSecrets []v1.Secret, podSandboxConfig *runtimeapi.PodSandboxConfig, podRuntimeHandler string, pullPolicy v1.PullPolicy) (string, string, error)
}ImageManager Implementation
The concrete imageManager struct holds the components needed for image handling.
type imageManager struct {
recorder record.EventRecorder
imageService kubecontainer.ImageService
backOff *flowcontrol.Backoff
puller imagePuller
podPullingTimeRecorder ImagePodPullingTimeRecorder
}Image Puller Configuration
The imagePuller can operate in serial or parallel mode, controlled by the Kubelet flag serialize-image-pulls. The default value is false. For Docker runtimes older than version 1.9, the flag must be set to true to avoid concurrency issues.
EnsureImageExists Execution Flow
Perform a pre‑check of the image using imagePullPrecheck.
If the image is not present according to the pull policy, invoke the puller to fetch the image.
imageRef, message, err = m.imagePullPrecheck(ctx, objRef, logPrefix, pullPolicy, &spec, imgRef)The pre‑check switches on the pull policy:
func (m *imageManager) imagePullPrecheck(ctx context.Context, objRef *v1.ObjectReference, logPrefix string, pullPolicy v1.PullPolicy, spec *kubecontainer.ImageSpec, imgRef string) (imageRef string, msg string, err error) {
switch pullPolicy {
case v1.PullAlways:
return "", msg, nil
case v1.PullIfNotPresent:
imageRef, err = m.imageService.GetImageRef(ctx, *spec)
if err != nil {
msg = fmt.Sprintf("Failed to inspect image %q: %v", imageRef, err)
m.logIt(objRef, v1.EventTypeWarning, events.FailedToInspectImage, logPrefix, msg, klog.Warning)
return "", msg, ErrImageInspect
}
return imageRef, msg, nil
case v1.PullNever:
imageRef, err = m.imageService.GetImageRef(ctx, *spec)
if err != nil {
msg = fmt.Sprintf("Failed to inspect image %q: %v", imageRef, err)
m.logIt(objRef, v1.EventTypeWarning, events.FailedToInspectImage, logPrefix, msg, klog.Warning)
return "", msg, ErrImageInspect
}
if imageRef == "" {
msg = fmt.Sprintf("Container image %q is not present with pull policy of Never", imgRef)
m.logIt(objRef, v1.EventTypeWarning, events.ErrImageNeverPullPolicy, logPrefix, msg, klog.Warning)
return "", msg, ErrImageNeverPull
}
return imageRef, msg, nil
}
return
}If the image must be pulled, the manager calls the puller:
m.puller.pullImage(ctx, spec, pullSecrets, pullChan, podSandboxConfig)Parallel Image Puller
The parallel implementation limits concurrency with a token channel.
func (pip *parallelImagePuller) pullImage(ctx context.Context, spec kubecontainer.ImageSpec, pullSecrets []v1.Secret, pullChan chan<- pullResult, podSandboxConfig *runtimeapi.PodSandboxConfig) {
go func() {
if pip.tokens != nil {
// Acquire a token to limit concurrency.
pip.tokens <- struct{}{}
defer func() { <-pip.tokens }()
}
startTime := time.Now()
// Invoke the CRI image pull API.
imageRef, err := pip.imageService.PullImage(ctx, spec, pullSecrets, podSandboxConfig)
var size uint64
if err == nil && imageRef != "" {
// Retrieve image size; ignore errors.
size, _ = pip.imageService.GetImageSize(ctx, spec)
}
// Send result back on the channel.
pullChan <- pullResult{imageRef: imageRef, imageSize: size, err: err, pullDuration: time.Since(startTime)}
}()
}ImageGCManager Interface and Policy
type ImageGCManager interface {
GarbageCollect(ctx context.Context, beganGC time.Time) error
Start()
GetImageList() ([]container.Image, error)
DeleteUnusedImages(ctx context.Context) error
} type ImageGCPolicy struct {
// Trigger GC when usage exceeds this percent.
HighThresholdPercent int
// Do not trigger GC when usage is below this percent.
LowThresholdPercent int
// Minimum age before an image is considered for GC.
MinAge time.Duration
// Delete regardless of disk usage after this age.
MaxAge time.Duration
}Garbage Collection Execution Flow
Query all images and sort them by lastUsed timestamp in descending order.
Delete images whose lastUsed exceeds MaxAge.
Obtain filesystem image usage statistics via ImageFsStats.
Calculate the usage percentage; if it meets or exceeds HighThresholdPercent, free space to satisfy the policy.
// Step 1: list images in eviction order
images, err := im.imagesInEvictionOrder(ctx, freeTime)
// Step 2: free old images based on MaxAge
images, err = im.freeOldImages(ctx, images, freeTime, beganGC)
// Step 3: retrieve image filesystem stats
fsStats, _, err := im.statsProvider.ImageFsStats(ctx)
// Step 4: compute usage percent and trigger GC if needed
usagePercent := 100 - int(available*100/capacity)
if usagePercent >= im.policy.HighThresholdPercent {
freed, err := im.freeSpace(ctx, amountToFree, freeTime, images)
}The GC process balances image age, disk‑usage thresholds, and explicit policy limits to reclaim space on a node.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Infra Learning Club
Infra Learning Club shares study notes, cutting-edge technology, and career discussions.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
