自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Go Cmd 服務(wù)無法退出的小坑

開發(fā) 后端
上家公司的案例。先說下使用背景,服務(wù)在每臺(tái)服務(wù)器上啟動(dòng) agent, 用戶會(huì)在指定機(jī)器上執(zhí)行任務(wù),并將結(jié)果返回到網(wǎng)頁上。執(zhí)行任務(wù)由用戶自定義腳本,一般也都是 shell 或是 python,會(huì)不斷的產(chǎn)生子進(jìn)程,孫進(jìn)程,直到執(zhí)行完畢或是超時(shí)被 kill。

[[409900]]

本文轉(zhuǎn)載自微信公眾號(hào)「董澤潤(rùn)的技術(shù)筆記」,作者董澤潤(rùn)。轉(zhuǎn)載本文請(qǐng)聯(lián)系董澤潤(rùn)的技術(shù)筆記公眾號(hào)。

上家公司的案例。先說下使用背景,服務(wù)在每臺(tái)服務(wù)器上啟動(dòng) agent, 用戶會(huì)在指定機(jī)器上執(zhí)行任務(wù),并將結(jié)果返回到網(wǎng)頁上。執(zhí)行任務(wù)由用戶自定義腳本,一般也都是 shell 或是 python,會(huì)不斷的產(chǎn)生子進(jìn)程,孫進(jìn)程,直到執(zhí)行完畢或是超時(shí)被 kill。

問題

最近發(fā)現(xiàn)經(jīng)常有任務(wù),一直處于運(yùn)行中,但實(shí)際上己經(jīng)超時(shí)被 kill,并未將輸出寫到系統(tǒng),看不到任務(wù)的執(zhí)行情況

登錄機(jī)器,發(fā)現(xiàn)執(zhí)行腳本進(jìn)程己經(jīng)殺掉,但是有子腳本卡在某個(gè) http 調(diào)用。再看下這個(gè)腳本,python requests 默認(rèn)沒有設(shè)置超時(shí)...

總結(jié)一下現(xiàn)象:agent 用 go cmd 啟動(dòng)子進(jìn)程,子進(jìn)程還會(huì)啟動(dòng)孫進(jìn)程,孫進(jìn)程因某種原因阻塞。此時(shí),如果子進(jìn)程因超時(shí)被 agent kill 殺掉, agent 卻仍然處于 wait 狀態(tài)

復(fù)現(xiàn)

環(huán)境是 go version go1.16.5 linux/amd64, agent 使用 exec.CommandContext 啟動(dòng)任務(wù),設(shè)置 ctx 超時(shí) 30s,并將結(jié)果寫到 bytes.Buffer, 最后打印。簡(jiǎn)化例子如下:

  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()) 

這個(gè)是 sleep.sh,模擬子進(jìn)程

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

這是 sleep1.sh 模擬孫進(jìn)程,sleep 1000 阻塞在這里

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

###現(xiàn)象 啟動(dòng)測(cè)試 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 成了孤兒進(jìn)程,被 init 1 認(rèn)領(lǐng),但是 28543 wait 并沒有退出,那他在做什么???

分析

這個(gè)時(shí)候僵住了,祭出我們的 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 時(shí),會(huì)先執(zhí)行 waitid, 阻塞在這里,然后再來一次 wait4 等待最終退出結(jié)果

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

打開 golang pprof, 再次運(yùn)行程序,并 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 數(shù)據(jù)。那么 errch 是如何生成的呢?

查看 cmd.Start 源碼,go 將 cmd.Stdin, cmd.Stdout, cmd.Stderr 組織成 *os.File,并依次寫到數(shù)組childFiles 中,這個(gè)數(shù)組索引就對(duì)應(yīng)子進(jìn)程的 0,1,2 文描術(shù)符,即子進(jìn)程的標(biāo)準(zhǔn)輸入,輸出,錯(cuò)誤

  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. }) 

在執(zhí)行 setupFd 時(shí),會(huì)有一個(gè)關(guān)鍵的操作,打開 pipe 管道,封裝一個(gè)匿名 func,功能就是將子進(jìn)程的輸出結(jié)果寫到 pipe 或是將 pipe 數(shù)據(jù)寫到子進(jìn)程標(biāo)準(zhǔn)輸入,最后關(guān)閉 pipe

