成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

一次 Docker 容器內大量僵尸進程排查分析

安全
前段時間線上的一個使用 Google Puppeteer 生成圖片的服務炸了,每個 docker 容器內都有幾千個孤兒僵死進程沒有回收。

前段時間線上的一個使用 Google Puppeteer 生成圖片的服務炸了,每個 docker 容器內都有幾千個孤兒僵死進程沒有回收,如下圖所示。

這篇文章比較長,主要就講了下面這幾個問題。

  • 什么情況下會出現僵尸進程、孤兒進程
  • Puppeteer 工作過程啟動的進程與線上事故分析
  • PID 為 1 的進程有什么特殊的地方
  • 為什么 node/npm 不應該作為鏡像中 PID 為 1 的進程
  • 為什么 Bash 可以作為 PID 為 1 的進程,以及它做 PID 為 1 的進程有什么缺陷
  • 鏡像中比較推薦的 init 進程的做法是什么

Puppeteer 是一個 node 庫,是 Chrome 官方提供的無界面 chrome 工具(headless chrome),它提供了操作 Chrome API 的方式,允許開發者在程序中啟動 chrome 進程,調用 JS 的 API 實現頁面加載、數據爬取、web 自動化測試等功能。

本案例中使用的場景是使用 Puppeteer 加載 html,隨后截圖生成一張分銷海報的圖片。文章分析了這個問題背后的原因,接下來開始正式的內容。

進程

每個進程都有一個唯一的標識,稱為 pid,pid 是一個非負的整數值,使用 ps 命令可以查看,在我的 Mac 電腦上執行 ps -ef 可以看到當前運行的所有進程,如下所示。

 

  1. UID   PID  PPID   C STIME   TTY           TIME CMD 
  2.   0     1     0   0 六04下午 ??        23:09.18 /sbin/launchd 
  3.   0    39     1   0 六04下午 ??         0:49.66 /usr/sbin/syslogd 
  4.   0    40     1   0 六04下午 ??         0:13.00 /usr/libexec/UserEventAgent (System) 

其中 PID 是表示進程號。

系統中每個進程都有對應的父進程,上面 ps 輸出中的 PPID 就表示進程的父進程號。最頂層的進程的 PID 為 1,PPID 為 0。

打開 iTerm,在終端中執行一個命令,比如 "ls",實際上系統會創建新的 iTerm 子進程,這個 iTerm 進程又創建了 zsh 子進程。在 zsh 中輸入的 ls 命令,則是 zsh 進程又啟動了一個 ls 子進程。在 iTerm 中輸入 ls 命令過程的進程關系如下所示。

 

  1. UID   PID  PPID   C STIME   TTY           TIME CMD 
  2.  501   321     1   0 六04下午 ??        61:01.45 /Applications/iTerm.app/Contents/MacOS/iTerm2 -psn_0_81940 
  3.  501 97920   321   0  8:02上午 ttys039    0:00.07 /Applications/iTerm.app/Contents/MacOS/iTerm2 --server login -fp arthur 
  4.    0 97921 97920   0  8:02上午 ttys039    0:00.03 login -fp arthur 
  5.  501 97922 97921   0  8:02上午 ttys039    0:00.29 -zsh 
  6.  501 98369 97922   0  8:14上午 ttys039    0:00.00 ./a.out 

進程與 fork

