使用Go编写一个进程监控软件
不怕服务器挂点,自动重启

使用Go编写一个进程监控软件

有这样一个需求:服务器上需要开启多个服务器程序,并且需要统计每一个程序的运行情况,如果某一个程序出现崩溃,需要重新启动该程序。 从需求上看:

  1. 查看进程中运行了哪些程序,并把已运行中的程序进行记录。
  2. 如果设定的程序没有开启,则重新开启程序。
  3. 监控所有的程序运行状态,并记录程序的cpu占用和内存占用。

获取进程

安装进程库文件

go get “github.com/shirou/gopsutil/v3/process”

	processes, err := process.Processes()
	if err != nil {
		fmt.Println(err.Error())
		return
	}
	for _, proc := range processes {
        //遍历所有的进程
	}

根据端口判断程序是否正常运行

获取端口的方法可以使用系统自带的netstat命令

linux:

statnet -tunlp

Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 127.0.0.1:43879         0.0.0.0:*               LISTEN      1227/node
tcp        0      0 127.0.0.1:3306          0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:33060         0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:6379          0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN      -
tcp6       0      0 ::1:6379                :::*                    LISTEN      -
tcp6       0      0 :::17758                :::*                    LISTEN      2429/./linker
tcp6       0      0 :::17759                :::*                    LISTEN      2429/./linker

windows:

statnet -aon

 协议  本地地址          外部地址        状态           PID
  TCP    0.0.0.0:135            0.0.0.0:0              LISTENING       1364
  TCP    0.0.0.0:445            0.0.0.0:0              LISTENING       4
  TCP    0.0.0.0:2179           0.0.0.0:0              LISTENING       2268
  TCP    0.0.0.0:5040           0.0.0.0:0              LISTENING       6192
  TCP    0.0.0.0:7680           0.0.0.0:0              LISTENING       18016

解析数据分析出端口和pid

//按行解析数据,将tcp或者TCP开头的数据进行解析
// 移除字符串中多余的空格
func MoveMoreSpace(origin string) string {
	buf := []byte(origin)

	writer := bytes.NewBuffer([]byte{})
	space_index := -1
	get_letter := false

	for i := 0; i < len(buf); i++ {
		if buf[i] == ' ' {
			if space_index == -1 && get_letter {
				writer.WriteByte(buf[i])
				space_index = i
			}
		} else {
			get_letter = true
			if space_index != -1 {
				space_index = -1
			}
			writer.WriteByte(buf[i])
		}
	}
	return writer.String()
}

//获取系统版本
func GetOS() {
	env := os.Getenv("OS")
	if strings.HasPrefix(env, "Windows") {
		fmt.Println("Windows environment")
		w.netArgs = []string{"-aon"}
		w.is_linux = false
	} else {
		fmt.Println("unix environment netstat -tunlp")
		w.netArgs = []string{"-tunlp"}
		w.is_linux = true
	}
}