這個(gè)匿名函數(shù)最終在 Start 時(shí)執(zhí)行

  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 

重新運(yùn)行測(cè)試 case,并用 lsof 查看進(jìn)程打開了哪些資源

  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 

原因總結(jié)

孫進(jìn)程啟動(dòng)后,默認(rèn)會(huì)繼承父進(jìn)程打開的文件描述符,即 node 2005473170 的 pipe

那么當(dāng)父進(jìn)程被 kill -9 后會(huì)清理資源,關(guān)閉打開的文件,但是 close 只是引用計(jì)數(shù)減 1。實(shí)際上 孫進(jìn)程 仍然打開著 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. }) 

那么當(dāng)子進(jìn)程執(zhí)行結(jié)束后,go cmd 執(zhí)行這個(gè)匿名函數(shù)的 io.Copy 來讀取子進(jìn)程輸出數(shù)據(jù),永遠(yuǎn)沒有數(shù)據(jù)可讀,也沒有超時(shí),阻塞在 copy 這里

解決方案

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

  1. 子進(jìn)程啟動(dòng)孫進(jìn)程時(shí),增加 CloseOnEec 標(biāo)記,但不現(xiàn)實(shí),還要看孫進(jìn)程的輸出日志
  2. io.Copy 改寫,增加超時(shí)調(diào)用,理論上可行,但是要改源碼
  3. 超時(shí) kill, 不單殺子進(jìn)程,而是殺掉進(jìn)程組,此時(shí) pipe 會(huì)被真正的關(guān)閉,觸發(fā) io.Copy 返回

最終采用方案 3,簡(jiǎn)化代碼如下,主要改動(dòng)點(diǎn)有兩處:

SysProcAttr 配置 Setpgid,讓子進(jìn)程與孫進(jìn)程,擁有獨(dú)立的進(jìn)程組id,即子進(jìn)程的 pid

Syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) 殺進(jìn)程時(shí)指定進(jìn)程組

  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, // 使子進(jìn)程擁有自己的 pgid,等同于子進(jìn)程的 pid 
  11.   Credential: &syscall.Credential{ 
  12.    Uid: uint32(uid), 
  13.    Gid: uint32(gid), 
  14.   }, 
  15.  } 
  16.  
  17.  // 超時(shí)控制 
  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.  // 超時(shí)殺掉進(jìn)程組 或正常退出 
  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 

但這種方式,也有個(gè)局限,目前只適用于類 linux 平臺(tái)

小結(jié) 

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

 

責(zé)任編輯:武曉燕 來源: 董澤潤(rùn)的技術(shù)筆記
相關(guān)推薦

2022-07-31 23:05:55

Go語言短變量

2021-06-07 23:51:16

MacGo服務(wù)

2021-10-28 19:10:02

Go語言編碼

2016-12-28 13:19:08

Android開發(fā)坑和小技巧

2022-08-08 06:50:06

Go語言閉包

2023-04-12 08:18:40

ChatGLM避坑微調(diào)模型

2022-01-03 20:13:08

Gointerface 面試

2022-08-08 08:31:55

Go 語言閉包匿名函數(shù)

2012-02-09 09:52:39

服務(wù)器節(jié)能

2021-03-16 08:56:35

Go interface面試

2023-03-13 13:36:00

Go擴(kuò)容切片

2017-03-31 10:27:08

推送服務(wù)移動(dòng)

2021-01-26 00:46:40

微服務(wù)架構(gòu)微服務(wù)應(yīng)用

2022-05-19 08:56:13

Go提案賦值

2024-04-01 08:05:27

Go開發(fā)Java

2021-10-18 21:41:10

Go程序員 Defer

2022-11-02 08:55:43

Gofor 循環(huán)存儲(chǔ)

2023-03-06 07:50:19

內(nèi)存回收Go

2024-09-20 06:00:32

2025-01-15 10:44:55

Go泛型接口
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)