接着上一篇:
golang 实现 linux 进程管理,管理php后台任务 - 第三节:核心代码部分
- 通过golang 实现 进程守护,进程挂掉后,自动拉起新进程
- 所有配置进程一键启动 ,一键关闭
- 进程数量控制,如:php a.php 开启2个
- 多进程数量控制,相同命令配置有多个时,缺少一个时,自动新启动
- 监控配置文件,配置有变化时,自动更新
- 配置分离,测试和线上配置分开
- 程序编译为二进制后体积小: 约4.2M
- 系统开销极小
- 无依赖:一个文件+配置文件即可
本文主要为核心代码部分
cmd/daemon-process.go
package main
import (
"daemon-process/internal/conf/constant"
"daemon-process/internal/engine"
"flag"
)
func init() {
flag.StringVar(&constant.ConfigDir, "configDir", "", "Config path")
flag.BoolVar(&constant.Debug, "debug", false, "Debug mode")
flag.StringVar(&constant.Env, "env", "dev", "env")
flag.Parse()
}
func main() {
app := engine.NewEngine()
app.DaemonProcess.Run()
}
configs/config-dev.yaml
#日志目录
LogDir: "xxxx"
#单位s
Interval: 30
ProcessList:
- ProcessName: "测试"
ProcessNum: 1
Command: "ping www.baidu.com"
User: "root"
- ProcessName: "测试2"
ProcessNum: 1
Command: "ping www.qq.com"
User: "root"
internal/conf/conf.go
package conf
import (
"daemon-process/internal/conf/constant"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
"log"
"sync"
)
type AppConfig struct {
LogDir string // 日志目录
Interval int // 间隔时间,单位秒
ProcessList []ProcessList // 进程列表
}
type ProcessList struct {
ProcessName string `json:"ProcessName"`
ProcessNum int `json:"ProcessNum"`
Command string `json:"Command"`
User string `json:"User"`
}
var (
appConfigInstance *AppConfig
appConfigInstanceOnce sync.Once
)
func GetAppConfigInstance() *AppConfig {
appConfigInstanceOnce.Do(func() {
log.Println("Load config")
var newAppConfig AppConfig
viper.SetConfigName("config-" + constant.Env)
viper.AddConfigPath(constant.ConfigDir)
viper.SetConfigType("yaml") // REQUIRED if the config file does not have the extension in the name
err := viper.ReadInConfig() // Find and read the config file
if err != nil { // Handle errors reading the config file
log.Fatalf("fatal error config file: %+v", err)
}
err = viper.Unmarshal(&newAppConfig)
if err != nil {
log.Fatalf("config Unmarshal err: %+v", err)
}
viper.OnConfigChange(func(e fsnotify.Event) {
log.Println("Config file changed:", e.Name)
err = viper.Unmarshal(&newAppConfig)
if err != nil {
log.Fatalf("config Unmarshal err: %+v", err)
}
appConfigInstance = &newAppConfig
})
viper.WatchConfig()
appConfigInstance = &newAppConfig
})
return appConfigInstance
}
func GetConfig() *AppConfig {
return GetAppConfigInstance()
}
internal/core/daemon/function.go
package daemon
import (
"bufio"
"fmt"
"io"
"os"
"syscall"
)
func read(done chan bool, std io.ReadCloser, p *os.Process) {
reader := bufio.NewReader(std)
for {
select {
case <-done:
return
default:
readString, err := reader.ReadString('\n')
if err != nil || err == io.EOF {
p.Signal(syscall.SIGQUIT)
return
}
fmt.Print(readString)
}
}
}
internal/core/daemon/process.go
package daemon
import (
"context"
"daemon-process/internal/conf"
"daemon-process/internal/utils"
"fmt"
"github.com/shirou/gopsutil/v3/process"
"log"
"os/exec"
"sync"
"time"
)
type Client struct {
AppConfig *conf.AppConfig
}
var (
daemonProcessInstance *Client
daemonProcessInstanceOnce sync.Once
ProcessList map[string]int
)
func NewClient() *Client {
daemonProcessInstanceOnce.Do(func() {
var client Client
client.AppConfig = conf.GetConfig()
daemonProcessInstance = &client
})
return daemonProcessInstance
}
func (sf Client) Run() {
for {
// 获取进程列表
totalErrProcess := 0
processListData := make(map[string]int, 0)
procs, err := process.Processes()
for _, proc := range procs {
// var exeName string
var cmdLine string
exeName, _ := proc.Exe()
cmdLine, err = proc.Cmdline()
if err != nil {
cmdLine = "Error: " + err.Error()
}
if cmdLine == "" {
continue
}
if exeName == "" {
// Error: invalid argument
fmt.Printf("Error_Process #%v: Name: %v / CmdLine: %v\n", proc.Pid, exeName, cmdLine)
// 通过计数判断错误数量,超过10个,则不再进行启动任务,需要解决有问题的任务配置
totalErrProcess++
}
cmdLineKey := utils.Md5(cmdLine)
processListData[cmdLineKey] += 1
}
if totalErrProcess > 10 {
fmt.Println("错误进程太多,需要解决进程配置中有问题的命令,然后执行:", "service vote-webapps restart")
time.Sleep(time.Duration(sf.AppConfig.Interval) * time.Second)
continue
}
ProcessList = processListData
for _, p := range sf.AppConfig.ProcessList {
sf.Task(p)
}
time.Sleep(time.Duration(sf.AppConfig.Interval) * time.Second)
}
}
func (sf Client) Task(cmdCfg conf.ProcessList) {
if cmdCfg.User == "root" {
cmdLineKey := utils.Md5(cmdCfg.Command)
processNum := 0
if data, ok := ProcessList[cmdLineKey]; ok {
processNum = data
}
for i := processNum; i < cmdCfg.ProcessNum; i++ {
log.Printf("启动进程: %s , 执行命令: %s ,进程数量: %d \n", cmdCfg.ProcessName, cmdCfg.Command, cmdCfg.ProcessNum)
go sf.execWithContext(cmdCfg)
time.Sleep(50 * time.Millisecond)
}
} else {
// sf.execWithUser(cmdCfg)
}
}
func (sf Client) execWithContext(cmdCfg conf.ProcessList) error {
ctx, _ := context.WithCancel(context.Background())
cmd := exec.CommandContext(ctx, "/bin/bash", "-c", cmdCfg.Command)
// cmd := exec.Cmd{}
args := cmd.Args
if args == nil {
args = []string{cmd.Path}
}
stdout, err := cmd.StdoutPipe()
if err != nil {
return err
}
stderr, err := cmd.StderrPipe()
if err != nil {
return err
}
if err := cmd.Start(); err != nil {
log.Printf("starting %s: %v", args, err)
}
p := cmd.Process
done := make(chan bool)
go read(done, stderr, p)
go read(done, stdout, p)
err = cmd.Wait()
if err != nil {
log.Printf("%s exit status: %v", args, err)
}
close(done)
return nil
}