<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Dockerd 資源泄露應(yīng)該如何應(yīng)對?

          共 2638字,需瀏覽 6分鐘

           ·

          2021-01-26 11:55


          點(diǎn)擊上方?藍(lán)字?關(guān)注我們!



          Java,Python,C/C++,Linux,PHP,Go,C#,QT,大數(shù)據(jù),算法,軟件教程,前端,簡歷,畢業(yè)設(shè)計(jì)等分類,資源在不斷更新中... 點(diǎn)擊領(lǐng)取

          每天 11 點(diǎn)更新文章,餓了點(diǎn)外賣,點(diǎn)擊 ??《無門檻外賣優(yōu)惠券,每天免費(fèi)領(lǐng)!》


          1. 現(xiàn)象

          線上 k8s 集群報(bào)警,宿主 fd 利用率超過 80%,登陸查看 dockerd 內(nèi)存使用 26G

          2. 排查思路

          由于之前已經(jīng)遇到過多次 dockerd 資源泄露的問題,先看是否是已知原因?qū)е碌模瑓⒖记懊鎯善?/p>

          3. fd 的對端是誰?

          執(zhí)行?ss -anp | grep dockerd,結(jié)果如下圖,可以看到和之前遇到的問題不同,第 8 列顯示為 0,與之前遇到的的情況不符,無法找到對端。

          4. 內(nèi)存為什么泄露?

          為了可以使用 pprof 分析內(nèi)存泄露位置,首先為 dockerd 打開 debug 模式,需要修改 service 文件,添加如下兩句

          ExecReload=/bin/kill?-s?HUP?$MAINPID
          KillMode=process

          同時(shí)在?/etc/docker/daemon.json?文件中添加?“debug”: true?的配置,修改完之后執(zhí)行?systemctl daemon-reload?重新加載 docker 服務(wù)配置,然后執(zhí)行?systemctl reload docker,重進(jìn)加載 docker 配置,開啟 debug 模式

          dockerd 默認(rèn)使用 uds 對未提供服務(wù),為了方便我們調(diào)試,可以使用 socat 對 docker 進(jìn)行端口轉(zhuǎn)發(fā),如下?sudo socat -d -d TCP-LISTEN:8080,fork,bind=0.0.0.0 UNIX:/var/run/docker.sock,意思是外部可以通過訪問宿主機(jī)的 8080 端口來調(diào)用 docker api,至此一切就緒

          在本地執(zhí)行?go tool pprof http://ip:8080/debug/pprof/heap?查看內(nèi)存使用情況,如下圖

          可以看到占用多的地方在 golang 自帶的 bufio?NewWriterSize?和?NewReaderSize?處,每次 http 調(diào)用都會(huì)都這里,也看出來有什么問題。


          5. Goroutine 也泄露?

          泄露位置

          通過內(nèi)存還是無法知道具體出問題的位置,問題不大,再看看 goroutine 的情況,直接在瀏覽器訪問?http://ip:8080/debug/pprof/goroutine?debug=1,如下圖

          一共?1572822?個(gè) goroutine,兩個(gè)大頭各占一半,各有?786212?個(gè)。看到這里基本就可以沿著文件行數(shù)去源碼中查看了,這里我們用的 docker 18.09.2 版本,把源碼切換到對應(yīng)版本下,通過查看源碼可以知道這兩大類的 goroutine 泄露的原因,dockerd 與 containerd 相關(guān)處理流程如下圖

          對應(yīng)上圖的話,goroutine?泄露是由上面最后 docker kill 時(shí)的?wait chan close?導(dǎo)致的,wait 的時(shí)候會(huì)啟動(dòng)另一個(gè) goroutine,每次 docker kill 都會(huì)造成這兩個(gè) goroutine 的泄露。對應(yīng)代碼如下

          //?Kill?forcefully?terminates?a?container.
          func?(daemon?*Daemon)?Kill(container?*containerpkg.Container)?error?{
          ???if?!container.IsRunning()?{
          ??????return?errNotRunning(container.ID)
          ???}

          ???//?1.?Send?SIGKILL
          ???if?err?:=?daemon.killPossiblyDeadProcess(container,?int(syscall.SIGKILL));?err?!=?nil?{
          ??????//?While?normally?we?might?"return?err"?here?we're?not?going?to
          ??????//?because?if?we?can't?stop?the?container?by?this?point?then
          ??????//?it's?probably?because?it's?already?stopped.?Meaning,?between
          ??????//?the?time?of?the?IsRunning()?call?above?and?now?it?stopped.
          ??????//?Also,?since?the?err?return?will?be?environment?specific?we?can't
          ??????//?look?for?any?particular?(common)?error?that?would?indicate
          ??????//?that?the?process?is?already?dead?vs?something?else?going?wrong.
          ??????//?So,?instead?we'll?give?it?up?to?2?more?seconds?to?complete?and?if
          ??????//?by?that?time?the?container?is?still?running,?then?the?error
          ??????//?we?got?is?probably?valid?and?so?we?return?it?to?the?caller.
          ??????if?isErrNoSuchProcess(err)?{
          ?????????return?nil
          ??????}

          ??????ctx,?cancel?:=?context.WithTimeout(context.Background(),?2*time.Second)
          ??????defer?cancel()

          ??????if?status?:=?<-container.Wait(ctx,?containerpkg.WaitConditionNotRunning);?status.Err()?!=?nil?{
          ?????????return?err
          ??????}
          ???}

          ???//?2.?Wait?for?the?process?to?die,?in?last?resort,?try?to?kill?the?process?directly
          ???if?err?:=?killProcessDirectly(container);?err?!=?nil?{
          ??????if?isErrNoSuchProcess(err)?{
          ?????????return?nil
          ??????}
          ??????return?err
          ???}

          ???//?Wait?for?exit?with?no?timeout.
          ???//?Ignore?returned?status.
          ???<-container.Wait(context.Background(),?containerpkg.WaitConditionNotRunning)

          ???return?nil
          }

          //?Wait?waits?until?the?container?is?in?a?certain?state?indicated?by?the?given
          //?condition.?A?context?must?be?used?for?cancelling?the?request,?controlling
          //?timeouts,?and?avoiding?goroutine?leaks.?Wait?must?be?called?without?holding
          //?the?state?lock.?Returns?a?channel?from?which?the?caller?will?receive?the
          //?result.?If?the?container?exited?on?its?own,?the?result's?Err()?method?will
          //?be?nil?and?its?ExitCode()?method?will?return?the?container's?exit?code,
          //?otherwise,?the?results?Err()?method?will?return?an?error?indicating?why?the
          //?wait?operation?failed.
          func?(s?*State)?Wait(ctx?context.Context,?condition?WaitCondition)?<-chan?StateStatus?{
          ???s.Lock()
          ???defer?s.Unlock()

          ???if?condition?==?WaitConditionNotRunning?&&?!s.Running?{
          ??????//?Buffer?so?we?can?put?it?in?the?channel?now.
          ??????resultC?:=?make(chan?StateStatus,?1)

          ??????//?Send?the?current?status.
          ??????resultC?<-?StateStatus{
          ?????????exitCode:?s.ExitCode(),
          ?????????err:??????s.Err(),
          ??????}

          ??????return?resultC
          ???}

          ???//?If?we?are?waiting?only?for?removal,?the?waitStop?channel?should
          ???//?remain?nil?and?block?forever.
          ???var?waitStop?chan?struct{}
          ???if?condition???????waitStop?=?s.waitStop
          ???}

          ???//?Always?wait?for?removal,?just?in?case?the?container?gets?removed
          ???//?while?it?is?still?in?a?"created"?state,?in?which?case?it?is?never
          ???//?actually?stopped.
          ???waitRemove?:=?s.waitRemove

          ???resultC?:=?make(chan?StateStatus)

          ???go?func()?{
          ??????select?{
          ??????case?<-ctx.Done():
          ?????????//?Context?timeout?or?cancellation.
          ?????????resultC?<-?StateStatus{
          ????????????exitCode:?-1,
          ????????????err:??????ctx.Err(),
          ?????????}
          ?????????return
          ??????case?<-waitStop:
          ??????case?<-waitRemove:
          ??????}

          ??????s.Lock()
          ??????result?:=?StateStatus{
          ?????????exitCode:?s.ExitCode(),
          ?????????err:??????s.Err(),
          ??????}
          ??????s.Unlock()

          ??????resultC?<-?result
          ???}()

          ???return?resultC
          }

          對照 goroutine 的圖片,兩個(gè) goroutine 分別走到了 Kill 最后一次的?container.Wait?處、Wait 的?select?處,正因?yàn)?Wait 方法的?select?一直不返回,導(dǎo)致?resultC?無數(shù)據(jù),外面也就無法從?container.Wait?返回的 chan 中讀到數(shù)據(jù),從而導(dǎo)致每次 docker stop 調(diào)用阻塞兩個(gè) goroutine。

          為什么泄露?

          為什么 select 一直不返回呢?可以看到 select 在等三個(gè) chan,任意一個(gè)有數(shù)據(jù)或者關(guān)閉都會(huì)返回

          1. ctx.Done():不返回是因?yàn)樽詈笠淮握{(diào)用 Wait 的時(shí)候傳入的是?context.Background()。這里其實(shí)也是 dockerd 對請求的處理方式,既然客戶端要?jiǎng)h除容器,那我就等著容器刪除,什么時(shí)間刪除什么時(shí)間退出,只要容器沒刪,就一直有個(gè) goroutine 在等待。
          2. waitStop?和?waitRemove:不返回是因?yàn)闆]收到 containerd 發(fā)來的 task exit 的信號,可以對照上圖看下,在收到 task exit 后才會(huì)關(guān)閉 chan。

          為什么沒收到 task exit 事件?

          問題逐漸明確,但還需要進(jìn)一步排查為什么沒有收到 task exit 的事件,兩種可能

          • 發(fā)出但沒收收到:這里首先想到的是之前騰訊遇到的一個(gè)問題,也是在 18 版本的 docker 上,processEvent?的 goroutine 異常退出了,導(dǎo)致無法接收到 containerd 發(fā)來的信號,參考這里[1]
          • 沒有發(fā)出

          首先看有沒有收到,還是看 goroutine 的內(nèi)容,如下圖,可以看到處理事件的?goroutine:processEventStream?和接收事件的?goroutine:Subscribe?都存在,可以排除第一種可能

          接著看第二種可能,根本沒發(fā)出 task exit 事件。經(jīng)過上面分析,已知存在 goroutine 泄露,且是通過 docker stop 引起的,所以可以肯定 kubelet 發(fā)起了刪除容器的請求,并且是在一直嘗試,要不然也不會(huì)一直泄露。那剩下唯一的問題就是找出來是在不斷的刪除哪個(gè)容器,又為什么刪不掉。其實(shí)這個(gè)時(shí)候,聰明的你們可能已經(jīng)想到容器里大概率是有 D 進(jìn)程了,所有即使發(fā)送 Kill 信號容器進(jìn)程無法正常退出。接下來就是去驗(yàn)證一下這個(gè)猜想,首先去找一下哪個(gè)容器出的問題,先看 Kubelet 日志和 docker 日志,如下

          好家伙,不止一個(gè)容器刪不掉。驗(yàn)證了確實(shí)在不斷刪除容器,但是刪不掉,接下來看下是不是有 D 進(jìn)程,如下

          確實(shí)容器內(nèi)有 D 進(jìn)程了,可以去宿主上看下,ps aux | awk ‘$8=“D”',特別多的 D 進(jìn)程。


          總結(jié)

          Kubelet 為了保證最終一致性,發(fā)現(xiàn)宿主上還有不應(yīng)該存在的容器就會(huì)一直不斷的去嘗試刪除,每次刪除都會(huì)調(diào)用 docker stop 的 api,與 dockerd 建立一個(gè) uds 連接,dockerd 刪除容器的時(shí)候會(huì)啟動(dòng)一個(gè) goroutine 通過 rpc 形式調(diào)用 containerd 來刪除容器并等待最終刪除完畢才返回,等待的過程中會(huì)另起一個(gè) goroutine 來獲取結(jié)果,然而 containerd 在調(diào)用 runc 去真正執(zhí)行刪除的時(shí)候因?yàn)槿萜鲀?nèi) D 進(jìn)程,無法刪除容器,導(dǎo)致沒有發(fā)出?task exit?信號,dockerd 的兩個(gè)相關(guān)的 goroutine 也就不會(huì)退出。整個(gè)過程不斷重復(fù),最終就導(dǎo)致 fd、內(nèi)存、goroutine 一步步的泄露,系統(tǒng)逐漸走向不可用。

          回過頭來想想,其實(shí) kubelet 本身的處理都沒有問題,kubelet 是為了確保一致性,要去刪除不應(yīng)該存在的容器,直到容器被徹底刪除,每次調(diào)用 docker api 都設(shè)置了 timeout。dockerd 的邏輯有待商榷,至少可以做一些改進(jìn),因?yàn)榭蛻舳苏埱髸r(shí)帶了 timeout,且 dockerd 后端在接收到 task exit 事件后是會(huì)去做 container remove 操作的,即使當(dāng)前沒有 docker stop 請求。所以可以考慮把最后傳入?context.Background()?的 Wait 函數(shù)調(diào)用去掉,當(dāng)前面帶超時(shí)的 Wait 返回后直接退出就可以,這樣就不會(huì)造成資源泄露了。

          往期推薦

          Dockerd 資源泄露怎么辦

          總結(jié)一波 Redis 面試題,趕緊收藏!

          如何從 100 億 URL 中找出相同的 URL?

          架構(gòu)圖,進(jìn)階必經(jīng)之路!


          看完文章,餓了點(diǎn)外賣,點(diǎn)擊 ??《無門檻外賣優(yōu)惠券,每天免費(fèi)領(lǐng)!》

          END



          若覺得文章對你有幫助,隨手轉(zhuǎn)發(fā)分享,也是我們繼續(xù)更新的動(dòng)力。


          長按二維碼,掃掃關(guān)注哦

          ?「C語言中文網(wǎng)」官方公眾號,關(guān)注手機(jī)閱讀教程??


          必備編程學(xué)習(xí)資料


          目前收集的資料包括:?Java,Python,C/C++,Linux,PHP,go,C#,QT,git/svn,人工智能,大數(shù)據(jù),單片機(jī),算法,小程序,易語言,安卓,ios,PPT,軟件教程,前端,軟件測試,簡歷,畢業(yè)設(shè)計(jì),公開課?等分類,資源在不斷更新中...


          點(diǎn)擊“閱讀原文”,立即免費(fèi)領(lǐng)取最新資料!
          ??????
          瀏覽 24
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲激情综合 | 青娱乐大香蕉 | 亚洲综合激情另类小说区 | 国产在线大香蕉 | np无码视频 |