【生产问题】K8s退出信号处理和僵尸进程问题
接上一篇容器多进程的内容延伸到僵尸进程,也是一个真实的生产问题
- 公司有大量的Python + Selenium爬虫服务,据开发所说一个服务有很多个并行任务
- 一天早上告警类似
Resource temporarily unavailable
的错误,对于这类问题其实只需根据ulimit -a
查看各项资源即可 - 因为确实部分资源使用率指标,所以只能在宿主机查看缺失的资源利用情况,如果只关心进程数直接
ps -aux | wc -l
- 僵尸进程对于多进程服务来说是常有的事,但需要通过一些自动化手段帮助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. 具体思路
- 需要遍历进程找到Z+的进程PID,这一步可以通过/proc/{pid}/stat目录得到我们想用的信息
- 找到他的母进程,看看是不是k8s容器所产生的
- hold住数秒,看看是否还在
- 可以集成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++
}
}
}
|
四、总结