【生产问题】K8s退出信号处理和僵尸进程问题


接上一篇容器多进程的内容延伸到僵尸进程,也是一个真实的生产问题

  1. 公司有大量的Python + Selenium爬虫服务,据开发所说一个服务有很多个并行任务
  2. 一天早上告警类似Resource temporarily unavailable的错误,对于这类问题其实只需根据ulimit -a查看各项资源即可
  3. 因为确实部分资源使用率指标,所以只能在宿主机查看缺失的资源利用情况,如果只关心进程数直接ps -aux | wc -l
  4. 僵尸进程对于多进程服务来说是常有的事,但需要通过一些自动化手段帮助k8s清理宿主机僵尸进程

一、什么是僵尸进程

通常来说就是,在 Unix-like 操作系统中已经完成执行(终止)但仍然保留在系统进程表中的进程记录。 这种状态的进程实际上已经停止运行,不占用除进程表外的任何资源,比如CPU和内存, 但它仍然保留了一个PID和终止状态信息,等待父进程通过调用wait()waitpid()函数来进行回收。

1. 生命周期

2. 系统对僵尸进程的处理

如果父进程没有清理僵尸进程,而该父进程最终也终止了,那么任何仍然存在的僵尸进程将被init进程(PID 为 1 的进程)接管。 init 进程将周期性地调用wait()来清理任何僵尸状态的子进程,从而保证系统进程表的整洁。

3. 用Golang写一个僵尸进程示例

其实就是Start()Run()这两个东西

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import (
	"os/exec"
	"time"
)

func main() {
	cmd := exec.Command("ls")
	cmd.Start()
	time.Sleep(100 * time.Second)
}

直接运行上面程序的话,看ps auxf就能看到ls命令状态变成了Z+

1
2
3
4
5
6
7
root@k8s-master01:~# ps auxf |grep Z
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root      124345  0.0  0.0      0     0 pts/0    Z+   17:21   0:00  |               \_ [ls] <defunct>
root      124348  0.0  0.0   6480  2228 pts/1    S+   17:21   0:00  |       \_ grep --color=auto Z
root@k8s-master01:~# ps -ef |grep defunct
root      124345  124340  0 17:21 pts/0    00:00:00 [ls] <defunct>
root      124374  106213  0 17:21 pts/1    00:00:00 grep --color=auto defunct

Golang中如果要避免这种情况当然可以用Run(),只是主线程就不再是异步了

1
2
3
4
5
6
7
8

// Run比 Start会多了一个 wait
func (c *Cmd) Run() error {
    if err := c.Start(); err != nil {
        return err
    }
    return c.Wait()
}

这里只是举个例子,实际编程可能会复杂很多

二、K8s中的僵尸进程

可能有人会疑惑,PID不是也有自己的命名空间吗?容器中产生的僵尸进程,为什么会占用宿主机的进程表呢? 尽管容器通过各种命名空间进行隔离,但所有的容器进程依然注册在宿主机的全局进程表中。可以做个实验看看

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func main() {
	num := 0
	for num < 36000 {
		cmd := exec.Command("ls")
		cmd.Start()

		time.Sleep(1 * time.Second)
		num++
	}

}

将这段程序部署在k8s

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
root@k8s-master01:~/defunct# kubectl get pod  -o wide |grep span
go-defunct-span                            1/1     Running   0             3s      10.250.85.225   k8s-node01     <none>           <none>

# 再回到Node上查看进程就一目了然了
root@k8s-node01:~# ps -ef |grep defunct
root       59042   58985  0 18:23 ?        00:00:00 ./defunct-span
root       59058   59042  0 18:23 ?        00:00:00 [ls] <defunct>
root       59061   59042  0 18:23 ?        00:00:00 [ls] <defunct>
root       59063   59042  0 18:23 ?        00:00:00 [ls] <defunct>
root       59064   59042  0 18:23 ?        00:00:00 [ls] <defunct>
root       59065   59042  0 18:23 ?        00:00:00 [ls] <defunct>
root       59066   59042  0 18:23 ?        00:00:00 [ls] <defunct>
root       59071   59042  0 18:23 ?        00:00:00 [ls] <defunct>
root       59072   59042  0 18:23 ?        00:00:00 [ls] <defunct>
root       59074   49682  0 18:23 pts/0    00:00:00 grep --color=auto defunct

三、如何处理集群中产生的僵尸进程?

正如上文所说,僵尸进程危害还是比较明显的,尤其是对于多进程服务来说。所以我们需要有个后台程序来定期处理集群节点的僵尸进程,避免需要重启节点的可能性。

1. 具体思路

  1. 需要遍历进程找到Z+的进程PID,这一步可以通过/proc/{pid}/stat目录得到我们想用的信息
  2. 找到他的母进程,看看是不是k8s容器所产生的
  3. hold住数秒,看看是否还在
  4. 可以集成alert了,最后kill掉containerd-shim进程,让容器重启

2. 代码实现

扫描/proc/{pid}/stat目录我们可以直接挪用prometheus的工具库github.com/prometheus/procfs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
func (c *Cleaner) Scan(ctx context.Context) {
    allFs, err := procfs.NewFS("/proc")
    if err != nil {
        klog.Errorf("打开/proc目录错误:%v nodeName:%v", err, c.nodeName)
        return
    }
    // 获取进程
    allPs, err := allFs.AllProcs()
    if err != nil {
        klog.Errorf("获取进程错误:%v  nodeName:%v", err, c.nodeName)
        return
    }
    allStatMap := sync.Map{}
    zStatMap := sync.Map{}
    dStatMap := sync.Map{}
    for _, p := range allPs {
        oneStat, err := p.Stat()
        if err != nil {
			continue
        }
        allStatMap.Store(p.PID, oneStat)
        switch oneStat.State {
        case "Z":
            zStatMap.Store(p.PID, oneStat)
            zNum++
        case "D":
            dStatMap.Store(p.PID, oneStat)
            dNum++
        }
    }
}

四、总结