<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 資源泄露怎么辦

          共 2638字,需瀏覽 6分鐘

           ·

          2021-01-25 11:48


          點擊上方?藍字?關注我們!



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

          每天 11 點更新文章,餓了點外賣,點擊 ??《無門檻外賣優(yōu)惠券,每天免費領!》


          1. 現(xiàn)象

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

          2. 排查思路

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

          3. fd 的對端是誰?

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

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

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

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

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

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

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

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


          5. Goroutine 也泄露?

          泄露位置

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

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

          對應上圖的話,goroutine?泄露是由上面最后 docker kill 時的?wait chan close?導致的,wait 的時候會啟動另一個 goroutine,每次 docker kill 都會造成這兩個 goroutine 的泄露。對應代碼如下

          //?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 的圖片,兩個 goroutine 分別走到了 Kill 最后一次的?container.Wait?處、Wait 的?select?處,正因為 Wait 方法的?select?一直不返回,導致?resultC?無數(shù)據(jù),外面也就無法從?container.Wait?返回的 chan 中讀到數(shù)據(jù),從而導致每次 docker stop 調(diào)用阻塞兩個 goroutine。

          為什么泄露?

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

          1. ctx.Done():不返回是因為最后一次調(diào)用 Wait 的時候傳入的是?context.Background()。這里其實也是 dockerd 對請求的處理方式,既然客戶端要刪除容器,那我就等著容器刪除,什么時間刪除什么時間退出,只要容器沒刪,就一直有個 goroutine 在等待。
          2. waitStop?和?waitRemove:不返回是因為沒收到 containerd 發(fā)來的 task exit 的信號,可以對照上圖看下,在收到 task exit 后才會關閉 chan。

          為什么沒收到 task exit 事件?

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

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

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

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

          好家伙,不止一個容器刪不掉。驗證了確實在不斷刪除容器,但是刪不掉,接下來看下是不是有 D 進程,如下

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


          總結

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

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



          往期推薦

          總結一波 Redis 面試題,趕緊收藏!

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

          架構圖,進階必經(jīng)之路!

          雙十一秒殺架構模型設計


          看完文章,餓了點外賣,點擊 ??《無門檻外賣優(yōu)惠券,每天免費領!》

          END



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


          長按二維碼,掃掃關注哦

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


          必備編程學習資料


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


          點擊“閱讀原文”,立即免費領取最新資料!
          ??????
          瀏覽 40
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <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>
                  91清清草视频 | 中文字幕不卡+婷婷五月 | 欧美精品一区二区婷婷 | 青娱乐免费在线观看视频 | 豆花传剧高清在线看 |