k8s 拉取镜像等待时间过长原因分析

背景

Today 3:44 PM 有同事 反馈 k8s 拉取镜像耗时很久,如下图所示:


从 log 可以看出,拉取镜像花费 2m8s,但是发起 Pulling 到 成功 pulled 镜像中间间隔 42min,原因何在? 后面同事提供完整 log 截图后发现 waiting 时间就有 42m39s


代码分析

由于生产环境中的 k8s 版本为 v1.23.17, 因此我们基于此分支代码进行分析,进而寻求解决方案。

// EnsureImageExists pulls the image for the specified pod and container, and returns                   
// (imageRef, error message, error).                                                                                
func (m *imageManager) EnsureImageExists(pod *v1.Pod, container *v1.Container, pullSecrets []v1.Secret, podSandboxConfig *runtimeapi.PodSandboxConfig) (string, string, error) {

    ..........

    m.logIt(ref, v1.EventTypeNormal, events.PullingImage, logPrefix, fmt.Sprintf("Pulling image %q", container.Image), klog.Info)    
    startTime := time.Now()                                                                                                                  
    pullChan := make(chan pullResult)                                                                   
    m.puller.pullImage(spec, pullSecrets, pullChan, podSandboxConfig)                                   
    imagePullResult := <-pullChan                                                                       
    if imagePullResult.err != nil {                                                                     
        m.logIt(ref, v1.EventTypeWarning, events.FailedToPullImage, logPrefix, fmt.Sprintf("Failed to pull image %q: %v", container.Image, imagePullResult.err), klog.Warning)
        m.backOff.Next(backOffKey, m.backOff.Clock.Now())                                               
        if imagePullResult.err == ErrRegistryUnavailable {                                              
            msg := fmt.Sprintf("image pull failed for %s because the registry is unavailable.", container.Image)    
            return "", msg, imagePullResult.err                                                         
        }                                                                                               

        return "", imagePullResult.err.Error(), ErrImagePull                                            
    }                                                                                                   
    m.logIt(ref, v1.EventTypeNormal, events.PulledImage, logPrefix, fmt.Sprintf("Successfully pulled image %q in %v (%v including waiting)", container.Image, imagePullResult.pullDuration, time.Since(startTime)), klog.Info)
    m.backOff.GC()                                                                                      
    return imagePullResult.imageRef, "", nil                                                            
}

从代码(第22行)可以看出,从开启 pulling 到 pulled 结束一共花费 42m39s,实际镜像拉取时间为 2m8s,因此可以排除 Harbor 的原因。下面从 ImageManager 的初始化开始分析拉取镜像的流程。

// NewImageManager instantiates a new ImageManager object.                                          
func NewImageManager(recorder record.EventRecorder, imageService kubecontainer.ImageService, imageBackOff *flowcontrol.Backoff, serialized bool, qps float32, burst int) ImageManager {
    imageService = throttleImagePulling(imageService, qps, burst)                                   

    var puller imagePuller                                                                          
    if serialized {                                                                                 
        puller = newSerialImagePuller(imageService)                                                 
    } else {
        puller = newParallelImagePuller(imageService)                                                                                                                                      puller = newParallelImagePuller(imageService)                                               
    }                                                                                               
    return &imageManager{                                                                           
        recorder:     recorder,                                                                     
        imageService: imageService,                                                                 
        backOff:      imageBackOff,                                                                 
        puller:       puller,                                                                       
    }                                                                                               
}

从上面代码可以看出,初始化 ImageManager时通过指定 serialized 参数来决定是否是序列化拉取还是并发拉取(其实并发拉取并未正在实现,只是简单的起了一个 goroutine 来拉取镜像,并没有做并发限制,因此,如果同时拉取镜像太多会对节点造成很大压力),这个参数是由 kubelet 的 serializeImagePulls 来控制的,而/var/lib/kubelet/config.yamlserializeImagePulls 默认值为 true。

serializeImagePulls: true

因此,我们只关心 newSerialImagePuller 的实现过程。