//检测所有监控的程序
func (w *Watcher) CheckNetListen() {
	cmd := exec.Command("netstat", w.netArgs...) 
	out, err := cmd.StdoutPipe()
	if err != nil {
		panic(err.Error())
	}

	err = cmd.Start()
	if err != nil {
		fmt.Println(err.Error())
	}
	s := bufio.NewScanner(out)
	for s.Scan() {
		head_str := string(s.Bytes())
		head_str = utils.MoveMoreSpace(head_str)

		splits := strings.Split(head_str, " ")

        if splits[0] == "TCP" || splits[0] == "tcp" {
        if w.is_linux {
            lastIndex := strings.LastIndex(splits[3], ":")
            if lastIndex != -1 {
                port, err := strconv.Atoi(splits[3][lastIndex+1:])
                if err != nil {
                    log.Println(err.Error())
                }
                if _, ok := w.MapExec[port]; ok {
                    pid_str := strings.Split(splits[len(splits)-1], "/")[0]

                    pid, err := strconv.Atoi(pid_str)
                    if err != nil {
                        log.Println(err.Error())
                    }
                    w.CheckExitsPid[port] = pid
                    delete(w.MapExec, port)
                }
            }
        } else {
            ip_splits := strings.Split(splits[1], ":")
            if len(ip_splits) == 2 {
                port, err := strconv.Atoi(ip_splits[1])
                if err != nil {
                    log.Fatal(err.Error())
                }
                if _, ok := w.MapExec[port]; ok {
                    pid, err := strconv.Atoi(splits[len(splits)-1])
                    if err != nil {
                        log.Println(err.Error())
                    }
                    w.CheckExitsPid[port] = pid
                    delete(w.MapExec, port)
                }
            }
        }		
	}
	cmd.Wait()
}

启动服务程序

//进程文件 
type (
	ProcessData struct {
		Exec string     //程序目录
		Args []string   //参数
		Port int        //端口 
	}
)

func (p *ProcStarter) startProcess(info utils.ProcessData) {
	// 创建日志文件
	_, err := os.Stat("log")
	if err != nil {
		fmt.Println(err.Error())
		os.Mkdir("log", os.ModePerm)
	} else {
		fmt.Println("exists")
	}
    //检测尚未启动的进程,并且启动他
	if _, ok := p.MapPortProc[info.Port]; !ok {
		name := strings.Split(info.Exec, ".")[0]
		idx := strings.LastIndex(info.Exec, "/")
		if idx != -1 {
			name = strings.Split(info.Exec[idx+1:], ".")[0]
		}
		out_f, err := os.OpenFile(fmt.Sprintf("log/%s_%d_%s-out.log", name, info.Port, time.Now().Format("20060102150405")), os.O_WRONLY|os.O_CREATE, 0666)
		if err != nil {
			fmt.Println(err.Error())
		}
		err_f, err := os.OpenFile(fmt.Sprintf("log/%s_%d_%s-error.log", name, info.Port, time.Now().Format("20060102150405")), os.O_WRONLY|os.O_CREATE, 0666)
		if err != nil {
			fmt.Println(err.Error())
		}

		procAttr := &os.ProcAttr{
			Dir: "E:\\go\\src\\kirin_go_combine\\game\\pochiko", //执行目录, 如果Exec传递的是绝对路径,这里可以不用传递
			Env: p.env,
			Sys: &syscall.SysProcAttr{
				HideWindow: false,
			},
			Files: []*os.File{
				nil,
				out_f,
				err_f,
			},
		}

		proc, err := os.StartProcess(info.Exec, []string{info.Exec, "-port", strconv.Itoa(info.Port)}, procAttr)
		if err != nil {
			fmt.Printf("Error %v starting process!", err) //
		}

		checkMap := make(map[int]int)
		checkMap[info.Port] = int(proc.Pid)
		p.CheckExistsProcess(checkMap)
	}
}

检测程序是否已经挂了(监控程序的cpu和内存占用)

RSS(Resident set size)实际使用物理内存(包含共享库占用的内存) 如果占用内存过低,可以认为此进程已经挂了

func (p *ProcStarter) MonitorRes(check chan struct{}) {
	ticker := time.NewTicker(time.Second * 30)
	for {
		<-ticker.C
		rm := make([]int, 0)
		for port, v := range p.MapPortProc {
			merinfo, err := v.MemoryInfo()
			if err != nil {
				fmt.Println(err.Error())
				rm = append(rm, port)
				continue
			}

			if merinfo.RSS < 100_0000 { //进程应该挂了
				rm = append(rm, port)
				continue
			}

			cpu, err := v.CPUPercent()
			if err != nil {
				fmt.Println(err.Error())
				rm = append(rm, port)
				continue
			}

			memer, err := v.MemoryPercent()
			if err != nil {
				fmt.Println(err.Error())
				rm = append(rm, port)
				continue
			}

			fmt.Println(port, cpu, memer, merinfo)
		}
		if len(rm) > 0 {
			for _, port := range rm {
				delete(p.MapPortProc, port)
			}
			check <- struct{}{}
		}
	}
}

GitHub地址: https://github.com/lixxix/go_monitor


最后修改于 2023-08-25