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

Go Cmd 服務無法退出的小坑

開發 后端
上家公司的案例。先說下使用背景,服務在每臺服務器上啟動 agent, 用戶會在指定機器上執行任務,并將結果返回到網頁上。執行任務由用戶自定義腳本,一般也都是 shell 或是 python,會不斷的產生子進程,孫進程,直到執行完畢或是超時被 kill。

[[409900]]

本文轉載自微信公眾號「董澤潤的技術筆記」,作者董澤潤。轉載本文請聯系董澤潤的技術筆記公眾號。

上家公司的案例。先說下使用背景,服務在每臺服務器上啟動 agent, 用戶會在指定機器上執行任務,并將結果返回到網頁上。執行任務由用戶自定義腳本,一般也都是 shell 或是 python,會不斷的產生子進程,孫進程,直到執行完畢或是超時被 kill。

問題

最近發現經常有任務,一直處于運行中,但實際上己經超時被 kill,并未將輸出寫到系統,看不到任務的執行情況

登錄機器,發現執行腳本進程己經殺掉,但是有子腳本卡在某個 http 調用。再看下這個腳本,python requests 默認沒有設置超時...

總結一下現象:agent 用 go cmd 啟動子進程,子進程還會啟動孫進程,孫進程因某種原因阻塞。此時,如果子進程因超時被 agent kill 殺掉, agent 卻仍然處于 wait 狀態

復現