// Maximum number of image pull requests than can be queued.                                                                                 
const maxImagePullRequests = 10                                                                         

type serialImagePuller struct {                                                                         
    imageService kubecontainer.ImageService                                                             
    pullRequests chan *imagePullRequest                                                                 
}                                                                                                       

func newSerialImagePuller(imageService kubecontainer.ImageService) imagePuller {                        
    imagePuller := &serialImagePuller{imageService, make(chan *imagePullRequest, maxImagePullRequests)}    
    go wait.Until(imagePuller.processImagePullRequests, time.Second, wait.NeverStop)                    
    return imagePuller                                                                                  
}                                                                                                       

type imagePullRequest struct {                                                                          
    spec             kubecontainer.ImageSpec                                                            
    pullSecrets      []v1.Secret                                                                        
    pullChan         chan<- pullResult                                                                  
    podSandboxConfig *runtimeapi.PodSandboxConfig                                                       
}                                                                                                       

func (sip *serialImagePuller) pullImage(spec kubecontainer.ImageSpec, pullSecrets []v1.Secret, pullChan chan<- pullResult, podSandboxConfig *runtimeapi.PodSandboxConfig) {
    sip.pullRequests <- &imagePullRequest{                                                              
        spec:             spec,                                                                         
        pullSecrets:      pullSecrets,                                                                  
        pullChan:         pullChan,                                                                     
        podSandboxConfig: podSandboxConfig,                                                             
    }                                                                                                   
}

func (sip *serialImagePuller) processImagePullRequests() {                                              
    for pullRequest := range sip.pullRequests {                                                         
        startTime := time.Now()                                                                         
        imageRef, err := sip.imageService.PullImage(pullRequest.spec, pullRequest.pullSecrets, pullRequest.podSandboxConfig)
        pullRequest.pullChan <- pullResult{                                                         
            imageRef:     imageRef,                                                                 
            err:          err,                                                                      
            pullDuration: time.Since(startTime),                                                    
        }                                                                                           
    }                                                                                               
} 

serialImagePuller 在初始化时会设置最大拉取镜像请求数的队列,puller 在收到拉取镜像的请求后会先将此请求放入此队列,后台依次从队列中取出拉取镜像请求并处理,这样如果请求数过多,或者拉取镜像比较耗时就会导致后面的拉取镜像请求一直阻塞。到这里,就已经清楚了为啥 waiting 时间会这么久。

如何解决

通过查看最新代码,发现已经实现了并发拉取,只需要设置以下参数即可,其最低支持版本为 v1.27

# Enable parallel image pulls
serializeImagePulls: false
# limit the number of parallel image pulls
maxParallelImagePulls: 10
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 {                                                                          
            pip.tokens <- struct{}{}                                                                    
            defer func() { <-pip.tokens }()                                                             
        }                                                                                               
        startTime := time.Now()                                                                         
        imageRef, err := pip.imageService.PullImage(ctx, spec, pullSecrets, podSandboxConfig)           
        var size uint64                                                                                 
        if err == nil && imageRef != "" {                                                               
            // Getting the image size with best effort, ignoring the error.                             
            size, _ = pip.imageService.GetImageSize(ctx, spec)                                          
        }                                                                                               
        pullChan <- pullResult{                                                                         
            imageRef:     imageRef,                                                                     
            imageSize:    size,                                                                         
            err:          err,                                                                          
            pullDuration: time.Since(startTime),                                                        
        }                                                                                               
    }()                                                                                                 
} 

parallelImagePuller 在拉取镜像时会先获取 token,相当于控制同时拉取镜像的并发数,只有在获取到 token 之后才进行镜像的拉取,以上面设置的值为例,则支持同时并发拉取 10 个镜像,这样大大缓解 waiting 时间过长的问题。

此 feature 对应的 enhancement 链接为 https://github.com/kubernetes/enhancements/issues/3673

结论

  1. 解决方案

升级 k8s 版本

参考链接

https://medium.com/@shahneel2409/kubernetes-parallel-image-pulls-a-game-changer-for-large-scale-clusters-46174ab340b1

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容