多进程 daemon进程和优雅重启
本文会详细介绍主流的daemon进程的实现方案,以及网络编程中如何实现优雅重启,这些都是多进程的一些编程技巧!
如何创建daemon进程
我们平时做服务器开发都是启动一个程序,这个程序是一个前台程序,但是前台程序它一直在那开着,我想让他后台运行,例如mysql的server,那么怎么解决呢,我自己怎么实现一个daemon进程呢?
- systemctl 是linux最常见的手段,它需要软件定义一个 .service 文件,来定义和管理 软件的等 www.freedesktop.org/software/sy…
- 可以用
systemctl status
查看所有 systemctl 的进程
~ cat /lib/systemd/system/docker.service
[Unit]
Description=Docker Application Container Engine
Documentation=https://docs.docker.com
After=network-online.target docker.socket firewalld.service
Wants=network-online.target
Requires=docker.socket
[Service]
Type=notify
1. the default is not to use systemd for cgroups because the delegate issues still
1. exists and systemd currently does not support the cgroup feature set required
1. for containers run by docker
ExecStart=/usr/bin/dockerd -H fd://
ExecReload=/bin/kill -s HUP $MAINPID
LimitNOFILE=1048576
1. Having non-zero Limit*s causes performance problems due to accounting overhead
1. in the kernel. We recommend using cgroups to do container-local accounting.
LimitNPROC=infinity
LimitCORE=infinity
1. Uncomment TasksMax if your systemd version supports it.
1. Only systemd 226 and above support this version.
TasksMax=infinity
TimeoutStartSec=0
1. set delegate yes so that systemd does not reset the cgroups of docker containers
Delegate=yes
1. kill only the docker process, not all processes in the cgroup
KillMode=process
1. restart the docker process if it exits prematurely
Restart=on-failure
StartLimitBurst=3
StartLimitInterval=60s
[Install]
WantedBy=multi-user.target
其实大概描述了,当前软件详情信息,如何启动当前软件等,具体可以参考这个文章 segmentfault.com/a/119000002…
-
nohup 就更简单了,只需要 nohup 命令一下即可,其实它所做的就更简单了,就是一个后台运行,并不会涉及到重启等操作
-
supervisor 我个人感觉就和 systemctl差不多
如何实现一个daemon进程
首先需要了解一个进程的机制,例如我们在shell里执行了一个命令,那么整体流程是,shell进程启动了我们的进程,那么我们当前进程的父亲进程就是 shell 进程,当我们把shell进程关了,那么我们的进程也没了!
下图是我写了一个 Go代码,其实就是解释了上面说的!整个进程的关系!
根据上面描述基本无解了,那么怎么办呢,实际上这里就需要用到孤儿进程,孤儿进程的父进程ID是1,他的回收权就转移给了init进程(进程ID为1),那么如何创建一个孤儿进程了!
其实很简单,就是当父进程退出,子进程还在运行,此时子进程就是孤儿进程了,孤儿进程的生命周期会被系统决定!
具体文章可以看: blog.csdn.net/a745233700/…
简单实现一个daemon进程
这个例子是实现一个 http 服务的daemon进程,直接运行后会后台启动一个http服务!
package main
import (
"fmt"
"net/http"
"os"
"path/filepath"
)
func main() {
fmt.Printf("当前ppid: %v, pid: %v, args: %#vn", os.Getppid(), os.Getpid(), os.Args)
if len(os.Args) > 1 && os.Args[1] == "child_process" { // 子进程
fmt.Println("child process")
if err := http.ListenAndServe(":10099", http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
fmt.Printf("method: %s, url: %sn", request.Method, request.URL)
_, _ = writer.Write([]byte(`hello world`))
})); err != nil {
panic(err)
}
return
}
executable, err := os.Executable()
if err != nil {
panic(err)
}
dir, err := os.Getwd()
if err != nil {
panic(err)
}
fmt.Printf("dir: %sn", dir)
stdout, err := os.OpenFile(filepath.Join(dir, "child_process.log"), os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
panic(err)
}
process, err := os.StartProcess(executable, []string{os.Args[0], "child_process"}, &os.ProcAttr{
Dir: dir,
Files: []*os.File{ // 共享fd
os.Stdin, // stdin
stdout, // stdout
stdout, // std error
},
})
if err != nil {
panic(err)
}
pidFile, err := os.OpenFile(filepath.Join(dir, "child_process.pid"), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
panic(err)
}
defer pidFile.Close()
if _, err := pidFile.WriteString(fmt.Sprintf("%d", process.Pid)); err != nil {
panic(err)
}
fmt.Printf("create child process %dn", process.Pid)
return
// 不执行这个,直接return就是孤儿进程了
if _, err := process.Wait(); err != nil {
panic(err)
}
}
我们成功实现了一个 孤儿进程, 如何结束孤儿进程了, 直接 kill 孤儿进程的进程ID即可
➜ test git:(master) ✗ ps -ef | grep './main'
502 48190 1 0 2:42PM ttys052 0:00.01 ./main child_process
502 48312 43352 0 2:42PM ttys052 0:00.00 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn --exclude-dir=.idea --exclude-dir=.tox ./main
➜ test git:(master) ✗ kill `cat child_process.pid`
封装 daemon 进程
这里我就不造轮子了,大概可以看一下 github.com/sevlyar/go-… 这个项目,我大概介绍一些几个方法的核心原理
func (s *DaemonService) newCtx() *daemon.Context {
return &daemon.Context{
PidFileName: filepath.Join(s.homeDir, PidFile), // pid所在文件,主要是解决如何获取子进程pid的问题
PidFilePerm: 0644,
LogFileName: filepath.Join(s.homeDir, LogFile), // 替换子进程的stdout/stderr
LogFilePerm: 0644,
WorkDir: s.homeDir, // 子进程工作目录
Umask: 027, // 文件权限,有兴趣可以查一下
Args: os.Args, // 子进程参数
}
}
func (s *DaemonService) DaemonStart() error {
ctx := s.newCtx()
// Search
// 其实就是读取pid文件,判断进程是否存在
search, err := ctx.Search()
if err == nil && search != nil {
return fmt.Errorf(`the background program has started. PID: %d`, search.Pid)
}
// Reborn
// 如果是父进程,则返回 child process
// 如果是子进程,则返回 空 (判断父子进程逻辑很简单就是根据环境变量 _GO_DAEMON=1,子进程会被注入这个环境变量)
childProcess, err := ctx.Reborn()
if err != nil {
return fmt.Errorf("unable to run background program, reason: %v", err)
}
// 子进程不为空,说明是父进程,直接退出即可(这里子进程就是孤儿进程了)
if childProcess != nil {
logs.Infof("start parent process success. pid: %d, cpid: %d", os.Getpid(), childProcess.Pid)
return nil
}
defer func() {
_ = ctx.Release() // 释放一些当时创建时分配的资源
}()
logs.Infof("start child process success. ppid: %d, pid: %d", os.Getppid(), os.Getpid())
return s.run()
}
实现优雅重启tcp服务
造个轮子
这个例子,我是主进程正常创建和监听TCPListener
, 当需要重启的时候此时需要关闭 TCPListener 然后创建子进程继续监听,当再监听到重启时同样的需要主进程关闭子进程!
package main
import (
"fmt"
"net"
"net/http"
"os"
"os/exec"
"os/signal"
"strings"
"sync"
"syscall"
"time"
)
func main() {
var name string
var err error
var listen net.Listener
// 如果是子进程的话,listen 获取不太一样
if os.Getenv("is_slave") == "true" {
file := os.NewFile(uintptr(3), "")
listen, err = net.FileListener(file)
name = fmt.Sprintf("slave-%d", os.Getpid())
} else {
listen, err = net.Listen("tcp", ":10086")
name = "master"
}
if err != nil {
panic(fmt.Errorf("init (%s) listen err: %v", name, err))
}
debug("[%s] start", name)
go func() {
if isSlave(name) {
return
}
var listenFd *os.File
var loadFD = sync.Once{}
loadListenFd := func() *os.File {
loadFD.Do(func() {
tl := listen.(*net.TCPListener)
fds, err := tl.File()
if err != nil {
panic(fmt.Errorf("tl.File() find err: %vn", err))
}
if err := listen.Close(); err != nil { // 只需要关闭一次,所以用sync.once
panic(err)
}
listenFd = fds
})
return listenFd
}
// 父亲进程watch 变更
debug("[%s] watch file changed", name)
var command *exec.Cmd
var done chan bool
var errch chan error
for {