前面提到的父進程“創建”子進程,更嚴謹的描述是 fork(孵化、衍生)。下面來看一個實際的例子,新建一個 fork_demo.c 文件。

 

  1. #include <unistd.h> 
  2. #include <stdio.h> 
  3.  
  4. int main() { 
  5.   int ret = fork(); 
  6.   if (ret) { 
  7.     printf("enter if block\n"); 
  8.   } else { 
  9.     printf("enter else block\n"); 
  10.   } 
  11.   return 0; 

執行上的代碼,會輸出如下的語句。

 

  1. enter if block 
  2. enter else block 

可以看到 if、else 語句都被執行了。

fork 調用

fork 是一個系統調用,它的方法聲明如下所示。

  1. pid_t fork(void); 

fork 調用完成后會生成一個新的子進程,且父子進程都從 fork 返回處繼續執行。這里需要特別注意的是 fork 的返回值的含義,在父進程和新的子進程中,它們的含義不一樣。

  • 在父進程中 fork 的返回值是新創建的子進程 id
  • 在創建的子進程中 fork 的返回值始終等于 0

因此可以通過 fork 的返回值區分父子進程,在運行過程中可以使用 getpid 方法獲取當前的進程 id。fork 典型的使用方式如下所示。

 

  1. #include <unistd.h> 
  2. #include <stdio.h> 
  3. #include <stdlib.h> 
  4.  
  5. int main() { 
  6.   printf("before fork, pid=%d\n", getpid()); 
  7.   pid_t childPid; 
  8.   switch (childPid = fork()) { 
  9.     case -1: { 
  10.       // fork 失敗 
  11.       printf("fork error, %d\n", getpid()); 
  12.       exit(1); 
  13.     } 
  14.     case 0: { 
  15.       // 子進程代碼進入到這里 
  16.       printf("in child process, pid=%d\n", getpid()); 
  17.       break; 
  18.     } 
  19.     default: { 
  20.       // 父進程代碼進入到這里 
  21.       printf("in parent process, pid=%d, child pid=%d\n", getpid(), childPid); 
  22.       break; 
  23.     } 
  24.   } 
  25.   return 0; 

執行上面的代碼,輸出結果如下所示。

 

  1. before fork, pid=26070 
  2. in parent process, pid=26070, child pid=26071 
  3. in child process, pid=26071 

子進程是父進程的副本,子進程擁有父進程數據空間、堆、棧的復制副本 ,fork 采用了 copy-on-write 技術,fork 操作幾乎瞬間可以完成。只有在子進程修改了相應的區域才會進行真正的拷貝。

孤兒進程:不能同年同月同日生,也不會同年同月同日死

接下來問一個問題,父進程掛掉時,子進程會掛掉嗎?

想象現實中的場景,父親不在了,兒子還可以活嗎?答案是肯定的。對應于進程,父進程退出時,子進程會繼續運行,不會一起共赴黃泉。

一個父進程已經終止的進程被稱為孤兒進程(orphan process)。操作系統這個大家長是比較人性化的,沒有人管的孤兒進程會被進程 ID 為 1 的進程接管。這個 PID 為 1 的進程后面還會再講到。

接下來對之前的代碼稍作修改,讓父進程 fork 子進程以后自殺退出,生成孤兒進程。代碼如下所示。

 

  1. #include <unistd.h> 
  2. #include <stdio.h> 
  3. #include <stdlib.h> 
  4.  
  5. int main() { 
  6.   printf("before fork, pid=%d\n", getpid()); 
  7.   pid_t childPid; 
  8.   switch (childPid = fork()) { 
  9.     case -1: { 
  10.       printf("fork error, %d\n", getpid()); 
  11.       exit(1); 
  12.     } 
  13.     case 0: { 
  14.       printf("in child process, pid=%d\n", getpid()); 
  15.       sleep(100000); // 子進程 sleep 不退出 
  16.       break; 
  17.     } 
  18.     default: { 
  19.       printf("in parent process, pid=%d, child pid=%d\n", getpid(), childPid); 
  20.       exit(0); // 父進程退出 
  21.     } 
  22.   } 
  23.   return 0; 

編譯運行上面的代碼

  1. gcc fork_demo.c -o fork_demo; ./fork_demo 

輸出結果如下。

 

  1. before fork, pid=21629 
  2. in parent process, pid=21629, child pid=21630 
  3. in child process, pid=21630 

可以看到父進程 id 為 21629, 生成的子進程 id 為 21630。

使用 ps 查看當前進程信息,結果如下所示。

 

  1. UID        PID  PPID  C STIME TTY          TIME CMD 
  2. root         1     0  0 12月12 ?      00:00:53 /usr/lib/systemd/systemd --system --deserialize 21 
  3. ya       21630     1  0 19:26 pts/8    00:00:00 ./fork_demo 

可以看到此時孤兒子進程 21630 的父 ID 已經變為了頂層的 ID 為 1 的進程。

僵尸進程

父進程負責生,如果不負責養,那就不是一個好父親。子進程掛了,如果父進程不給子進程“收尸”(調用 wait/waitpid),那這個子進程小可憐就變成了僵尸進程。

新建一個 make_zombie.c 文件,內容如下。

 

  1. #include <stdio.h> 
  2. #include <stdlib.h> 
  3. #include <unistd.h> 
  4.  
  5. int main() { 
  6.  
  7.   printf("pid %d\n", getpid()); 
  8.   int child_pid = fork(); 
  9.   if (child_pid == 0) { 
  10.     printf("-----in child process:  %d\n", getpid()); 
  11.     exit(0); 
  12.   } else { 
  13.     sleep(1000000); 
  14.   } 
  15.   return 0; 

編譯運行上面的代碼,就可以生成一個進程號為 22538 的僵尸進程,如下所示。

 

  1. UID        PID  PPID  C STIME TTY          TIME CMD 
  2. ya       22537 20759  0 19:57 pts/8    00:00:00 ./make_zombie 
  3. ya       22538 22537  0 19:57 pts/8    00:00:00 [make_zombie] <defunct> 

CMD 名中的 defunct 表示這是一個僵尸進程。

也使用 ps 命令查看進程的狀態,顯示為 "Z" 或者 "Z+" 表示這是一個僵尸進程,如下所示。

 

  1. ps -ho pid,state -p 22538 
  2. 22538 Z 

子進程退出后絕大部分資源已經被釋放可供其他進使用,但是內核的進程表中的槽位沒有釋放。

僵尸進程有一個很神奇的特性,使用 kill -9 必殺信號都沒有辦法殺掉僵尸進程,這樣的設計利弊參半,好的地方是父進程可以總是有機會執行 wait/waitpid 等命令收割子進程,壞的地方是無法強制回收這種僵尸進程。

PID 為 1 的進程

Linux 中內核初始化以后會啟動系統的第一個進程,PID 為 1,也可以稱之為 init 進程或者根(ROOT)進程。在我的 Centos 機器上,這個 init 進程是 systemd,如下所示。

 

  1. UID        PID  PPID  C STIME TTY          TIME CMD 
  2. root         1     0  0 12月12 ?      00:00:54 /usr/lib/systemd/systemd --system --deserialize 21 

在我的 Mac 電腦上,這個進程為 launchd,如下所示。

 

  1. UID   PID  PPID   C STIME   TTY           TIME CMD 
  2.   0     1     0   0 六04下午 ??        28:40.65 /sbin/launchd 

init 進程有下面這幾個功能

  • 如果一個進程的父進程退出了,那么這個 init 進程便會接管這個孤兒進程。
  • 如果一個進程的父進程未執行 wait/waitpid 就退出了,init 進程會接管子進程并自動調用 wait 方法,從而保證系統中的僵尸進程可以被移除。
  • 傳遞信號給子進程,這點后面會介紹。

為什么 Node.js 不適合做 Docker 鏡像中 PID 為 1 的進程

在 Node.js 的官方最佳實踐里有寫到 "Node.js was not designed to run as PID 1 which leads to unexpected behaviour when running inside of Docker."。下圖來自 github.com/nodejs/dock… 。

接下來會做兩個實驗:第一個實驗是在 Centos 機器上,第二個實驗是在 Docker 鏡像中

實驗一:在 Centos 上,systemd 作為 PID 為 1 的進程

下面來做一些測試,修改上面的代碼,將父進程 sleep 的時間改短為 15s,新建一個 make_zombie.c 文件,如下所示。

 

  1. #include <stdio.h> 
  2. #include <stdlib.h> 
  3. #include <unistd.h> 
  4.  
  5. int main() { 
  6.   printf("pid %d\n", getpid()); 
  7.   int child_pid = fork(); 
  8.   if (child_pid == 0) { 
  9.     printf("-----in child process:  %d\n", getpid()); 
  10.     exit(0); 
  11.   } else { 
  12.     sleep(15); 
  13.     exit(0); 
  14.   } 

編譯生成可執行文件 make_zombie。

  1. gcc make_zombie.c -o make_zombie 

然后新建一個 run.js 代碼,內部啟動一個進程運行 make_zombie,如下所示。

 

  1. const { spawn } = require('child_process'); 
  2. const cmd = spawn('./make_zombie'); 
  3. cmd.stdout.on('data', (data) => { 
  4.     console.log(`stdout: ${data}`); 
  5. }); 
  6.  
  7. cmd.stderr.on('data', (data) => { 
  8.     console.error(`stderr: ${data}`); 
  9. }); 
  10.  
  11. cmd.on('close', (code) => { 
  12.     console.log(`child process exited with code ${code}`); 
  13. }); 
  14. setTimeout(function () { 
  15.     console.log("..."); 
  16. }, 1000000); 

執行 node run.js 運行這段 js 代碼,使用 ps -ef 查看進程關系如下。

 

  1. UID        PID  PPID  C STIME TTY          TIME CMD 
  2. ya       19234 19231  0 12月20 ?       00:00:00 sshd: ya@pts/6 
  3. ya       19235 19234  0 12月20 pts/6   00:00:01 -zsh 
  4. ya       29513 19235  3 15:28 pts/6    00:00:00 node run.js 
  5. ya       29519 29513  0 15:28 pts/6    00:00:00 ./make_zombie 
  6. ya       29520 29519  0 15:28 pts/6    00:00:00 [make_zombie] <defunct> 

過 15s 以后,再次執行 ps -ef 查詢當前運行的進程,可以看到 make_zombie 相關進程都不見了。

 

  1. UID        PID  PPID  C STIME TTY          TIME CMD 
  2. ya       19234 19231  0 12月20 ?       00:00:00 sshd: ya@pts/6 
  3. ya       19235 19234  0 12月20 pts/6   00:00:01 -zsh 
  4. ya       29513 19235  3 15:28 pts/6    00:00:00 node run.js 

這是因為 PID 為 29519 的 make_zombie 父進程在 15s 以后退出,僵尸子進程被托管到 init 進程,這個進程會調用 wait/waitfor 為這個僵尸收尸。

實驗二:在 Docker 上,node 作為 PID 為 1 的進程

將 make_zombie 可執行文件和 run.js 打包為 .tar.gz 包,隨后新建一個 Dockerfile,內容如下。

 

  1. #指定基礎鏡像 
  2. FROM  registry.gz.cctv.cn/library/your_node_image:your_tag 
  3.  
  4. WORKDIR / 
  5.  
  6. #復制包文件到工作目錄,. 代表當前目錄,也就是工作目錄 
  7. ADD test.tar.gz . 
  8.  
  9. #指定啟動命令 
  10. CMD ["node""run.js"

執行 docker build 命令構建一個鏡像,在我的電腦上 Image ID 為 ab71925b5154, 執行 docker run ab71925b5154,啟動 docker 鏡像,使用 docker ps 找到鏡像 CONTAINER ID,這里為 e37f7e3c2e39。隨即使用 docker exec 進入到鏡像終端

  1. docker exec -it e37f7e3c2e39 /bin/bash 

執行 ps 命令查看當前的進程狀況,如下所示。

 

  1. UID        PID  PPID  C STIME TTY          TIME CMD 
  2. root         1     0  1 07:52 ?        00:00:00 node run.js 
  3. root        12     1  0 07:52 ?        00:00:00 ./make_zombie 
  4. root        13    12  0 07:52 ?        00:00:00 [make_zombie] <defunct> 

等一段時間(15s),再次執行 ps 查看當前進程,如下所示。

 

  1. UID        PID  PPID  C STIME TTY          TIME CMD 
  2. root         1     0  0 07:52 ?        00:00:00 node run.js 
  3. root        13     1  0 07:52 ?        00:00:00 [make_zombie] <defunct> 

可以看到 PID 為 13 的僵尸進程已經托管到 PID 為 1 的 node 進程,但是沒有被回收。

這是 node 不適合做 init 進程的最主要原因:無法回收僵尸進程。

說到 node,這里提一下 npm,npm 實際上是使用 npm 進程啟動了一個子進程啟動了 package.json 中 scripts 里寫的啟動腳本,示例 package.json 腳本如下所示。

 

  1.   "name""test-demo"
  2.   "version""1.0.0"
  3.   "description"""
  4.   "main""index.js"
  5.   "scripts": { 
  6.     "test""echo \"Error: no test specified\" && exit 1"
  7.     "start""node run.js" 
  8.   }, 
  9.   "keywords": [], 
  10.   "author"""
  11.   "license""ISC"
  12.   "dependencies": { 
  13.   } 

使用 npm run start 啟動,得到的進程如下所示。

 

  1. ya       19235 19234  0 12月20 pts/6  00:00:01 -zsh 
  2. ya       32252 19235  0 16:32 pts/6    00:00:00 npm 
  3. ya       32262 32252  0 16:32 pts/6    00:00:00 node run.js 

與 node 一樣,npm 也不會處理僵尸子進程回收。

線上問題分析

我們線上出問題的情況下使用 npm start 來啟動一個 Puppeteer 項目,每生成一次圖片便會創建 4 個 chrome 相關的進程,如下所示。

 

  1. └── chrome(1) 
  2.     ├── gpu-process(2) 
  3.     └── zygote(3) 
  4.         └── renderer(4) 

在圖片生成完成時,chrome 主進程退出,剩下的三個孤兒僵尸進程被托管到頂層 npm 進程下,但是 npm 進程無力回收,所有每生成一次圖片便會新增三個僵尸進程。在成千上萬次圖片生成以后,系統中就充滿了僵尸進程。

解決辦法

為了解決這個問題,不能讓 node/npm 成為 init 進程,讓有能力接管僵尸進程的服務成為 init 進程即可,有兩個解決辦法。

  • 使用 bash 啟動 node 或者 npm
  • 增加專門的 init 進程,比如 tini

解決方式一:使用 bash 啟動 node

讓 bash 成為頂層進程是比較快的一種方式,bash 進程會負責回收僵尸進程,修改 Dockerfile,如下所示。

 

  1. ADD test.tar.gz . 
  2. # CMD ["npm""run""start"
  3. CMD ["/bin/bash""-c""set -e && npm run start"

使用這種方式是比較簡單,而且之前線上沒有出問題正是因為一開始是使用這種 bash 方式啟動 node,后面有一個小兄弟為了統一啟動命令將這個命令改為 npm run start,問題才出現的。

但使用 bash 并非完美的方案,它有一個比較嚴重的問題,bash 不會傳遞信號給它啟動的進程,優雅停機等功能無法實現。

接下來做一個實驗,驗證 bash 不會傳遞信號給子進程的說法,新建一個 signal_test.c 文件,它處理 SIGQUIT、SIGTERM、SIGTERM 三個信號,內容如下。

 

  1. #include <signal.h> 
  2. #include <stdio.h> 
  3.  
  4. static void signal_handler(int signal_no) { 
  5.   if (signal_no == SIGQUIT) { 
  6.     printf("quit signal receive: %d\n", signal_no); 
  7.   } else if (signal_no == SIGTERM) { 
  8.     printf("term signal receive: %d\n", signal_no); 
  9.   } else if (signal_no == SIGTERM) { 
  10.     printf("interrupt signal receive: %d\n", signal_no); 
  11.   } 
  12.  
  13. int main() { 
  14.   printf("in main\n"); 
  15.  
  16.   signal(SIGQUIT, signal_handler); 
  17.   signal(SIGINT, signal_handler); 
  18.   signal(SIGTERM, signal_handler); 
  19.  
  20.   getchar(); 

在我 Centos 和 Mac 上運行這個 signal_test 程序時,發送 kill -2、-3、-15 給這個程序,都會有對應的打印輸出,表示收到了信號。如下所示。

 

  1. kill -15 47120 
  2. term signal receive: 15 
  3.  
  4. kill -3 47120 
  5. quit signal receive: 3 
  6.  
  7. kill -2 47120 
  8. interrupt signal receive: 2 

在 Docker 鏡像中使用 bash 啟動這個程序時,發送 kill 命令給 bash 以后,bash 并不會將信號傳遞給 signal_test 程序。在執行 docker stop 以后,docker 會發送 SIGTERM(15) 信號給 bash,bash 并不會將這個信號傳遞給啟動的應用程序,只能等一段時間超時,docker 會發送 kill -9 強制殺死這個 docker 進程,無法達到優雅停機的功能。

于是有了下面的第二種解決方案。

解決方式二:使用專門的 init 進程

Node.js 提供了兩種方案,第一種是使用 docker 官方的輕量級 init 系統,如下所示。

  1. docker run -it --init you_docker_image_id 

這種啟動方式會以 /sbin/docker-init 為 PID 為 1 的 init 進程,不會把 Dockerfile 中 CMD 作為第一個啟動進程。

以下面的 Dockerfile 內容為例

 

  1. ... 
  2. CMD ["./signal_test"
  3. ... 

執行 docker run -it --init image_id 啟動 docker 鏡像,此時鏡像內的進程如下所示。

 

  1. UID        PID  PPID  C STIME TTY          TIME CMD 
  2. root         1     0  0 15:30 pts/0    00:00:00 /sbin/docker-init -- /app/node-default 
  3. root         6     1  0 15:30 pts/0    00:00:00 ./signal_test 

可以看到 signal_test 程序作為 docker-init 的子進程啟動了。

在 docker stop 命令發送 SIGTERM 信號給鏡像以后,docker-init 進程會將這個信號轉給 signal_test,這個應用進程就可以收到 SIGTERM 信號做自定義的處理,比如優雅停機等。

除了 docker 的官方方案,Node.js 的最佳實踐還推薦了一個 tini 這樣一個 C 語言寫的極小的 init 進程,github.com/krallin/tin… 。它的代碼較短,很值得一讀,對理解信號傳遞、處理僵尸進程非常有幫助。

小結

通過這篇文章,希望你可以搞懂僵尸進程、孤兒進程、PID 為 1 的進程是什么,以及為什么 node/npm 不適合做 PID 為 1 的進程,bash 作為 PID 為 1 的進程有什么缺陷。

下面留一個作業題,考考你對進程 fork 函數的理解。如下程序連續調用三次 fork() 調用后會產生多少新進程?

 

  1. #include <stdio.h> 
  2. #include <unistd.h> 
  3.  
  4. int main() { 
  5.   printf("Hello, World!\n"); 
  6.  
  7.   fork(); 
  8.   fork(); 
  9.   fork(); 
  10.   sleep(100); 
  11.   return 0; 

 

責任編輯:未麗燕 來源: 今日頭條
相關推薦

2018-01-19 11:12:11

HTTP問題排查

2022-02-08 17:17:27

內存泄漏排查

2025-03-17 10:01:07

2023-04-06 07:53:56

Redis連接問題K8s

2018-07-12 10:33:50

Docker容器內存

2017-12-19 14:00:16

數據庫MySQL死鎖排查

2019-03-15 16:20:45

MySQL死鎖排查命令

2021-05-13 08:51:20

GC問題排查

2019-09-10 10:31:10

JVM排查解決

2021-03-29 12:35:04

Kubernetes環境TCP

2022-11-03 16:10:29

groovyfullGC

2023-01-04 18:32:31

線上服務代碼

2019-01-21 11:17:13

CPU優化定位

2021-09-14 13:25:23

容器pod僵尸進程

2011-04-07 11:20:21

SQLServer

2018-12-06 16:01:01

Redis遷移容器數據庫

2018-07-20 08:44:21

Redis內存排查

2021-11-23 21:21:07

線上排查服務

2020-11-02 09:48:35

C++泄漏代碼

2022-10-10 09:10:07

命令磁盤排查
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 亚洲社区在线 | 国产伦精品一区二区三区视频金莲 | 久久一视频 | 久久精品99国产精品 | 久久精品日 | 8x国产精品视频一区二区 | 亚洲伦理自拍 | 91免费在线视频 | www.久草.com | 91在线资源 | 国产精品久久国产精品 | 国产在线中文 | 亚洲欧洲精品一区 | 国产综合区 | 99re热精品视频国产免费 | 天天天天天操 | 欧洲精品在线观看 | 欧产日产国产精品视频 | 日韩中文在线 | 激情久久久久 | 第四色播日韩第一页 | 毛片一区二区三区 | 台湾佬成人网 | 日韩欧美在线播放 | 黄色一级片aaa | 国产日韩欧美在线观看 | 精品成人佐山爱一区二区 | 北条麻妃99精品青青久久主播 | 欧美一区二区三区高清视频 | 欧美一区二区在线视频 | 中文字幕国产精品 | 一区二区三区av | 免费日韩av| 精品伊人 | 国产精品不卡一区二区三区 | 一区二区三区免费 | 国产一级在线 | 狠狠狠色丁香婷婷综合久久五月 | 国产亚洲精品美女久久久久久久久久 | 成人精品视频在线观看 | 视频在线观看亚洲 |