環境是 go version go1.16.5 linux/amd64, agent 使用 exec.CommandContext 啟動任務,設置 ctx 超時 30s,并將結果寫到 bytes.Buffer, 最后打印。簡化例子如下:

  1. ~/zerun.dong/code/gotest# cat wait.go 
  2. package main 
  3.  
  4. import ( 
  5.     "bytes" 
  6.     "context" 
  7.     "fmt" 
  8.     "os/exec" 
  9.     "time" 
  10.  
  11. func main() { 
  12.     ctx, cancelFn := context.WithTimeout(context.Background(), time.Second*30) 
  13.     defer cancelFn() 
  14.     cmd := exec.CommandContext(ctx, "./sleep.sh"
  15.     var b bytes.Buffer 
  16.     cmd.Stdout = &b //劇透,坑在這里 
  17.     cmd.Stderr = &b 
  18.     cmd.Start() 
  19.     cmd.Wait() 
  20.     fmt.Println("recive: ", b.String()) 

這個是 sleep.sh,模擬子進程

  1. #!/bin/sh 
  2. echo "in sleep" 
  3. sh ./sleep1.sh 

這是 sleep1.sh 模擬孫進程,sleep 1000 阻塞在這里

  1. #!/bin/sh 
  2. sleep 1000 

###現象 啟動測試 wait 程序,查看 ps axjf | less查看

  1. ppid  pid   pgid 
  2.  2468 32690 32690 32690 ?           -1 Ss       0   0:00  \_ sshd: root@pts/6 
  3. 32690 32818 32818 32818 pts/6    28746 Ss       0   0:00  |   \_ -bash 
  4. 32818 28531 28531 32818 pts/6    28746 S        0   0:00  |       \_ strace ./wait 
  5. 28531 28543 28531 32818 pts/6    28746 Sl       0   0:00  |       |   \_ ./wait 
  6. 28543 28559 28531 32818 pts/6    28746 S        0   0:00  |       |       \_ /bin/sh /root/dongzerun/sleep.sh 
  7. 28559 28560 28531 32818 pts/6    28746 S        0   0:00  |       |           \_ sh /root/dongzerun/sleep1.sh 
  8. 28560 28563 28531 32818 pts/6    28746 S        0   0:00  |       |               \_ sleep 1000 

等過了 30s,通過 ps axjf | less 查看

  1.  2468 32690 32690 32690 ?           -1 Ss       0   0:00  \_ sshd: root@pts/6 
  2. 32690 32818 32818 32818 pts/6    36192 Ss       0   0:00  |   \_ -bash 
  3. 32818 28531 28531 32818 pts/6    36192 S        0   0:00  |       \_ strace ./wait 
  4. 28531 28543 28531 32818 pts/6    36192 Sl       0   0:00  |       |   \_ ./wait 
  5.     1 28560 28531 32818 pts/6    36192 S        0   0:00 sh /root/dongzerun/sleep1.sh 
  6. 28560 28563 28531 32818 pts/6    36192 S        0   0:00  \_ sleep 1000 

通過上面的 case,可以看到 sleep1.sh 成了孤兒進程,被 init 1 認領,但是 28543 wait 并沒有退出,那他在做什么???

分析

這個時候僵住了,祭出我們的 strace 大法,查看 wait 程序

  1. epoll_ctl(4, EPOLL_CTL_DEL, 6, {0, {u32=0, u64=0}}) = 0 
  2. close(6)                                = 0 
  3. futex(0xc420054938, FUTEX_WAKE, 1)      = 1 
  4. waitid(P_PID, 28559, {si_signo=SIGCHLD, si_code=CLD_KILLED, si_pid=28559, si_status=SIGKILL, si_utime=0, si_stime=0}, WEXITED|WNOWAIT, NULL) = 0 
  5. 卡在這里約 30s 
  6. --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_KILLED, si_pid=28559, si_status=SIGKILL, si_utime=0, si_stime=0} --- 
  7. rt_sigreturn()                          = 0 
  8. futex(0x9a0378, FUTEX_WAKE, 1)          = 1 
  9. futex(0x9a02b0, FUTEX_WAKE, 1)          = 1 
  10. wait4(28559, [{WIFSIGNALED(s) && WTERMSIG(s) == SIGKILL}], 0, {ru_utime={0, 0}, ru_stime={0, 0}, ...}) = 28559 
  11. futex(0x9a0b78, FUTEX_WAIT, 0, NULL 

通過 go 源碼可以看到 go exec wait 時,會先執行 waitid, 阻塞在這里,然后再來一次 wait4 等待最終退出結果

不太明白為什么兩次 wait... 但是最后卡在了 futex 這里,看著像在等待什么資源???

打開 golang pprof, 再次運行程序,并 pprof

  1. go func() { 
  2.  err := http.ListenAndServe(":6060", nil) 
  3.  if err != nil { 
  4.   fmt.Printf("failed to start pprof monitor:%s", err) 
  5.  } 
  6. }() 
  1. curl http://127.0.0.1:6060/debug/pprof/goroutine?debug=2 
  2.  
  3. goroutine 1 [chan receive]: 
  4. os/exec.(*Cmd).Wait(0xc42017a000, 0x7c3d40, 0x0) 
  5.  /usr/local/go/src/os/exec/exec.go:454 +0x135 
  6. main.main() 
  7.  /root/dongzerun/wait.go:32 +0x167 

程序沒有退出,并不可思議的卡在了 exec.go:454 行代碼,查看源碼:

  1. // Wait releases any resources associated with the Cmd. 
  2. func (c *Cmd) Wait() error { 
  3.       ...... 
  4.  state, err := c.Process.Wait() 
  5.  if c.waitDone != nil { 
  6.   close(c.waitDone) 
  7.  } 
  8.  c.ProcessState = state 
  9.  
  10.  var copyError error 
  11.  for range c.goroutine { 
  12.         //卡在了這里 
  13.   if err := <-c.errch; err != nil && copyError == nil { 
  14.    copyError = err 
  15.   } 
  16.  } 
  17.  
  18.  c.closeDescriptors(c.closeAfterWait) 
  19.     ...... 
  20.  return copyError 

通過源代碼分析,程序 wait 卡在了 <-c.errch 獲取 chan 數據。那么 errch 是如何生成的呢?

查看 cmd.Start 源碼,go 將 cmd.Stdin, cmd.Stdout, cmd.Stderr 組織成 *os.File,并依次寫到數組childFiles 中,這個數組索引就對應子進程的 0,1,2 文描術符,即子進程的標準輸入,輸出,錯誤

  1. type F func(*Cmd) (*os.File, error) 
  2. for _, setupFd := range []F{(*Cmd).stdin, (*Cmd).stdout, (*Cmd).stderr} { 
  3.  fd, err := setupFd(c) 
  4.  if err != nil { 
  5.   c.closeDescriptors(c.closeAfterStart) 
  6.   c.closeDescriptors(c.closeAfterWait) 
  7.   return err 
  8.  } 
  9.  c.childFiles = append(c.childFiles, fd) 
  10. c.childFiles = append(c.childFiles, c.ExtraFiles...) 
  11.  
  12. var err error 
  13. c.Process, err = os.StartProcess(c.Path, c.argv(), &os.ProcAttr{ 
  14.  Dir:   c.Dir, 
  15.  Files: c.childFiles, 
  16.  Env:   dedupEnv(c.envv()), 
  17.  Sys:   c.SysProcAttr, 
  18. }) 

在執行 setupFd 時,會有一個關鍵的操作,打開 pipe 管道,封裝一個匿名 func,功能就是將子進程的輸出結果寫到 pipe 或是將 pipe 數據寫到子進程標準輸入,最后關閉 pipe

這個匿名函數最終在 Start 時執行

  1. func (c *Cmd) stdin() (f *os.File, err error) { 
  2.  if c.Stdin == nil { 
  3.   f, err = os.Open(os.DevNull) 
  4.   if err != nil { 
  5.    return 
  6.   } 
  7.   c.closeAfterStart = append(c.closeAfterStart, f) 
  8.   return 
  9.  } 
  10.  
  11.  if f, ok := c.Stdin.(*os.File); ok { 
  12.   return f, nil 
  13.  } 
  14.  
  15.  pr, pw, err := os.Pipe() 
  16.  if err != nil { 
  17.   return 
  18.  } 
  19.  
  20.  c.closeAfterStart = append(c.closeAfterStart, pr) 
  21.  c.closeAfterWait = append(c.closeAfterWait, pw) 
  22.  c.goroutine = append(c.goroutine, func() error { 
  23.   _, err := io.Copy(pw, c.Stdin) 
  24.   if skip := skipStdinCopyError; skip != nil && skip(err) { 
  25.    err = nil 
  26.   } 
  27.   if err1 := pw.Close(); err == nil { 
  28.    err = err1 
  29.   } 
  30.   return err 
  31.  }) 
  32.  return pr, nil 

重新運行測試 case,并用 lsof 查看進程打開了哪些資源

  1. root@nb1963:~/dongzerun# ps aux |grep wait 
  2. root      4531  0.0  0.0 122180  6520 pts/6    Sl   17:24   0:00 ./wait 
  3. root      4726  0.0  0.0  10484  2144 pts/6    S+   17:24   0:00 grep --color=auto wait 
  4. root@nb1963:~/dongzerun# 
  5. root@nb1963:~/dongzerun# ps aux |grep sleep 
  6. root      4543  0.0  0.0   4456   688 pts/6    S    17:24   0:00 /bin/sh /root/dongzerun/sleep.sh 
  7. root      4548  0.0  0.0   4456   760 pts/6    S    17:24   0:00 sh /root/dongzerun/sleep1.sh 
  8. root      4550  0.0  0.0   5928   748 pts/6    S    17:24   0:00 sleep 1000 
  9. root      4784  0.0  0.0  10480  2188 pts/6    S+   17:24   0:00 grep --color=auto sleep 
  10. root@nb1963:~/dongzerun# 
  11. root@nb1963:~/dongzerun# lsof -p 4531 
  12. COMMAND  PID USER   FD   TYPE     DEVICE SIZE/OFF       NODE NAME 
  13. wait    4531 root    0w   CHR        1,3      0t0       1029 /dev/null 
  14. wait    4531 root    1w   REG        8,1    94371    4991345 /root/dongzerun/nohup.out 
  15. wait    4531 root    2w   REG        8,1    94371    4991345 /root/dongzerun/nohup.out 
  16. wait    4531 root    3u  IPv6 2005568215      0t0        TCP *:6060 (LISTEN) 
  17. wait    4531 root    4u  0000       0,10        0       9076 anon_inode 
  18. wait    4531 root    5r  FIFO        0,9      0t0 2005473170 pipe 
  19. root@nb1963:~/dongzerun# lsof -p 4543 
  20. COMMAND   PID USER   FD   TYPE DEVICE SIZE/OFF       NODE NAME 
  21. sleep.sh 4543 root    0r   CHR    1,3      0t0       1029 /dev/null 
  22. sleep.sh 4543 root    1w  FIFO    0,9      0t0 2005473170 pipe 
  23. sleep.sh 4543 root    2w  FIFO    0,9      0t0 2005473170 pipe 
  24. sleep.sh 4543 root   10r   REG    8,1       55    4993949 /root/dongzerun/sleep.sh 
  25. root@nb1963:~/dongzerun# lsof -p 4550 
  26. COMMAND  PID USER   FD   TYPE DEVICE SIZE/OFF       NODE NAME 
  27. sleep   4550 root  mem    REG    8,1  1607664    9179617 /usr/lib/locale/locale-archive 
  28. sleep   4550 root    0r   CHR    1,3      0t0       1029 /dev/null 
  29. sleep   4550 root    1w  FIFO    0,9      0t0 2005473170 pipe 
  30. sleep   4550 root    2w  FIFO    0,9      0t0 2005473170 pipe 

原因總結

孫進程啟動后,默認會繼承父進程打開的文件描述符,即 node 2005473170 的 pipe

那么當父進程被 kill -9 后會清理資源,關閉打開的文件,但是 close 只是引用計數減 1。實際上 孫進程 仍然打開著 pipe?;仡^看 agent 代碼

  1. c.goroutine = append(c.goroutine, func() error { 
  2.  _, err := io.Copy(pw, c.Stdin) 
  3.  if skip := skipStdinCopyError; skip != nil && skip(err) { 
  4.   err = nil 
  5.  } 
  6.  if err1 := pw.Close(); err == nil { 
  7.   err = err1 
  8.  } 
  9.  return err 
  10. }) 

那么當子進程執行結束后,go cmd 執行這個匿名函數的 io.Copy 來讀取子進程輸出數據,永遠沒有數據可讀,也沒有超時,阻塞在 copy 這里

解決方案

原因找到了,解決方法也就有了。

  1. 子進程啟動孫進程時,增加 CloseOnEec 標記,但不現實,還要看孫進程的輸出日志
  2. io.Copy 改寫,增加超時調用,理論上可行,但是要改源碼
  3. 超時 kill, 不單殺子進程,而是殺掉進程組,此時 pipe 會被真正的關閉,觸發 io.Copy 返回

最終采用方案 3,簡化代碼如下,主要改動點有兩處:

SysProcAttr 配置 Setpgid,讓子進程與孫進程,擁有獨立的進程組id,即子進程的 pid

Syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) 殺進程時指定進程組

  1. func Run(instance string, env map[string]string) bool { 
  2.  var ( 
  3.   cmd         *exec.Cmd 
  4.   proc        *Process 
  5.   sysProcAttr *syscall.SysProcAttr 
  6.  ) 
  7.  
  8.  t := time.Now() 
  9.  sysProcAttr = &syscall.SysProcAttr{ 
  10.   Setpgid: true, // 使子進程擁有自己的 pgid,等同于子進程的 pid 
  11.   Credential: &syscall.Credential{ 
  12.    Uid: uint32(uid), 
  13.    Gid: uint32(gid), 
  14.   }, 
  15.  } 
  16.  
  17.  // 超時控制 
  18.  ctx, cancel := context.WithTimeout(context.Background(), time.Duration(j.Timeout)*time.Second
  19.  defer cancel() 
  20.  
  21.  if j.ShellMode { 
  22.   cmd = exec.Command("/bin/bash""-c", j.Command) 
  23.  } else { 
  24.   cmd = exec.Command(j.cmd[0], j.cmd[1:]...) 
  25.  } 
  26.  
  27.  cmd.SysProcAttr = sysProcAttr 
  28.  var b bytes.Buffer 
  29.  cmd.Stdout = &b 
  30.  cmd.Stderr = &b 
  31.  
  32.  if err := cmd.Start(); err != nil { 
  33.   j.Fail(t, instance, fmt.Sprintf("%s\n%s", b.String(), err.Error()), env) 
  34.   return false 
  35.  } 
  36.  
  37.  waitChan := make(chan struct{}, 1) 
  38.  defer close(waitChan) 
  39.  
  40.  // 超時殺掉進程組 或正常退出 
  41.  go func() { 
  42.   select { 
  43.   case <-ctx.Done(): 
  44.    log.Warnf("timeout kill job %s-%s %s ppid:%d", j.Group, j.ID, j.Name, cmd.Process.Pid) 
  45.    syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) 
  46.   case <-waitChan: 
  47.   } 
  48.  }() 
  49.  
  50.  if err := cmd.Wait(); err != nil { 
  51.   j.Fail(t, instance, fmt.Sprintf("%s\n%s", b.String(), err.Error()), env) 
  52.   return false 
  53.  } 
  54.  return true 

但這種方式,也有個局限,目前只適用于類 linux 平臺

小結 

大家也可以看到,只要權限足夠,問題穩定復現,沒有查不出來的問題。套路也都差不多,回歸問題開始,python request 庫不寫 timeout 的比比皆是 ...

 

責任編輯:武曉燕 來源: 董澤潤的技術筆記
相關推薦

2022-07-31 23:05:55

Go語言短變量

2021-06-07 23:51:16

MacGo服務

2021-10-28 19:10:02

Go語言編碼

2016-12-28 13:19:08

Android開發坑和小技巧

2023-04-12 08:18:40

ChatGLM避坑微調模型

2022-01-03 20:13:08

Gointerface 面試

2022-08-08 08:31:55

Go 語言閉包匿名函數

2022-08-08 06:50:06

Go語言閉包

2012-02-09 09:52:39

服務器節能

2021-03-16 08:56:35

Go interface面試

2023-03-13 13:36:00

Go擴容切片

2017-03-31 10:27:08

推送服務移動

2021-01-26 00:46:40

微服務架構微服務應用

2022-05-19 08:56:13

Go提案賦值

2024-04-01 08:05:27

Go開發Java

2021-10-18 21:41:10

Go程序員 Defer

2023-03-06 07:50:19

內存回收Go

2022-11-02 08:55:43

Gofor 循環存儲

2025-01-15 10:44:55

Go泛型接口

2024-09-20 06:00:32

點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 亚洲国产精品一区二区久久 | 亚洲精选久久 | 亚洲精品欧美精品 | 精品一区二区三区在线观看国产 | 久久久久久久久久影视 | 成人一区二区三区在线 | 超碰成人免费观看 | 亚洲国产一区视频 | 午夜电影网站 | 欧美日韩看片 | 男女爱爱网站 | 日韩中文一区二区三区 | 欧美www在线| 欧美一区二区在线观看视频 | 超碰在线网站 | 亚洲午夜视频在线观看 | 国产视频久 | 午夜一级做a爰片久久毛片 精品综合 | 免费观看一级特黄欧美大片 | 欧美a在线| 欧美国产日韩一区二区三区 | 久久久久久黄 | 免费一级网站 | 久久精品av麻豆的观看方式 | 男女网站免费 | 国产精品久久毛片av大全日韩 | 噜噜噜噜狠狠狠7777视频 | 欧洲性生活视频 | 罗宾被扒开腿做同人网站 | av在线免费观看网址 | 日韩精品 电影一区 亚洲 | 99pao成人国产永久免费视频 | 国产精品成人一区二区三区吃奶 | 91国产在线播放 | www.欧美.com| 澳门永久av免费网站 | 成人性生交大免费 | 青娱乐自拍 | 国产精品有限公司 | 午夜激情一区 | 欧美影院|