Stories

Detail Return Return

一次容器裏的殭屍進程排查 - Stories Detail

背景

“大棟老師”的一個應用,經常會有殭屍進程產生。程序的調用邏輯大概如下:
主進程A產生多個B類進程B1,B2,B3等,每一個B類進程又產生了若干個C類進程,C1,C2,C3,現象就是容器中會出現部分C進程的殭屍進程。
經過簡單的分析發現是一些B類進程先結束,導致一些C類進程成為殭屍進程。但是這個不符合常規的邏輯,因為正常情況下父進程如果結束,子進程會成為孤兒進程,從而被內核的1號進程接管,結束之後自然被清理了,理論上不會成為殭屍進程。“大鵬老師”提出,上面的邏輯是在傳統的操作系統上,容器會不會有一些特殊呢。我們帶着這個猜想,進入驗證階段。

模擬以及驗證

簡單模擬單條鏈路A--->B--->C的情況,看看是否能復現。

callmain作為上面描述的A進程,代碼如下

package main

import (
    "context"
    "os/exec"
    "time"
)

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    cmd := exec.CommandContext(ctx, "bash", "-c", " ./main")
    err := cmd.Run()
    if err != nil {
        fmt.Println(err)
    }
    time.Sleep(5 * time.Hour)
}

main作為上面描述的B進程,代碼如下

package main

import (
    "context"
    "os/exec"
)

func main() {
    cmd := exec.CommandContext(context.Background(), "bash", "-c", "sleep 100")
    err := cmd.Run()
    if err != nil {
        panic(err)
    }
}

簡單描述一下上面的邏輯,callmain裏面調用編譯好的main執行文件,main執行文件裏面則是調用shell命令阻塞100秒,這樣main(B)結束的時候,
sleep進程(C)仍在繼續。把兩個可執行文件放入容器的同一目錄,如下如

image.png

然後執行 ./callmain
ps -ef 結果如下,進程父子關係符合預期

image.png

30s之後,這裏我們發現sleep進程已經最為孤兒進程被1號進程作為子進程接管,如圖

image.png

100s之後,如下圖,sleep進程已經正常結束釋放資源,並沒有成為所謂的殭屍進程
image.png

質疑與新的猜測

上面基於容器模擬生產類似的場景,但是卻沒有復現。是因為只是單條鏈路,進程不夠多?還是因為模擬的時候邏輯沒梳理清楚相似度不夠?
這時候“大鵬老師”又站出來了,他提出,我們生產環境的entrypoint就是直接啓動進程A,模擬的場景則是在bash裏面手動啓動的。有道理哦,似乎接近真相了。

再次驗證

為了方便我們需要在callmain裏面使用絕對路徑調用main執行文件
所以略微修改一下callmain的代碼

    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    cmd := exec.CommandContext(ctx, "bash", "-c", " /home/rain/shell/main")
    err := cmd.Run()
    if err != nil {
        fmt.Println(err)
    }
    time.Sleep(5 * time.Hour)

執行下面的命令

docker run -v C:\work\go\code\first\test:/home/rain --entrypoint /home/rain/shell/callmain centos:7

進入容器如圖

image.png

main結束後sleep被1號進程接管

image.png

但是當100秒過去,sleep應該結束的時候,如圖,卻沒有釋放資源,成為了殭屍進程
image.png

結論

在容器裏面,自定義的進程作為entrypoint啓動時,它是1號進程,它不具備waitpid回收由它接管的孤兒進程資源的能力

解決方案

既然有這個問題,肯定有相應的處理辦法。大棟老師心生一計:既然go進程作為1號進程沒有這個能力,那我們套一層bash呢?我們感覺應該可行,話不多説,直接進入驗證。

驗證解決方案

我們引入一個簡單的shell腳本,內容如下:

#! /usr/bin/bash
/home/rain/shell/callmain

執行

docker run -v C:\work\go\code\first\test:/home/rain --entrypoint /home/rain/shell/shell.sh centos:7

未到30s,進程父子關係符合預期:
image.png

30s之後,發現sleep成功被1號進程接管

image.png

100s之後,發現sleep正常退出
image.png

結果符合預期,所以這個方案是可行的

其他思考

在上面的例子中,我們進程的父子關係最多產生了四級,拿最後一個例子來説,從父到子依次是
為了方便描述,我們簡單編了個號
1./usr/bin/bash /home/rain/shell/shell.sh
2./home/rain/shell/callmain
3./home/rain/shell/main
4.sleep 100
當進程3運行超過30s收到結束信號結束後,進程4仍然在運行,4作為孤兒進程託管給了進程1直到正常結束回收資源。
其實在有一些場景下,我們更多的是期望進程3結束了,4也不需要再運行了,因為繼續運行在一些場景下會有進程泄漏,其實這些進程並沒有其他進程在等待它的結果,它在運行會造成沒必要的資源佔用和浪費。因此,我們會有一個需要是,當一個進程退出了,它的產生的子進程、孫子進程乃至後面的子子孫孫都需要退出。

基於上面的需求,golang有一個通用的解決方案,我們還是修改callmain代碼

    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    cmd := exec.CommandContext(ctx, "/home/rain/shell/main")
    cmd.SysProcAttr = &syscall.SysProcAttr{
        Setpgid: true,
    }
    cmd.Cancel = func() error {
        syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
        return nil
    }
    err := cmd.Run()
    if err != nil {
        fmt.Println(err)
    }
    time.Sleep(5 * time.Hour)

上面的代碼含義是,在進程2啓動進程3的時候,設置進程3的進程組id為其進程id,這樣進程3產生的子孫進程都會使用這個id作為及進程組id,這樣在進程3結束的時候,只需要執行系統調用kill -9 -pid就能殺死整個進程組。下面會進行驗證。

結束子孫進程方法驗證

還是執行命令
docker run -v C:\work\go\code\first\test:/home/rain --entrypoint /home/rain/shell/shell.sh centos:7
開始的情況符合預期

image.png

30s之後再查看,發現sleep進程也被結束了,方案可行

image.png

總結

  • 經過分析、猜想以及實驗驗證,我們發現,如果entrypoint是一個golang的可執行文件(entrypoint執行的命令就是容器裏面的1號進程),那這個進程啓動之後是不具備傳統操作系統中的1號進程的對孤兒進程waitpid、等孤兒進程結束回收其資源的能力的。後面我們會繼續研究,為什麼golang執行文件不具備這個能力,bash就可以。
  • 另外我們順便也驗證了在golang中結束整個進程樹的方案。
user avatar xiuji Avatar soroqer Avatar u_15700751 Avatar hppyvyv6 Avatar meiduyandechengzi Avatar yqyx36 Avatar tim_xiao Avatar crossoverjie Avatar duiniwukenaihe_60e4196de52b7 Avatar kubeexplorer Avatar yian Avatar congjunhua Avatar
Favorites 33 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.