k8s 拉取鏡像等待時間過長原因分析

背景

Today 3:44 PM 有同事 反饋 k8s 拉取鏡像耗時很久溢吻,如下圖所示:


從 log 可以看出桩皿,拉取鏡像花費 2m8s撵渡,但是發(fā)起 Pulling 到 成功 pulled 鏡像中間間隔 42min勾哩,原因何在岔霸? 后面同事提供完整 log 截圖后發(fā)現(xiàn) waiting 時間就有 42m39s


代碼分析

由于生產(chǎn)環(huán)境中的 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 結(jié)束一共花費 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 參數(shù)來決定是否是序列化拉取還是并發(fā)拉仍岷伞(其實并發(fā)拉取并未正在實現(xiàn),只是簡單的起了一個 goroutine 來拉取鏡像纽帖,并沒有做并發(fā)限制闯狱,因此,如果同時拉取鏡像太多會對節(jié)點造成很大壓力)抛计,這個參數(shù)是由 kubelet 的 serializeImagePulls 來控制的,而/var/lib/kubelet/config.yamlserializeImagePulls 默認值為 true照筑。

serializeImagePulls: true

因此吹截,我們只關心 newSerialImagePuller 的實現(xiàn)過程。

// 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 在初始化時會設置最大拉取鏡像請求數(shù)的隊列凝危,puller 在收到拉取鏡像的請求后會先將此請求放入此隊列波俄,后臺依次從隊列中取出拉取鏡像請求并處理,這樣如果請求數(shù)過多蛾默,或者拉取鏡像比較耗時就會導致后面的拉取鏡像請求一直阻塞懦铺。到這里,就已經(jīng)清楚了為啥 waiting 時間會這么久支鸡。

如何解決

通過查看最新代碼冬念,發(fā)現(xiàn)已經(jīng)實現(xiàn)了并發(fā)拉取,只需要設置以下參數(shù)即可牧挣,其最低支持版本為 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急前,相當于控制同時拉取鏡像的并發(fā)數(shù),只有在獲取到 token 之后才進行鏡像的拉取摆出,以上面設置的值為例稍刀,則支持同時并發(fā)拉取 10 個鏡像,這樣大大緩解 waiting 時間過長的問題俭厚。

此 feature 對應的 enhancement 鏈接為 https://github.com/kubernetes/enhancements/issues/3673

結(jié)論

  1. 解決方案

升級 k8s 版本

參考鏈接

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末世吨,一起剝皮案震驚了整個濱河市澡刹,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌耘婚,老刑警劉巖罢浇,帶你破解...
    沈念sama閱讀 211,561評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異边篮,居然都是意外死亡己莺,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,218評論 3 385
  • 文/潘曉璐 我一進店門戈轿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來凌受,“玉大人,你說我怎么就攤上這事思杯∈を龋” “怎么了?”我有些...
    開封第一講書人閱讀 157,162評論 0 348
  • 文/不壞的土叔 我叫張陵色乾,是天一觀的道長誊册。 經(jīng)常有香客問我,道長暖璧,這世上最難降的妖魔是什么案怯? 我笑而不...
    開封第一講書人閱讀 56,470評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮澎办,結(jié)果婚禮上嘲碱,老公的妹妹穿的比我還像新娘。我一直安慰自己局蚀,他們只是感情好麦锯,可當我...
    茶點故事閱讀 65,550評論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著琅绅,像睡著了一般扶欣。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上千扶,一...
    開封第一講書人閱讀 49,806評論 1 290
  • 那天料祠,我揣著相機與錄音,去河邊找鬼县貌。 笑死术陶,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的煤痕。 我是一名探鬼主播梧宫,決...
    沈念sama閱讀 38,951評論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼接谨,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了塘匣?” 一聲冷哼從身側(cè)響起脓豪,我...
    開封第一講書人閱讀 37,712評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎忌卤,沒想到半個月后扫夜,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,166評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡驰徊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,510評論 2 327
  • 正文 我和宋清朗相戀三年笤闯,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片棍厂。...
    茶點故事閱讀 38,643評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡颗味,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出牺弹,到底是詐尸還是另有隱情浦马,我是刑警寧澤,帶...
    沈念sama閱讀 34,306評論 4 330
  • 正文 年R本政府宣布张漂,位于F島的核電站晶默,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏航攒。R本人自食惡果不足惜磺陡,卻給世界環(huán)境...
    茶點故事閱讀 39,930評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望漠畜。 院中可真熱鬧仅政,春花似錦、人聲如沸盆驹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,745評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽躯喇。三九已至,卻和暖如春硝枉,著一層夾襖步出監(jiān)牢的瞬間廉丽,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,983評論 1 266
  • 我被黑心中介騙來泰國打工妻味, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留正压,地道東北人。 一個月前我還...
    沈念sama閱讀 46,351評論 2 360
  • 正文 我出身青樓责球,卻偏偏與公主長得像焦履,于是被迫代替她去往敵國和親拓劝。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,509評論 2 348

推薦閱讀更多精彩內(nèi)容