目录
最近业务新上了一个功能,使用了chromedp
借用官方的demo,几段小小的代码,我们可以实现后台截chrome的图:
package main
import (
"context"
"github.com/chromedp/chromedp"
"io/ioutil"
"log"
)
func main() {
ctx, cancel := chromedp.NewContext(
context.Background(),
)
defer cancel()
var buf []byte
if err := chromedp.Run(ctx, elementScreenshot(`https://pkg.go.dev/`, `img.Homepage-logo`, &buf)); err != nil {
log.Fatal(err)
}
if err := ioutil.WriteFile("elementScreenshot.png", buf, 0o644); err != nil {
log.Fatal(err)
}
}
func elementScreenshot(urlstr, sel string, res *[]byte) chromedp.Tasks {
return chromedp.Tasks{
chromedp.Navigate(urlstr),
chromedp.Screenshot(sel, res, chromedp.NodeVisible),
}
}
看上去很不错,小O很快帮研发同学小D制作了一个可以运行chromedp的基础镜像
# syntax=DOCKER/dockerfile:1.3-labs
FROM centos:7
RUN <<"EOF" cat > /etc/yum.repos.d/google-chrome.repo
[google-chrome]
name=google-chrome
baseurl=http://dl.google.com/linux/chrome/rpm/stable/$basearch
enabled=1
gpgcheck=1
gpgkey=https://dl-ssl.google.com/linux/linux_signing_key.pub
EOF
RUN yum install -y google-chrome-stable --nogpgcheck && yum clean all
(注:上文使用了here-documents ,需大于Docker 18.09,且打开export DOCKER_BUILDKIT=1开启BuildKit mode,方可支持)
dockerfile Here Document 举例(直接书写python代码):
# syntax = docker/dockerfile:1.3-labs
FROM python:3.6
RUN <<eot
#!/usr/bin/env python
print("hello world")
eot
Bash Here Document举例(直接书写文件内容):
cat <<EOF > testfile
hello world!!
EOF
业务很快就跑起来了,结果半夜小O就被告警短信轰炸了
故障描述Pid爆炸至宿主机内存使用率暴涨
处理经过时间 | 处理同学 | 动作 |
17:00 | 小D | 开发代码,上线新功能 |
02:17 | 小O | 被告警吵醒,紧急规避 |
在宿主机上通过top/ps命令立刻就发现了大量僵尸进程
原因很显然了,和下午上线的chromedp脱不了干系。进到容器内:
在Linux操作系统下,一个进程结束后,如果它的父进程没有通过wait系统调用等待它,则它会变成Zombie Process,即图中的defunct状态进程。
When a process ends via exit, all of the memory and resources associated with it are deallocated so they can be reused . However, the process's entry in the process table remains. The parent can read the child's exit status by executing the wait system call, whereupon the zombie is removed. ---wikipedia
wait 系统调用承担了2个功能
正常情况下,如果父进程退出,则所有僵尸进程则会被1号进程收养。1号进程负责wait僵尸进程。显然在容器内部,业务的golang进程不会去回收chrome的进程。
这时我们需要一个容器内真正能做事的1号进程:
业界有以下几个做法
Bash
bash 作为父进程,具备回收子进程的功能,但是它会不透传信号给子进程
CMD ["/bin/bash", "-c", "set -e && top"]
如果写做以下方式,则会有问题
CMD ["/bin/bash", "-c", "top"]
这种形式属于“simple command”,bash内部会做优化而舍弃fork直接exec替换掉bash进程。所以在容器内看到的结果1号进程仍然是业务进程。
Supervisord
成熟的进程管理和守护工具,较重,python实现,根据公司架构情况食用。
systemd
由于一些技术原因,目前在容器内运行systemd需要开启特权模式。
tini
一个非常轻量的init实现。是docker官方原装的init二进制。在docker命令下通过--init参数即可开启,但是在k8s情况下需要将tinit拷贝到镜像当中,在Docker中增加以下即可:
ENV TINI_VERSION v0.19.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
RUN chmod x /tini
ENTRYPOINT ["/tini", "--"]
pause
k8s的基础设施pause容器的pause进程,也可以充当1号进程,只要我们和pause容器共享pid namespace即可。
业务进程回收
对于golang来说,我们可以自己动手回收:
import "golang.org/x/sys/unix"
func ReapChildren() {
c := make(chan os.Signal, 100)
signal.Notify(c, unix.SIGCHLD)
for {
<-c
var status unix.WaitStatus
for {
pid, err := unix.Wait4(-1, &status, unix.WNOHANG, nil)
switch err {
case nil:
if pid > 0 {
fmt.Println("Reap pid", pid)
}
case unix.ECHILD:
// No more children, we are done.
break
case unix.EINTR:
continue
default:
fmt.Println(err)
}
}
}
}
对c语言来说则相对简单一些
static void sigreap(int signo) {
while (waitpid(-1, NULL, WNOHANG) > 0)
;
}
int main(int argc, char **argv) {
sigaction(SIGCHLD, &(struct sigaction){.sa_handler = sigreap,.sa_flags = SA_NOCLDSTOP},NULL)
}
大半夜的,小O只想睡好好觉。直接修改了deployment让pause来回收规避了问题。至于后续小D怎么改随他去吧。
故障思考记录那些年我们一起处理过的故障~
Copyright © 2024 妖气游戏网 www.17u1u.com All Rights Reserved